识别插件的类
方案:将所有的插件 dex 合并到 宿主中,具体步骤为:
根据宿主的 ClassLoader,获取宿主的 dexElements 字段
具体步骤为: 首先反射出 BaseDexClassLoader 的 pathList 字段,它是 DexPathList 类型的;第二步,反射出 DexPathList 的 dexElements 字段,他是个数组。
根据插件的 dexFile ,反射出一个 Element 类型对象,这就是插件 dex
把插件 dex 和 宿主 dexElements 合并成一个新的 dex 数组,替换宿主之前的 dexElements 字段
欺骗系统
启动时,其实是启动 PlaceHoldActivity ,将真正要启动的 TargetActivity 放在 Intent 的数据里面,主要步骤:
1、Hook 到 ActivityManagerNative (它里面的gDefault,这个字段是个单例),把 TargetActivity 替换为 PlaceHoldActivity
2、Hook 到 H(Handler) 类的 mCallback 字段,将 PlaceHoldActivity 换成真正的 TargetActivity。或者,如果不想使用这种 Hook,可以在创建Activity (execStartActivity)对象的时候再替换回来
资源使用
原理
- 通过反射,构建一个 AssetManager 对象 mAssetManager,之后,反射调用 mAssetManager 的 addAssetPath 方法,将宿主和插件的 dexPath 全部添加进去,这样,这个新的 mAssetManager 拥有宿主以及插件中的所有的资源了。
- 根据上述的 mAssetManager 构建处一个 Resources 对象 mResources
- 使用 newTheme 创建一个 Theme 对象 mTheme
- 在 Activity 中重写 getAssets()、getResources()、getTheme() 方法,分别返回上述的mAssetManager、mResources、mTheme
1 | private void loadResources(){ |
注意一点,由于将所有资源集合在一块,所以资源id可能冲突,解决方案:修改AAPT命令,将宿主和插件之间的id前缀都区分开,比如宿主的资源id以 0x71 开头,插件1以 0x72,插件2以 0x73 等,避免冲突。
还有,如果插件需要使用宿主的资源,那么,可以在宿主中将id写死,具体方式为自定义一个public.xml ,在里面写死。然后插件中以 provided 的方式将宿主的aar引入(这样,打包的时候就不会将这个aar打进去),引用资源的时候,直接使用这个写死的id就行。
1 | <!--public.xml--> |
启动模式(LaunchMode)
普通使用的都是Standard 模式,如果要解决其他 3 种 LaunchMode 问题,使用的是占位Activity的思想,即事先为这3种 LaunchMode 创建很多 PlaceHoldActivity。比如:
1 | SingleInstancePlaceHoldActivity1 |
接下来,从服务器下载一个 Json ,指定插件Activity 要使用哪个 PlaceHoldActivity ,这里所说的 Activity 只包括 LauncherMode 为 SingleTop、SingleTask、SingleInstance 的 Activity。我们无法指定插件 Activity 的 LaunchMode ,但是通过这种指定占位,当 ActivityA 和 SingleTaskPlaceHoldActivity1 建立对应关系之后,ActivityA 的 LaunchMode 就是 SingleTask 了!
但是这里还有个小bug,无论是SingleTop 还是 SingleTask ,再回到这个Activity时,并不会触发它的 onCreate ,而是触发 它的 onNewIntent 方法!为此,我们需要在Hook的 Handler.Callback 中拦截 onNewIntent 方法,把占位的 PlaceHoldActivity 替换回插件Activity:
1 | class MockClass2 implements Handler.Callback { |