惯性聚合 高效追踪和阅读你感兴趣的博客、新闻、科技资讯
阅读原文 在惯性聚合中打开

推荐订阅源

Microsoft Azure Blog
Microsoft Azure Blog
V
V2EX
博客园 - 【当耐特】
WordPress大学
WordPress大学
爱范儿
爱范儿
美团技术团队
宝玉的分享
宝玉的分享
钛媒体:引领未来商业与生活新知
钛媒体:引领未来商业与生活新知
小众软件
小众软件
量子位
Hugging Face - Blog
Hugging Face - Blog
B
Blog RSS Feed
Recorded Future
Recorded Future
Engineering at Meta
Engineering at Meta
雷峰网
雷峰网
CTFtime.org: upcoming CTF events
CTFtime.org: upcoming CTF events
M
MIT News - Artificial intelligence
freeCodeCamp Programming Tutorials: Python, JavaScript, Git & More
博客园 - 聂微东
H
Hackread – Cybersecurity News, Data Breaches, AI and More
腾讯CDC
大猫的无限游戏
大猫的无限游戏
Jina AI
Jina AI
博客园 - 叶小钗
GbyAI
GbyAI
Y
Y Combinator Blog
Cyber Security Advisories - MS-ISAC
Cyber Security Advisories - MS-ISAC
F
Full Disclosure
G
Google Developers Blog
D
Docker
T
Tailwind CSS Blog
C
Check Point Blog
Last Week in AI
Last Week in AI
人人都是产品经理
人人都是产品经理
T
The Blog of Author Tim Ferriss
B
Blog
博客园 - 三生石上(FineUI控件)
博客园 - Franky
H
Help Net Security
MyScale Blog
MyScale Blog
U
Unit 42
D
DataBreaches.Net
奇客Solidot–传递最新科技情报
奇客Solidot–传递最新科技情报
I
InfoQ
阮一峰的网络日志
阮一峰的网络日志
The GitHub Blog
The GitHub Blog
L
LangChain Blog
有赞技术团队
有赞技术团队
Martin Fowler
Martin Fowler
Microsoft Security Blog
Microsoft Security Blog

美团技术团队

