概述
插件化是一种动态升级app功能的解决方案,不同于热修复(仅仅是修复功能),类似于RN、Weex(目的类似)。都是为了在不发版本的情况下,可以让用户用上最新的功能。不过RN、Weex还额外支持跨平台。相对于RN和Weex,插件化有以下的优缺点:
优点:
- 对于业务方,无额外的学习成本,基本无感知
- 性能等同于原生、可以做任何原生可以做的事情
- 天然代码隔离、使得插件化的代码更加的“高内聚、低耦合”
- 插件并发开发,开发之间互不影响
缺点:
- 稳定性差,使用了大量大反射来实现,尤其是Android P以后Google对系统API的调用做出了限制,虽然有办法跳过(后面会说),但是却无疑增加了使用风险。
- 安全性低,恶意插件将会有比较大的权限,来做破坏软件的事情。(一般会添加签名校验)
- 插件化目前没有一套通用的规范,基本上都是各用各的,导致插件无法通用
- 只适用于android,无法和iOS保持方案统一
详情
类加载
说到插件化,第一个谈到的是类加载器,这个是插件化的核心,所有的一切都是围绕着这个转的。与JAVA类加载的过程类似、android的类加载也是双亲委派、如下图:
不同的是android使用的类加载器是PathClassLoader(加载已经安装过的APK)和DexClassLoader(未安装的apk、dex、jar)。对于一般情况,可以使用类加载器直接加载下载下来的apk文件,不需要做任何处理,android平台上有些双开程序也是利用这个原理,因为apk是完整的。但是考虑到插件体积的问题,我们会将宿主中已经存在的代码从插件中剔除,来保证插件的最小化。为了保证不冲突,我们会想办法优先加载插件里面的class,由以下两种方式可以实现:
通过在dexPathList的前面插入插件的DexFile来实现优先加载的逻辑
另外这种通过更改类加载模型,从双亲委派更改为,优先子加载器加载,加载失败才从父加载器加载。
资源加载
资源加载是插件化的另外一个问题,幸运的是android也提供的API来加载额外的资源文件、不过可惜的是方法并没有暴露出来,只能通过反射进行调用。
遇到的问题
四大组件启动
在android中,四大组件想要启动必须事先定义到AndroidManifest文件中,而插件由于是动态下发的,无法事先确定内部使用到的四大组件,导致启动会出现异常。目前的解决方案有两种:“占坑法”、“欺上瞒下”
占坑法:通过实现在AndroidManifest中定义一系列的四大组件、通过代理的方式来将实现委托给插件来完成。
代理模式:使用代理类来接收用户的请求,而真正的处理交给被代理类来进行处理,它们的关系基本上在编译阶段就已经确定下来了(这点和装饰器模式不同,装饰器在运行时指定),代理主要关注的是请求的访问控制,比如插件化中Activity的代理会将实际的跳转意图,控制到占位Activity中来实现四大组件的启动。
代理分类:静态代理、动态代理(目前android只支持接口)、字节码代理(静态代理的自动化版本,在编译期间自动生成代理对象)。
以下是个人的一些理解,不一定正确,我单独提出来:
欺上瞒下:思路大概是,先在宿主中定义一个替身Activity,然后也是在启动Activity过程中通过反射Hook两处地方代码,第一处是在准备跨进程调用ActivityManagerService前,即Instrumentation.java的execStartActivity方法中通过ActivityManagerNative.getDefault()它返回一个IActivityManager对象,我们对它创建一个代理,在IActivityManager对象去调用startActivity前把目标Activity替换成替身Activity,以达到欺骗目的。然后第二处是在ActivityManagerSerfvice跨进程回来后,在ActivityThread.java中接收LAUNCH_ACTIVITY消息前,可以对Handle的callback进行代码,让其消息接收前将目标Activity换回来。这样做就能达到只需要在宿主中只声明一个替身Activity就能满足于插件中Actvitiy
资源命名冲突
插件的资源在运行时会进行加载,并合入到宿主插件资源中,因为插件和宿主的编译过程是独立中,无法保证资源ID不进行冲突。而一旦资源冲突,将会导致冲突的资源被覆盖,将会导致显示内容混乱,影响插件正常展示。
解决思路:
1. 修改aapt源码,定制aapt工具,编译期间修改PP段。(PP字段是资源id的第一个字节,表示包空间)
DynamicAPK的做法就是如此,定制aapt,替换google的原始aapt,在编译的时候可以传入参数修改PP段:例如传入0x05编译得到的资源的PP段就是0x05。对于具体实现可以参考这篇博客blog.csdn.net/jiangwei091…
2. 修改aapt的产物,即,编译后期重新整理插件Apk的资源,编排ID。
VirtualApk采用的就是这个方案。对于具体实现可以参考这篇博客blog.csdn.net/weixin_4388…
3. 使用aapt2的新能力
Android P以后反射限制
google为了app生态的稳定性和安全性,在Android P(9.0)对系统私有API的访问进行了限制。不幸的是,插件化使用了大量的系统私有API,我们不得不想办法来绕过这些限制。
目前来看,最简单的方式就是使用“元反射”
插件和宿主三方库冲突
插件可能会使用到宿主已经存在的库, 为了降低插件的体积,我么需要想办法在打包阶段将重复代码提出调。Gradle有提供compileOnly来实现仅仅在编译阶段依赖三方库,但是如果你依赖了三方库的资源,那么这种方式会导致编译失败。
自定义编译脚本,在编译时仅仅删除代码,不删除资源来实现该功能。可以参考:
最后
实现插件化的方式很多,但不管怎么样,都绕不开一些黑科技,需要Hook住系统的关键节点。但是Android P以后google为了安全性和稳定性的考虑,已经开始收紧了对系统api的反射调用,虽然目前有办法绕过,但是绝对不是长久之技。除此之外有那些替代方案呢?
我们回忆下我们做插件化的目的:
- 边界隔离
- 并行开发
- 动态部署
- 快速编译
- 模块解耦
- 降低大小
探索道路
● Follow官方的方案
这种方案只解决了包大小的问题,开发者将app分为三部分(基本apk、资源apk、非必须apk),且依赖googlePlay。
● 组件化方案
● 大前端:RN, Weex, 快应用等