android插件化

 

概述

       插件化是一种动态升级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。对于具体实现可以参考这篇博客https://blog.csdn.net/jiangwei0910410003/article/details/50820219

 

2. 修改aapt的产物,即,编译后期重新整理插件Apk的资源,编排ID。

VirtualApk采用的就是这个方案。对于具体实现可以参考这篇博客http://www.javashuo.com/article/p-yommjibe-kp.html

 

3. 使用aapt2的新能力

 

 

Android P以后反射限制

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, 快应用等