美团 · 技术团队 美团 · 技术团队 美团 · 技术团队 美团 · 技术团队 美团 · 技术团队 美团 · 技术团队 美团发布基于 N-gram 全新模型:嵌入扩展新范式,实现轻量化 MoE 高效进化 2025美团技术年货,「马」上到来 多维创新打造强泛化智能体模型,LongCat-Flash-Thinking-2601技术报告发布 美团 EvoCUA 刷新开源 SOTA,会用电脑还会持续进化的智能体! 美团 LongCat-Flash-Thinking-2601 发布,工具调用能力登顶开源 SOTA! KuiTest:基于大模型通识的 UI 交互遍历测试 AAAI 2026 | 美团技术团队学术论文精选 2025 | 美团技术团队热门技术文章汇总 美团 LongCat-Video-Avatar 正式发布,实现开源 SOTA 级拟真表现 大模型剪枝新范式:先浓缩,再剪枝——DenoiseRotator技术解读 LongCat 上线 AI 生图!精准高效,AI 创作不设限 美团发布 LongCat-Image 图像生成模型,编辑能力登顶开源 SOTA AI Coding与单元测试的协同进化:从验证到驱动 R-HORIZON:探索长程推理边界,复旦NLP&美团LongCat联合提出LRMs能力评测新框架 美团 LongCat 发布 AMO-Bench:突破 AIME 评测饱和困境,重新定义 LLM 数学上限 美团 LongCat Interaction 团队发布大模型交互系统技术报告 WOWService 美团 LongCat 团队发布全模态一站式评测基准 UNO-Bench 美团开源LongCat-Audio-Codec,高效语音编解码器助力实时交互落地 NeurIPS 2025 | 美团技术团队论文精选 LongCat-Flash-Omni正式发布并开源:开启全模态实时交互时代 美团 LongCat 团队发布 VitaBench:基于复杂生活场景的交互式 Agent 评测基准 LongCat-Video 视频生成模型正式发布,探索世界模型的第一步 ICCV 2025 | 美团论文精选及多模态推理竞赛冠军方法分享 从0到1建设美团数据库容量评估系统 可验证过程奖励在提升大模型推理效率中的探索与实践 LongCat-Flash-Thinking 正式发布,更强、更专业,保持极速! 开源 | InfiniteTalk:无限长虚拟人视频生成的新范式 美团正式发布并开源 LongCat-Flash-Chat,动态计算开启高效 AI 时代 美团 M17 团队开源 Meeseeks 评测集:揭秘大模型的“听话”能力 可信实验白皮书系列08:开放式分析引擎 | 附PDF合集 美团智能头盔研发实践系列02:软件功能篇 美团智能头盔研发实践系列01:硬件设计篇 ACL 2025 | 美团技术团队论文精选 美团开源OIBench与CoreCodeBench:揭示大模型编程能力的真实水平 可信实验白皮书系列07:高阶实验工具 开源 | MeiGen-MultiTalk:基于单张照片实现多人互动演绎 可信实验白皮书系列06:观察性研究 JDK高版本特性总结与ZGC实践 可信实验白皮书系列05:准实验 可信实验白皮书系列04:随机轮转实验 可信实验白皮书系列03:随机对照实验 可信实验白皮书系列02:AB实验基础 可信实验白皮书系列01:从0到1的方法论与实践指南 MTGR:美团外卖生成式推荐Scaling Law落地实践 OR算法+ML模型混合推理框架架构演进 ICLR&CVPR 2025美团技术团队论文精选 行为正则化与顺序策略优化结合的离线多智能体学习算法 预测技术在美团弹性伸缩场景的探索与应用 美团技术年货 | 600+页电子书,算法、工程、测试、数据、安全系列大合集 鸿蒙应用签名实操及机制探究 2024 | 美团技术团队热门技术文章汇总 AutoConsis:UI内容一致性智能检测 CIKM 2024 | 美团技术团队精选论文解读 大前端:如何突破动态化容器的天花板? KDD 2024 OAG-Challenge Cup赛道三项冠军技术方案解读 新一代实验分析引擎:驱动履约平台的数据决策 大众点评技术部包揽KDD 2024 OAG-Challenge Cup赛道全部3项冠军 ACL 2024 | 美团技术团队精选论文解读 KDD 2024 | 美团技术团队精选论文解读 基本功 | 一文讲清多线程和多线程同步 小程序可测性能力建设与实践 SIGIR 2024 | 美团技术团队精选论文解读 Spark向量化计算在美团生产环境的实践 CVPR 2024 | 美团技术团队精选论文解读 基于多模态信息抽取的菜品知识图谱构建 DDD在大众点评交易系统演进中的应用 美团外卖基于GPU的向量检索系统实践 美团大规模KV存储挑战与架构实践 基于接口数据变异的App健壮性测试实践 美团技术年货 | 600+页电子书,前端、后端、算法、测试、运维系列大合集 美团RASP大规模研发部署实践总结 2023 | 美团技术团队热门技术文章汇总 美团到店终端从标准化到数字化的演进之路 AIOps在美团的探索与实践——事件管理篇 美团技术博客十周年,感谢一路相伴 基于UI交互意图理解的异常检测方法 如何利用「深度上下文兴趣网络」提升点击率? 基于模式挖掘的可靠性治理探索 代码变更风险可视化系统建设与实践 美团多场景建模的探索与实践 MJDK 如何实现压缩速率的 5 倍提升? 如何提供一个可信的AB测试解决方案 KDD 2023 | 美团技术团队精选论文解读 美团前端研发框架Rome实践和演进趋势 斩获CVPR 2023竞赛2项冠军|美团街景理解中视觉分割技术的探索与应用 MySQL自治平台建设的内核原理及实践(下) CVPR 2023 | 美团技术团队精选论文解读 超大规模数据库集群保稳系列之三:美团数据库容灾体系建设实践 超大规模数据库集群保稳系列之二:数据库攻防演练建设实践 Robust 2.0:支持Android R8的升级版热修复框架 超大规模数据库集群保稳系列之一:高可用系统 一次「找回」TraceId的问题分析与过程思考 基于AI+数据驱动的慢查询索引推荐 SOTA!目标检测开源框架YOLOv6 3.0版本来啦
美团App 插件化实践
李挺 夏伟 · 2017-10-12 · via 美团技术团队

