为什么使用插件机制
Android插件化可以带来以下好处:
- 可以解决65536问题:但是在5.x以后加上mutildex这个需求变得不哪么强了
- 插件可以动态升级:对动态升级有需求的APP来说,这个吸引力很大
- 可以减小APK包大小:前提是插件不内置,通过异步进行下载
插件框架的分类
Android插件框架要解决的三个基本问题:
- 如何在插件Activity中启动另一个Activity?
- 如何加载插件APK中的类?
- 如何加载插件APK中的资源?
第2、3问题的答案比较明确,都是通过创建自定义的DexClassLoader、AssetManager(Resource)来确现的。但是第1个问题的实现方案就比较多了。
Android系统启动Activity时只能启动在manifest.xml中注册过的Activity,这是在AMS中进行校验的,无法改变。所以只能用占坑的方式,在manifest.xml中提前注册一些ActivityProxy,然后在启动插件的时候欺骗系统是要启动这些ActivityProxy。在此就根据“欺骗”方式的不同进行分类。
继承方式
怎么样“欺骗”系统是在启动ActivityProxy呢?
继承Activity
首先想到的是让插件开发时继承系统Activity,然后重写里面的startActivityForResult()方法,修改里面的Intent指向ActivityProxy。但是很遗憾,Activity.startActivityForResult()是final方法,无法复写。
所以需要在继承的Activity中添加一个对象(that),来实现这些final的方法,在开发的时候用that.startActivity()。如下面的类结果图:
优点:
- 插件框架开发相对简单,插件中也可以使用Activity
缺点:
由于Activity中的很多final方法,是无法重写的,所以开发会有很多限制(startActivity finish等)
调activity中的方法时要用that,比如that.finish(),that.setResult(),new View(that)等等,this和that用法容易混淆
以上限制使得改造一个插件的成本偏高,而且容易出错
继承ContextWrapper
既然Activity中有很多final方法无法重写,可以直接继承ContextWrapper,参考Activity来实现里面的方法。因为Activity也是继承的ContextWrapper类,所以这个方法是可行的,相当于我们自定义了一个Activity。这样就可以直接重写startActivityForResult()方法,修改里面的目标Intent,完成“欺骗”系统的目的。
优点:
- 开发插件时符合平时开发习惯
缺点:
- 插件框架开发成本高
- 开发过程中插件不能直接运行(因为它继承的不是真正的Activity,但是可以通过优化开发流程避免这个问题,开发时继承Activity,打正式包时集成ContextWrapper)
Hook方式
通过研究Activity的启动流程,发现用动态代理或反射可以拦截启动流程,替换其中的Intent,统称之为Hook方式。
根据拦截的地方不同,又分为Hook Instrumentation和Hook ActivityManagerNative。
Hook Instrumentation
在Activity中有个成员变量Instrumentation,Activity的启动和生命周期都会调用它。
在ActivityTread中也有个Instrumentation,当Activity对象真实创建的时候会调用这个它来创建。
所以在启动插件的时候就可以通过替换Activity、ActivityTread中的Instrumentation为自定义的对象,就可以拦截修改Intent,并且在真实创建Activity时再指向插件中的Activity。这种方式也是[small]框架的思想
优点:
- Hook的点比较少,兼容性好
缺点:
- Instrumentation只支持Activity,所以这种方式不能支持Serivce、ContentProvider等
Hook ActivityManagerNative
ActivityManagerNative是和AMS交互的client端,它是运行在应用进程的,所以可以通过动态代理拦截里面的所有的方法。其中不只有启动Activity的方法,还包括Service等。[DroidPlugin]框架用的这种方式,只不过它Hook的更多。
优点:
- 功能强大,支持Android四大组件
缺点:
- Hook的地方比较多,可能会出现兼容问题
真实运行的Activity
以上介绍的都是通过一定的手段“欺骗”系统(AMS),要启动占坑的ActivityProxy。但是AMS真的相信了你要启动ActivityProxy,所以验证通过后它会通知ActivityTread.mH要启动ActivityProxy。在这里也有两种处理方式:
代理Activity
不做处理,就真实启动ActivityProxy,然后再在ActivityProxy生命周期中加载插件的Activity类,然后通过反射同步调用插件Activity的生命周期。
优点:
- 不需要反射等其它操作
- 而且可以在ActivityProxy生命周期中做一些事情,比如:清理缓存、处理插件切换等
缺点:
- 需要反射调用Activity生命周期
Hook mH(Instrumentation)
因为Activity真实创建的过程都是通过ActivityTread.mH调用Instrumentation来创建的,所以可以通过Hook这两个对象让它返回插件中的Activity。这样运行起来的就是插件Activity,而不需要ActivityProxy。
优点:
- 不需要反射调用Activity生命周期
缺点:
- 需要Hook mH或Instrumentation,增加框架的不稳定性
- 不能在插件Activity生命周期中做一些事情
方案选择
我们选择的方案是Hook ActivityManagerNative + 代码Activity,这是在功能和稳定性之间做的平衡
进程管理
插件动行在哪个进程,如何管理?根据插件进程数量可以分为以下几种性况:
- 每个插件一个进程:如果插件太多,会造成进程数膨胀,只适合少量插件的情况。
- 固定进程数,插件共用一定数量的进程:两个插件不能同时启动,适合独立插件,插件退出进程会被杀。
- 所有插件共用一个进程:会出现View类冲突和单独问题
View类冲突
当多个插件在一个进程中时,如果插件中有两个相同的View(包名和类名都相同),哪么在加载第二个插件的时候可能会出现类型转换错误。这是因为LayoutInflater中会缓存View对象,当加载第二个插件中的相同View时会把缓存返回,这样就造成了类型转换错误。
解决方案:判断插件切换时,清除LayoutInflater中的缓存
同理Fragment也会出现这种情况。
单例问题
当多个插件共用主APP中的单例时,这个单例在插件进程中只会初始化一次,如果这个单例中需要Context时。这个Context是属于哪个插件呢?
这种情况在用Context加载本地资源时,就会出现混乱。