插件化是一种动态升级app功能的解决方案,不同于热修复(仅仅是修复功能),类似于RN、Weex(目的类似)。都是为了在不发版本的情况下,可以让用户用上最新的功能。不过RN、Weex还额外支持跨平台。相对于RN和Weex,插件化有以下的优缺点:
优点:
缺点:
事实上,有一部分的项目采用动态化、不仅仅是为了其动态性,更是为了对模块化的升级,对于大型复杂项目而言,单纯的“模块化”已经不能很好的隔离分解项目的复杂性,而“插件化”可以帮助项目,将独立且足够内聚的业务独立出来成为一个插件,然后交给其他团队来维护。
康威定律:软件源代码的组织结构要与开发团队的组织结构尽量保持一致
说到插件化,第一个谈到的是类加载器,这个是插件化的核心,所有的一切都是围绕着这个转的。与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。对于具体实现可以参考这篇博客https://blog.csdn.net/jiangwei0910410003/article/details/50820219
2. 修改aapt的产物,即,编译后期重新整理插件Apk的资源,编排ID。
VirtualApk采用的就是这个方案。对于具体实现可以参考这篇博客http://www.javashuo.com/article/p-yommjibe-kp.html
3. 使用aapt2的新能力
google为了app生态的稳定性和安全性,在Android P(9.0)对系统私有API的访问进行了限制。不幸的是,插件化使用了大量的系统私有API,我们不得不想办法来绕过这些限制。
目前来看,最简单的方式就是使用“元反射”
http://weishu.me/2019/03/16/another-free-reflection-above-android-p/
插件可能会使用到宿主已经存在的库, 为了降低插件的体积,我么需要想办法在打包阶段将重复代码提出调。Gradle有提供compileOnly来实现仅仅在编译阶段依赖三方库,但是如果你依赖了三方库的资源,那么这种方式会导致编译失败。
自定义编译脚本,在编译时仅仅删除代码,不删除资源来实现该功能。可以参考:
https://blog.csdn.net/xuexiangjys/article/details/84147652
实现插件化的方式很多,但不管怎么样,都绕不开一些黑科技,需要Hook住系统的关键节点。但是Android P以后google为了安全性和稳定性的考虑,已经开始收紧了对系统api的反射调用,虽然目前有办法绕过,但是绝对不是长久之技。除此之外有那些替代方案呢?
我们回忆下我们做插件化的目的:
探索道路
● Follow官方的方案
这种方案只解决了包大小的问题,开发者将app分为三部分(基本apk、资源apk、非必须apk),且依赖googlePlay。
● 组件化方案
● 大前端:RN, Weex, 快应用等