背景

在Android开发行业里,插件化已经不是一门新鲜的技术了,在稍大的平台型App上早已是标配。进入2017年,Atlas、Replugin、VirtualAPK相继开源,标志着插件化技术进入了成熟阶段。但纵观各大插件框架,都是基于自身App的业务来开发的,目标或多或少都有区别,所以很难有一个插件框架能一统江湖解决所有问题。最后就是绕不开的兼容性问题,Android每次版本升级都会给各个插件化框架带来不少冲击,都要费劲心思适配一番,更别提国内各个厂商对在ROM上做的定制了,正如VirtualAPK的作者任玉刚所说:完成一个插件化框架的 Demo 并不是多难的事儿,然而要开发一款完善的插件化框架却并非易事。

早在2014年美团移动技术团队就开始关注插件化技术了,并且意识到插件化架构是美团这种平台型App最好的集成形式。但由于业务增长、迭代、演化太快,受限于业务耦合和架构问题,插件化一直无法落地。到了2016年底,经过一系列的代码架构调整、技术调研,我们终于能腾出手来让插件化技术落地了。

美团平台(与点评平台一起)目前承载了美团所有事业群近20条业务线的业务。其中有相对成熟的业务,比如外卖、餐饮,他们对插件的要求是稳定性高,不能因为上了插件导致业务出问题;也有迭代变化很快的业务,如交通、跑腿、金融等,他们要求能快速迭代上线;此外,由于美团App采用的二进制AAR依赖方式集成已经运转了两年,各种基础设施都很成熟了,我们不希望换成插件形式的接入之后还要改变开发模式。所以,美团平台对插件的诉求主要集中在兼容性和不影响开发模式这两个点上。

美团插件化框架的原理和特点

插件框架的兼容性体现在多个方面,由于Android机制的问题,有些写法在插件化之前运行的很正常,但是接入插件化之后就变得不再有效。如果不解决兼容性问题,插件化的口碑和推广都会很大阻碍。兼容性不仅仅指的是对Android系统、Android碎片化的兼容,还要对已有基础库和构建工具的兼容。特别是后者,我们经常看到Github上开源的插件化框架里面有大量Crash的Issue,就是这个方面原因导致的。每个App的基础库和既有构建工具都不太一样,所以为自己的App选择合适的方案显得尤为重要。

为了保证插件的兼容性,并能无缝兼容当前AAR开发模式,美团的插件化框架方案主要做了以下几点::

  • 插件的Dex加载使用类似MultiDex方案,保证对反射的兼容
  • 替换所有的AssetManager,保证对资源访问的兼容
  • 四大组件预埋,代理新增Activity
  • 让构建系统来抹平AAR开发模式和插件化开发模式的差异

MultiDex和组件代理这里不细说,网上有很多这方面的博客可以参考。下面重点说一下美团插件化框架对资源的处理和支持AAR、插件一键切换的构建系统。

资源处理

了解插件化的读者都知道:如果希望访问插件的资源,需要使用AssetManager把插件的路径加入进去。但这样做是远远不够的。这是因为如果希望这个AssetManager生效,就得把它放到具体的Resources或ResourcesImpl里面,大部分插件化框架的做法是封装一个包含插件路径AssetManager的Resources,然后插件中只使用这一个Resources。

这样的做法大多数情况是有效的,但是有至少3个问题: 1. 如果在插件中使用了宿主Resources,如:getApplicationContext().getResources()。 这个Resources就无法访问插件的资源了 2. 插件外的Resources 并不唯一,需要全局查找和替换 3. Resoureces在使用的过程中有很多中间产物,例如Theme、TypedArray等等。这些都需要清理才能正常使用

