插件化技术整理
前言
读<<Android插件化开发指南>>总结。
这篇文章不分析系统源码,只整理对加载插件apk中的一些问题的解决方案。插件化就是加载未再Android系统中注册的apk
- 加载插件中的类 有两种方案
-
合并dex
加载类的ClassLoader中有一个DexPathList对象,里面有一个dexElements数组,专门用来存放apk中dex集合的,通过DexFile加载插件中的dex文件,将它与宿主中的dex合并形成一个新的数组,通过反射修改系统中dexElements的值,这样就可以直接加载在插件apk中的类了。该方案也可用于热修复。
-
为插件apk构建一个单独classLoader
val mBaseClassLoader = context.classLoader val dexOutputPath = context.getDir("dex", Context.MODE_PRIVATE).absolutePath // 第一个参数 apk文件路径 第二个:dex文件存放路径 第三个 lib(so)文件存放路径 val dexLoader = DexClassLoader(path,dexOutputPath,null,mBaseClassLoader)
自定义实现一个classloader 用来代理 所有插件和宿主的classloader,再用这个classloader替换系统中的。
val packageInfo = RefInvoke.getFieldObject(context, "mPackageInfo")
RefInvoke.setFieldObject(packageInfo,"mClassLoader",pluginClassLoader)
Thread.currentThread().contextClassLoader = pluginClassLoader
此方案,用Class.forName加载不到插件中的类
2. 一个apk除了代码,还有的就是资源文件了,指放在res和assets目录下的文件,构建能够加载插件apk中资源的Resources和AssetManager
assets = RefInvoke.createObject(AssetManager::class.java) as AssetManager
RefInvoke.invokeInstanceMethod(assets,"addAssetPath",String::class.java,path)
mResources = Resources(assets,context.resources.displayMetrics,
context.resources.configuration)
通过addAssetPath方法可以将所有插件以及宿主的资源都放在一个AssetManager中,然后再反射替换系统原来的Resources.
此时需要注意资源文件id冲突的问题,解决方案:
- apk中的资源文件的打包工作都是通过aapt完成的,可以通过修改系统源码中aapt设置的packageid,即资源id的0x7f前缀,重新生成一个新的aapt命令替换原生的。
- 通过hook Android的打包流程,修改R文件以及最终生成的arsc文件的id完成。(gradle-small 框架)
- 一个apk除了普通的类以外,还包含需要与AMS等系统服务打交道的类。如果apk中不包含这些类,比如插件中全是fragment,那么就无需做特殊的处理了,早期的一个插件化框架就是基于fragment完成的。
而这些需要系统进行创建的特殊类,比如四大组件以及application。加载这些特殊类有两种方案:动态替换以及静态代理
-
动态替换
- activity以及service的动态替换
activity和service的创建,系统会检测是否在manifest中注册,动态替换是在开发中通过hook系统类,欺骗AMS的校验,再校验通过后再启动为插件中的类。
校验前:
修改Activity的startAcivityForResult方法,先替换为占坑的activity,同时吧真实要启动的信息保存在intent中。
或者通过 hook AMS 完成替换校验后:
为 ActivityThread 的H 类加一个callback,再启动activity以及创建service 时替换为真实插件中的类。同时需要替换原来的Instrumentation类,因为activity创建是通过这个类的newActivity方法,service创建是通过ActivityThread的handleCreateService方法。若需要对activity的launchMode支持,需要提前在宿主中定义不同的launchMode占坑activity。
service的启动因为多次start只会创建一次,所以此方案需要再宿主中提前定义好多个占坑service
- receiver的处理
动态广播可以当成一个普通的观察者类看待,无需做特殊处理。
静态广播:通过PackageParser解析插件apk的manifest中的receiver信息,然后再动态注册这些广播。
若想支持静态广播的特性,需要提前在宿主中定义一个占坑的静态receiver,再想办法将插件receiver与这个占坑receiver的action一一对应起来(比如插件receiver定义一个metedata值,用来定义占坑receiver对应的action值)
然后再收到占坑receiver的广播后再分发给插件的receiver。
此时占坑receiver需要定义很多的action针对不同的插件receiver- contentprovider的处理
先解析manifest中的provider信息,然后反射调用ActivityThread 的 installContentProviders 方法 把插件 中的 provide 安装到 宿主中
也可以通过 在宿主中定义一个contentProvider作为中转,再间接调用插件的provider。
-
静态代理
静态代理是将插件中的所有类都当成一个普通类来对待,这样就不需要hook系统类来完成替换工作了。
- activity的应用
而要使普通类具备activity的生命周期,需要先在宿主中定义一个代理的activity,再代理的activity中反射创建出插件的activity,生命周期方法中 调用插件activity的方法。
@SuppressLint("MissingSuperCall")
class ProxyActivity : Activity() {
protected var pluginActivity: IRemoteActivity? = null
get() {
if (field != null) return field
else {
field = loadPluginActivity()
}
return field
}
override fun onCreate(savedInstanceState: Bundle?) {
// super.onCreate(savedInstanceState) // 插件activity 决定调用时机
pluginActivity?.onCreate(savedInstanceState)
}
fun superOnCreate(savedInstanceState: Bundle?) { // 需要插件activity调用 所以改为public
super.onCreate(savedInstanceState)
}
private fun loadPluginActivity() : IRemoteActivity {
val pluginName = intent?.getStringExtra(PluginConstant.PLUGIN_NAME) ?: ""
val activityName = intent?.getStringExtra("activity_name")
val a = PluginManager.getPluginClassLoader(pluginName)?.loadClass(activityName)?.newInstance() as IRemoteActivity
a.setProxy(this,pluginName)
return a
}
}
-
receiver 以及 provider 的应用
receiver: 在宿主的ProxyReceiver中 根据intent的信息 反射创建 插件 receiver 对象 调用其的onReceiver方法
provider: 在宿主ProxyProvider中 通过uri 解析出插件对应provider的信息,反射调用对应的方法。
-
service 的 应用
重复启动一个service的时候,是不会重复创建的,所以一个代理service只能对应一个插件service。而若想做到一对多的关系,可以自己管理插件service的创建销毁,同时因为service 能够跨进程间通信,所以需要一个ProxyService来代理,将真实要启动的service信息通过intent传递给ProxyService
startService: 在宿主ProxyService中的onStartCommand方法创建插件service 并调用onCreate 再调用onStartCommand方法(重复启动service时,onStartCommand此方法会重复调用)
bindService: 此方法会返回一个IBinder对象,同时会传入一个ServiceConnection接口做回调,而unBinder时直接通过connection停止的,所以再处理时 需要将intent 与 connection 做一个对应关系存储。(问题: 重复bindService,onBind()方法只会走一次。)
-
so库加载
so的加载有两种方案:
- System.loadLibrary()
此方法可以加载放在libs目录下的so文件。在创建DexClassLoader时,第三个参数用来指定解压后的so文件存放目录,即可以用这种方式加载。 - System.load()
此方法 , 可以加载任意路径下的so文件。
注意,如果要用此方法在64位设备上加载32位的so库,需要现在libs目录下放一个占位用的32位(armeabi-v7a),先用loadLibrary加载一次。
-
相关技术
- 降级开发 为奔溃的界面(activity),下发一个H5页面,用来暂时替代。
- 混淆 插件宿主同时用到的公共库必须用同一套混淆规则。需要在宿主中通过反射创建的类,不能被混淆。打包插件apk,可以分dex打包,打包后,最终可以去掉与宿主共用到的公共库dex。
- 增量更新 用bsdiff 生成两个apk之间的差异文件,然后再用ApkPatchLibrary 将这个差异包与以前的apk合并成一个新的apk。