背景

在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);}@Overridepublic 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);}@Overridepublic 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>>() {@Overridepublic 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>>() {@Overridepublic List<ResourceSet> call() throws Exception {List<ResourceSet> res = srcMethod.getValue(null, null)... // 处理这个resreturn 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 foundMatcher 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技术、逆向研究,习惯从源码中寻找解决方案。

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

美团App 插件化实践相关推荐

  1. 美团App插件化实践

    点击上方"公众号"可以订阅哦 背景 在Android开发行业里,插件化已经不是一门新鲜的技术了,在稍大的平台型App上早已是标配.进入2017年,Atlas.Replugin.Vi ...

  2. 掌阅Android App插件补丁实践(ZeusPlugin)

    掌阅Android App插件补丁实践(ZeusPlugin) 遇到问题 65K方法数超限 随着应用不断迭代,业务线的扩展,应用越来越大,那么很不幸,总有一天,当你编译的时候,会遇到一个类似下面的错误 ...

  3. 携程Android App插件化和动态加载实践

    转载自:http://www.infoq.com/cn/articles/ctrip-android-dynamic-loading?email=947091870@qq.com 编者按:本文为携程无 ...

  4. 酷狗 Android App 插件化实施过程

    什么是插件化框架     插件化框架可以在主程序不重新安装的情况下,针对单个业务模块进行加载达到模块更新的目的,整个加载更新过程,对用户来说也是无感知的.     正式因为这样,新需求比起传统更新方式 ...

  5. 微店 Android 插件化实践

    随着微店业务的发展,App 不可避免地也遇到了 65535 的大坑.除此之外,业务模块增多.代码量增大所带来的问题也逐渐显现出来.模块耦合度高.协作开发困难.编译时间过长等问题严重影响了开发进程.在预 ...

  6. android加固 app插件化,自定义Gradle插件给应用加固

    场景 当我们发布app时候都会选择一个加固的形式给我们的apk加壳,本文章就360加固为例,如何用gradle的方式在我们apk生成后自动加固. 自定义插件 自定义插件其实说白了就是在gradle里面 ...

  7. android加固 app插件化,[求助]app被加固了,该怎么用xposed模块hook?

    楼上说的不错,不过,有时候有些App只是ClassLoader被壳换了,这个时候,选对ClassLoader就可以了.比如这样: public EncryptHook(ClassLoader cl) ...

  8. Android APP热更新中的插件化(Hook技术:反射或动态代理),Demo (2)

    修改AAPT,资源分区,用于Android插件化- https://github.com/BaoBaoJianqiang/AAPT -- Android下的挂钩(hook)和代码注入(inject) ...

  9. Android 插件化总结

    2019独角兽企业重金招聘Python工程师标准>>> 1.Android中插件开发篇总结和概述 2.Android组件化和插件化开发 3.携程Android App插件化和动态加载 ...

最新文章

  1. 【2021斯坦福新书】统计学思维,300页pdf
  2. 【传智播客】Javaweb程序设计任务教程 黑马程序员 第四章 课后答案
  3. 【dfs】益智游戏(2017 特长生 T2)
  4. C#完整的通信代码(点对点,点对多,同步,异步,UDP,TCP),多多宜善
  5. 第二届太原理工大学程序设计新生赛预赛(公开赛)题解
  6. centos7黑客帝国装逼
  7. C#3.0 语言基础扩充
  8. http://www.myeclipseide.com/ 官网打不开的问题!myeclipse 官网!
  9. 自由软件之“父”—Richard. M. Stallman
  10. pyton构建一个计算列表中位数的函数
  11. 因为计算机限制无法访问U盘,U盘拒绝访问怎么办解决教程
  12. 修复音频服务器,以上就是Win7系统如何修复音频服务未运行的具体方法
  13. vue 实现元素可拖曳
  14. 如何在Visual Studio中安装.net6.0 或者 将在Visual Studio中将.net 5.0更新为6.0
  15. 【C#:WinForm+ADO.NET+SQL Server实现验证码登录】
  16. 用浅浅的幸福交换落寞的伤
  17. PHP 门面设计模式
  18. 关于拨号上网的几种错误解决办法
  19. MySQL在线DDL gh-ost 使用说明
  20. 人人商城v3.28.41修复11月8日微信登录接口,公众号和小程序同步修复

热门文章

  1. CNS服务器搭建(配合百度直连)
  2. 微创电生理通过注册:年营收1.9亿 微创批量生产上市企业
  3. Java递归删除空文件夹
  4. <JVM上篇:内存与垃圾回收篇>01-JVM与Java体系结构
  5. Facebook、Twitter、LinkedIn分享按钮总结
  6. 美式口语发音技巧:《英美发音区别》
  7. mybatis-generator同名表的处理
  8. IDEA 出现错误:找不到或无法加载主类
  9. vue 子组件与父组件运行的顺序
  10. 【测试专场沙龙报名】千万级日活App的质量保证