要完全解决这些问题,我们另辟蹊径,做了一个全局的资源处理方式: * 新建或者使用已有AssetManger,加载插件资源 * 查找所有的Resources/Theme,替换其中的AssetManger * 清理Resources缓存,重建Theme * AssetManager的重建保护,防止丢失插件路径

这个方案和InstantRun有点类似,但是原生InstantRun有太多的问题: * 清理顺序错误,应该先清理Applicaiton后清理Activity * Resources/Theme找不全,没有极端情况应对机制 * Theme光清理不重建 * 完全不适配 Support包里面自己埋的“雷” 等等

举个例子Theme找不全:InstantRun会替换Theme中的AssetManager,做法是从每个Activity里面获取。

for (Activity activity : activities) {
    ... // 省略部分代码
    Resources.Theme theme = activity.getTheme();
    try {
        try {
            Field ma = Resources.Theme.class.getDeclaredField("mAssets");
            ma.setAccessible(true);
            ma.set(theme, newAssetManager);
        } catch (NoSuchFieldException ignore) {
            Field themeField = Resources.Theme.class.getDeclaredField("mThemeImpl");
            themeField.setAccessible(true);
            Object impl = themeField.get(theme);
            Field ma = impl.getClass().getDeclaredField("mAssets");
            ma.setAccessible(true);
            ma.set(impl, newAssetManager);
        }
        ...
    } catch (Throwable e) {
        Log.e(LOG_TAG, "Failed to update existing theme for activity " + activity,
                e);
    }
    pruneResourceCaches(resources);
}

这个思路是对的,但是远不够。例如,Google 自己的Support包里面的一个类 android.support.v7.view.ContextThemeWrapper会生成一个新的Theme保存:

public class ContextThemeWrapper extends ContextWrapper {
    private int mThemeResource;
    private Resources.Theme mTheme;
    private LayoutInflater mInflater;
    ...
    private void initializeTheme() {
        final boolean first = mTheme == null;
        if (first) {
            mTheme = getResources().newTheme();
            final Resources.Theme theme = getBaseContext().getTheme();
            if (theme != null) {
                mTheme.setTo(theme);
            }
        }
        onApplyThemeResource(mTheme, mThemeResource, first);
    }
    ...
}

如果没有替换了这个ContextThemeWrapper的Theme,假如配合它使用的Reources/AssetManager是新的,就会导致Crash: java.lang.RuntimeException: Failed to resolve attribute at index 0 这是大部分开源框架都存在的Issue。 为了解决这个问题,我们不仅清理所有Activity的Theme,还清理了所有View的Context。

try {
    List<View> list = getAllChildViews(activity.getWindow().getDecorView());
    for (View v : list) {
        Context context = v.getContext();
        if (context instanceof ContextThemeWrapper
                && context != activity
                && !clearContextWrapperCaches.contains(context)) {
            clearContextWrapperCaches.add((ContextThemeWrapper) context);
            pruneSupportContextThemeWrapper((ContextThemeWrapper) context, newAssetManager); // 清理Theme
        }
    }
} catch (Throwable ignore) {
    Log.e(LOG_TAG, ignore.getMessage());
}

但是这些做法还是不能解决所有问题,有时候为了实现一个产品需求,Android工程师可能会采取一些非常规写法,导致变成插件之后资源加载失败。比如在一个自己的类里面保存了Theme。这种问题不可能一个个改业务代码,那能不能让插件兼容这种写法呢? 我们对这种行为也做了兼容:修改字节码

了解虚拟机指令的同学都知道,如果要保存一个类变量,对应的虚拟机的指令是PUTFIELD/PUTSTATIC,以此为突破口,用ASM写一个MethodVisitor:

static class MyMethodVisitor extends MethodVisitor {
    int stackSize = 0;

    MyMethodVisitor(MethodVisitor mv) {
        super(Opcodes.ASM5, mv);
    }

    @Override
    public void visitFieldInsn(int opcode, String owner, String name, String desc) {
        if (opcode == Opcodes.PUTFIELD || opcode == Opcodes.PUTSTATIC) {
            if ("Landroid/content/res/Resources$Theme;".equals(desc)) {
                stackSize = 1;
                visitInsn(Opcodes.DUP);
                super.visitMethodInsn(Opcodes.INVOKESTATIC,
                        "com/meituan/hydra/runtime/Transformer",
                        "collectTheme",
                        "(Landroid/content/res/Resources$Theme;)V",
                        false);
            }
        }
        super.visitFieldInsn(opcode, owner, name, desc);
    }

    @Override
    public void visitMaxs(int maxStack, int maxLocals) {
        super.visitMaxs(maxStack + stackSize, maxLocals);
        stackSize = 0;
    }
}

这样可以保证所有被类保存的Theme都会被收集起来,在插件安装后,统一清理、重建就行了。

插件的构建系统

为了实现在AAR集成方式和插件集成方式之间一键切换,并解决插件化遇到的“API陷阱”的问题,我们把大量的时间花在构建系统的建设上面,我们的构建系统除了支持常规的构建插件之外,还支持已有构建工具和未来可能存在的构建工具。 我们将正常构建过程分为4个阶段: 1. 收集依赖 2. 处理资源 3. 处理代码 4. 打包签名

那么如何保证对已有Gradle插件的支持?最好的方式是不对这个构建过程做太多干涉,保证它们的正常、按顺序执行。所以我们的构建系统在不干扰这个顺序的基础上,把插件的构建过程插入进去,对应正常构建的4个阶段,主要做了如下工作。

  • 宿主解析依赖之后,分析插件的依赖,进行依赖仲裁和引用计数分析
  • 宿主处理资源之前,处理插件资源,规避了资源访问的陷阱,生成需要Merge的资源列表给宿主,开发 美团AAPT 处理插件资源
  • 宿主处理代码之中,规避插件API使用的陷阱,复用宿主的Proguard和Gradle插件,做到对原生构建过程的最大兼容。我们也修复了Proguard Mapping的问题,后续会有专门的博客介绍
  • 宿主打包签名之前,构建插件APK,计算升级兼容的Hash特征,使用V2签名加快运行时的验证

构建系统的流程如下图

API陷阱

我们做插件化构建系统还有另外一个非常重要的目的,就是规避“API陷阱”。下面是接入Atlas所需要注意的部分问题,我们称为“API陷阱” 1. Activity通过overridePendingTransition使用的切换动画的文件要放在主APK中; 2. Bundle内如果有用到自定义style,那么style的parent如果也是自定义的话,parent的定义必须位于主APK中,这是由于5.0以后系统内style查找的固有逻辑导致的,容器内暂不能完全兼容 3. Bundle内部如果有so,则安装时so由于无法解压到APK lib目录中,对于直接通过native层使用dlopen来使用so的情况,会存在一定限制,且会影响后续so动态部署,所以目前bundle内so不建议使用dlopen的方式来使用

那我们是怎么做的呢? 我们用构建工具自动对插件资源进行处理。先把插件独有的依赖从宿主处理的依赖里面抽离,然后为宿主单独准备一份资源目录,这个目录只包括需要merge的资源。 那么怎么抽离呢?我们看下处理资源的task是如何获得这些资源的。代码在com.android.build.gradle.tasks.MergeResources$ConfigAction

ConventionMappingHelper.map(mergeResourcesTask, "inputResourceSets",
        new Callable<List<ResourceSet>>() {
            @Override
            public List<ResourceSet> call() throws Exception {
                List<File> generatedResFolders = Lists.newArrayList(
                        scope.getRenderscriptResOutputDir(),
                        scope.getGeneratedResOutputDir());
                if (variantData.getExtraGeneratedResFolders() != null) {
                    generatedResFolders.addAll(
                            variantData.getExtraGeneratedResFolders());
                }
                if (scope.getMicroApkTask() != null &&
                        variantData.getVariantConfiguration().getBuildType()
                                .isEmbedMicroApp()) {
                    generatedResFolders.add(scope.getMicroApkResDirectory());
                }

                return variantData.getVariantConfiguration().getResourceSets(
                        generatedResFolders, includeDependencies, validateEnabled);
            }
        });

了解Groovy的同学都知道,设置这个inputResourceSets,其实就是重写了这个mergeResourcesTask的getInputResourceSets方法。那么我们也这可以这么做:

ConventionMapping conventionMapping =
                (ConventionMapping) ((GroovyObject) variantData.mergeResourcesTask).getProperty("conventionMapping");
def srcMethod = conventionMapping._mappings.get("inputResourceSets");

conventionMapping.map("inputResourceSets", new Callable<List<ResourceSet>>() {
    @Override
    public List<ResourceSet> call() throws Exception {
        List<ResourceSet> res = srcMethod.getValue(null, null)
        ... // 处理这个res
        return res
    }
})

对于第一个问题:前面提到的插件为宿主提供的资源文件夹,如果是一个空的没有任何意义。我们会分析插件的AndroidManifest.xml文件,以此作为root,遍历被它引用的所有的资源,不管是文件,还是values文件夹下面的单个value,全部merge进这个文件夹。 但是只是AndroidManifest.xml文件是不够的,所有传给系统的文件,比如提到的“Activity通过overridePendingTransition使用的切换动画的文件”,也一并放进这个文件夹。这里需要使用ASM扫描插件的所有API调用,类似上面的Theme查找,不细展开了。

第二个问题:把插件values里面style的parent也作为检索的root,遍历merge。

第三个问题:API陷阱除了资源,还有大量的代码级别的,上面的插件so加载问题就是很典型的一个例子,正常使用System.loadLibrary(path)是不行的,但是可以把它转化成下面的写法:我们发现,如果插件dlopen来加载的so之前被加载过,就不会出现这个问题。

private static Pattern compile = Pattern.compile("dlopen failed: library \"lib(.+).so\" not found");
public static void system_loadLibrary(String libname) {
    LinkedList<String> list = new LinkedList<>();
    list.add(libname);
    while (list.size() > 0) {
        try {
            System.loadLibrary(list.peekFirst());
            list.pop();
        } catch (UnsatisfiedLinkError error) {
            // dlopen failed: library "libglog_init.so" not found
            Matcher matcher = compile.matcher(error.getMessage());
            if (matcher.matches()) {
                String group = matcher.group(1);
                list.addFirst(group);
            } else {
                throw error;
            }
        }
    }
}

当然需要替换的API很多,如 getIdentifier、Notification、Glide等等,不一一列举。

总结

本文主要介绍美团插件化的设计思路和一些实现。经过我们这些努力,美团平台的业务集成模式可以平滑的在AAR集成模式和插件化集成模式之间无缝切换,且上线几乎没出现兼容问题。目前在美团App最近的几个版本上,搜索、收藏、订单等重要模块都是插件形式加载的。

作者简介

李挺,美团技术专家,2014年加入美团。先后负责过多个业务项目和技术项目,致力于推动AOP和字节码技术在美团的应用。曾独立负责美团App预装项目并推动预装实现自动化。主导了美团插件化框架的设计和开发工作,目前工作重心是美团插件化框架的布道和推广。

夏伟,美团资深工程师,2017年加入美团。目前从事美团插件化开发,美团平台的一些底层工具优化,如AAPT、ProGuard等,专注于Hook技术、逆向研究,习惯从源码中寻找解决方案。

美团平台客户端技术团队,负责美团平台的基础业务和移动基础设施的开发工作。基于海量用户的美团平台,支撑了美团多条业务线的快速发展。同时,我们也在移动开发技术方面做了一些积极的探索,在动态化、质量保障、开发模型等方面有一定积累。客户端技术团队积极采用开源技术的同时,也把我们的一些积累回馈给开源社区,希望跟业界一起推动移动开发效率、质量的提升。