1. 发展历史和流派
先稍微介绍一下插件化的发展历史。插件化技术,主要用在新闻,电商,阅读,出行,视频等领域,可以看到包含了我们生活的很多场景。在应用迭代的过程中,1. 能快速的修复应用出问题的部分,2. 为了抢占市场,快速的根据市场反应进行迭代,3. 将不常用功能模块做成插件,减少包体积,这几点对于应用的发展都是重点。在这种背景下,插件化技术应运而生。
下面是比较出名的几个插件化框架,根据出现的时间排序,通过研究他们的原理,可以把发展历史大概分成三代。
2015 年及以前,插件化技术分成了明显的两派:以 DroidPlugin 为代表的动态替换方案和以 dynamic-load-apk 为代表的静态代理方案。后来动态替换方案因为侵入性低,灵活稳定,逐步得到了更多人的支持。而从热修复方案和 react native 开始应用以来,插件化技术不再是唯一的选择,而是进入慢慢完善的阶段,到 2017 年以后插件化技术基本成熟,兼容性和稳定性也达到了较高的层次。大家有兴趣的可以看看上面讲到的几个开源库,体会插件化技术的发展历程。
2. 插件化技术
插件化技术的技术主要可以概括为以下几点:1. 插件和宿主之间的代码和资源互用 2. 插件的四大组件支持和跳转
这里我们说到了插件和宿主之间的代码和资源互用。其实这里也是有学问的。插件根据是否需要共享资源代码分为独立插件和耦合插件。独立插件是单独运行在一个进程中的,与宿主完全隔离,崩溃不会影响到宿主。但是耦合插件却是和宿主运行在一个进程中,所以插件崩溃,宿主也崩溃了。所以一般业务要根据资源和代码的耦合程度,插件的可靠性等综合考虑插件类型。
我们接下来慢慢讲解。
2.1 代码和资源互通
插件与 dex
因为可能看我文章的还有没接触过插件化的同学,所以增加这一部分讲解插件和 dex 到底是怎么一种存在形式,插件,我们可以理解为一个单独打包出来的 apk。在项目中我们可以建立 module 并且在模块的 build.gradle 中把 apply plugin: ‘com.android.library’ 改为 apply plugin: ‘com.android.application’。这样对这个模块打包的产物就是 apk。
apk 在打包的过程中,有一个 class 文件打入 dex 的操作,最终 Apk 中存在的是 dex。加载这种 dex 中的类,使用的 ClassLoader 也很有讲究。Android 常用的就是 PathClassLoader 和 DexClassLoader。PathClassLoader 适用于已经安装了的 apk,一般作为默认加载器。而这里插件的 apk 是没有安装的,所以我们需要使用 DexClassLoader 来加载插件 dex 中的类。下面是一段基本代码,演示了如何从插件 apk 的 dex 中读取类。
// 生成 ClassLoaderFile apkFile = File(apkPath, apkName);String dexPath = apkFile.getPath();File releaseFile = context.getDir(“dex”, 0);DexClassLoader loader = new DexClassLoader(dexPath, releaseFile.getAbsolutePath(), null, getClassLoader());
// 加载类,使用类的方法 Class bean = loader.loadClass(“xxx.xxx.xxx”) // 填入类的包名 Object obj = bean.newInstance();Method method = bean.getMethod(“xxx”) // 填入方法名 method.setAccessible(true);method.invoke(obj)
这样,我们就可以通过反射来获取到类,并使用相应的方法了。
面向接口编程
大家会看到,如果像上面那样大量的使用反射,代码是相当丑陋的,扩展性能也差。这让我们想到了,能不能参考依赖倒置原则中的面向接口或抽象编程的思想,预先定义好接口。这样等需要使用的时候,就只需要把对象转换为接口,就能调用接口的方法了。
比如我们 app 模块和插件模块 plugin 依赖了接口模块 interface, interface 中定义了接口 IPlugin。IPlugin 的定义是
interface IPlugin {void sayHello(String name)}
plugin 中就可以定义实现类
class PluginImpl implement IPlugin {@override void sayHello(String name) {Log.d(“log”,”hello world” + name); }}
这样,我们就可以在宿主 app 模块中去使用。具体的使用方法可以有反射和服务发现机制。为了简单,这里只用反射来调用具体的实现类。
Class pluginImpl = loader.loadClass(“xxx.xxx.xxx”) // PluginIMpl 类的包名 Object obj = pluginImpl.newInstance(); // 生成 PluginImpl 对象 IPlugin plugin = (IPlugin)obj;plugin.sayHello(“AndroidEarlybird”);
既然接口都给出了,我们想做别的事情肯定就得心应手了。但是值得注意的是这里的前提是宿主和插件都需要依赖接口模块,也就是说双方是有代码和资源依赖的,因此这种方法只适用于耦合插件,独立插件的话就只能用反射来调用了。
PMS
在插件化技术中,ActivityManagerServiche(AMS)和 PackageManagerService(PMS)都是相当重要的系统服务。AMS 自不用说,四大组件各种操作都需要跟它打交道,PMS 也十分重要,完成了诸如权限校验 (checkPermission,checkUidPermission),Apk meta 信息获取(getApplicationInfo 等),四大组件信息获取(query 系列方法) 等重要功能。
使用 PMS
android 一般使用 PMS 来进行应用安装,安装的时候 PMS 需要借助于 PackageParser 进行 apk 解析工作,主要负责解析出一个 PackageParser.Package 对象,这个对象还是很大用途的。下面是这个 Package 对象的一些属性值。
可以看到我们通过这个类可以拿到 apk 中的四大组件,权限等信息,在插件化中,我们有时候会需要利用这个类去拿到广播的信息来处理插件中的静态广播。
那么如何使用 PackageParser 这个类呢?下面是 VirtualApk 的一些使用
public static final PackageParser.Package parsePackage(final Context context, final File apk, final int flags) throws PackageParser.PackageParserException {if (Build.VERSION.SDK_INT >= 24) {return PackageParserV24.parsePackage(context, apk, flags); } else if (Build.VERSION.SDK_INT >= 21) {return PackageParserLollipop.parsePackage(context, apk, flags); } else {return PackageParserLegacy.parsePackage(context, apk, flags); } }
private static final class PackageParserV24 {static final PackageParser.Package parsePackage(Context context, File apk, int flags) throws PackageParser.PackageParserException {PackageParser parser = new PackageParser(); PackageParser.Package pkg = parser.parsePackage(apk, flags); ReflectUtil.invokeNoException(PackageParser.class, null, "collectCertificates", new Class[]{PackageParser.Package.class, int.class}, pkg, flags); return pkg; } }
因为 PackageParser 针对系统版本变化很大,所以 VirtualApk 对这个类做了多版本的适配,我们这里只展示了一种。
Hook PMS
正如我们需要 hook AMS 去进行一些插件化的一些工作,有时候我们也得对 PMS 进行 hook。通过看源码,我们知道 PMS 的获取也是通过 Context 获取的,直奔 ContextImpl 类的 getPackageManager 方法。
public PackageManager getPackageManager() { if (mPackageManager != null) {return mPackageManager;}
IPackageManager pm = ActivityThread.getPackageManager(); if (pm != null) {// Doesn't matter if we make more than one instance. return (mPackageManager = new ApplicationPackageManager(this, pm)); } return null;}
// 继续跟进到 ActivityThread 的 getPackageManager 方法中 public static IPackageManager getPackageManager() { if (sPackageManager != null) {return sPackageManager;} IBinder b = ServiceManager.getService(“package”); sPackageManager = IPackageManager.Stub.asInterface(b); return sPackageManager;}
这里我们可以看到,要想 hook PMS 需要把这两个地方都 hook 住:
ActivityThread 的静态字段 sPackageManager
通过 Context 类的 getPackageManager 方法获取到的 ApplicationPackageManager 对象里面的 mPM 字段。
示例代码如下:
// 获取全局的 ActivityThread 对象 Class<?> activityThreadClass = Class.forName(“android.app.ActivityThread”);Method currentActivityThreadMethod = activityThreadClass.getDeclaredMethod(“currentActivityThread”);Object currentActivityThread = currentActivityThreadMethod.invoke(null);
// 获取 ActivityThread 里面原始的 sPackageManagerField sPackageManagerField = activityThreadClass.getDeclaredField(“sPackageManager”);sPackageManagerField.setAccessible(true);Object sPackageManager = sPackageManagerField.get(currentActivityThread);
// 准备好代理对象, 用来替换原始的对象 Class<?> iPackageManagerInterface = Class.forName(“android.content.pm.IPackageManager”);Object proxy = Proxy.newProxyInstance(iPackageManagerInterface.getClassLoader(), new Class<?>[] { iPackageManagerInterface}, new HookHandler(sPackageManager));
// 1. 替换掉 ActivityThread 里面的 sPackageManager 字段 sPackageManagerField.set(currentActivityThread, proxy);
// 2. 替换 ApplicationPackageManager 里面的 mPM 对象 PackageManager pm = context.getPackageManager();Field mPmField = pm.getClass().getDeclaredField(“mPM”);mPmField.setAccessible(true);mPmField.set(pm, proxy);
管理 ClassLoader
上面我们讲到了如何利用 ClassLoader 来加载 dex 中的类,现在我们再来深入聊聊这个话题。首先,需要明确的是,因为我们插件的类都是位于没有安装的 apk 的 dex 中,所以我们不能直接使用主 app 的 ClassLoader。那么就会有多种解决方案。
比较直接的思想是通过对每一个插件都新建一个 ClassLoader 来做加载。那么如果我们插件很多的时候,我们需要做的就是把每个插件的 ClassLoader 给记录下来,当使用某个插件的类的时候,用它对应的 ClassLoader 去加载。正如我们上节的例子中展示的那样。
另一种思想是直接操作 dex 数组。宿主和插件的 ClassLoader 都会对应一个 dex 数组。那么我们如果能把插件的 dex 数组合并到宿主的 dex 数组里面去的话,我们就能用宿主的 ClassLoader 来反射加载插件的 dex 数组中的类了。这样做的目的是不需要管理插件的 ClassLoader,只要用宿主的 ClassLoader 就行了。比如我们曾经在 Android 插件化系列一: 开篇前言,Binder 机制,ClassLoader 中讲到 DexClassLoader 的源代码。
public class BaseDexClassLoader extends ClassLoader {private final DexPathList pathList;
public BaseDexClassLoader(String dexPath, File optimizedDirectory, String libraryPath, ClassLoader parent) {super(parent); // 见下文 // 收集 dex 文件和 Native 动态库【见小节 3.2】this.pathList = new DexPathList(this, dexPath, libraryPath, optimizedDirectory); }}
public class DexPathList {private Element[] dexElements;
public DexPathList(ClassLoader definingContext, String dexPath, String librarySearchPath, File optimizedDirectory) {this.dexElements = makeDexElements(splitDexPath(dexPath), optimizedDirectory, suppressedExceptions, definingContext); }
private static List<File> splitDexPath(String path) {return splitPaths(path, false); }
private static List<File> splitPaths(String searchPath, boolean directoriesOnly) {List<File> result = new ArrayList<>(); if (searchPath != null) {for (String path : searchPath.split(File.pathSeparator)) {// 省略} } return result; }}
从上面我们可以看出,dexPath 字符串是由多个分号分割的。拆分成字符串数组以后,每个 path 都是一个外部的 dex/apk 路径。那么我们很自然的想到,能不能把插件的 dex 路径手动添加到宿主的 dexElements 数组中呢?答案当然是 ok 的,方案就是使用 Hook。我们可以先反射获取到 ClassLoader 的 dexPathList,然后再获取这个 list 的 dexElements 数组,然后手动把插件构建出 Element,再拷贝到 dexElements 数组中。热修复框架 Nuwa 也是使用这种思想。
第三种思路是 ClassLoader delegate。本文推荐这种方法。首先我们自定义 ClassLoader,取代原先宿主的 ClassLoader,并且把宿主作为 Parent,同时在自定义的 ClassLoader 中用一个集合放置所有插件的 ClassLoader,然后这个自定义 ClassLoader 在加载任何一个类的时候,依据双亲委托机制,加载类都会先从宿主的 ClassLoader 中寻找,没有的话再遍历 ClassLoader 集合寻找能加载这个类的插件 ClassLoader。当然这里又会有提高效率的优化点,比如遍历集合的方式可以改为先从已加载过的集合中寻找,再从未加载过的集合中寻找。下面是示例代码。
class PluginManager {public static void init(Application application) {// 初始化一些成员变量和加载已安装的插件 mPackageInfo = RefInvoke.getFieldObject(application.getBaseContext(), “mPackageInfo”); mBaseContext = application.getBaseContext(); mNowResources = mBaseContext.getResources();
mBaseClassLoader = mBaseContext.getClassLoader(); mNowClassLoader = mBaseContext.getClassLoader(); ZeusClassLoader classLoader = new ZeusClassLoader(mBaseContext.getPackageCodePath(), mBaseContext.getClassLoader());
File dexOutputDir = mBaseContext.getDir("dex", Context.MODE_PRIVATE); final String dexOutputPath = dexOutputDir.getAbsolutePath(); for(PluginItem plugin: plugins) {DexClassLoader dexClassLoader = new DexClassLoader(plugin.pluginPath, dexOutputPath, null, mBaseClassLoader); classLoader.addPluginClassLoader(dexClassLoader); } // 替换原有的宿主的 ClassLoader 为自定义 ClassLoader,将原来的宿主 ClassLoader 作为自定义 ClassLoader 的 RefInvoke.setFieldObject(mPackageInfo, "mClassLoader", classLoader); Thread.currentThread().setContextClassLoader(classLoader); mNowClassLoader = classLoader; }}
class ZeusClassLoader extends PathClassLoader {private List<DexClassLoader> mClassLoaderList = null;
public ZeusClassLoader(String dexPath, ClassLoader parent, PathClassLoader origin) {super(dexPath, parent);
mClassLoaderList = new ArrayList<DexClassLoader>();}
/** * 添加一个插件到当前的 classLoader 中 */ protected void addPluginClassLoader(DexClassLoader dexClassLoader) {mClassLoaderList.add(dexClassLoader); }
@Override protected Class<?> loadClass(String className, boolean resolve) throws ClassNotFoundException {Class<?> clazz = null; try { // 先查找 parent classLoader,这里实际就是系统帮我们创建的 classLoader,目标对应为宿主 apk clazz = getParent().loadClass(className); } catch (ClassNotFoundException ignored) { }
if (clazz != null) {return clazz;}
// 挨个的到插件里进行查找 if (mClassLoaderList != null) {for (DexClassLoader classLoader : mClassLoaderList) {if (classLoader == null) continue; try {// 这里只查找插件它自己的 apk,不需要查 parent,避免多次无用查询,提高性能 clazz = classLoader.loadClass(className); if (clazz != null) {return clazz;} } catch (ClassNotFoundException ignored) {}} } throw new ClassNotFoundException(className + "in loader" + this); }}
资源
Resources&AssetManager
android 中的资源大致分为两类:一类是 res 目录下存在的可编译的资源文件,比如 anim,string 之类的,第二类是 assets 目录下存放的原始资源文件。因为 Apk 编译的时候不会编译这些文件,所以不能通过 id 来访问,当然也不能通过绝对路径来访问。于是 Android 系统让我们通过 Resources 的 getAssets 方法来获取 AssetManager,利用 AssetManager 来访问这些文件。
Resources resources = context.getResources();AssetManager manager = resources.getAssets();InputStream is = manager.open(“filename”);
Resources 和 AssetManager 的关系就像销售和研发。Resources 负责对外,外部需要的 getString, getText 等各种方法都是通过 Resources 这个类来调用的。而这些方法其实都是调用的 AssetManager 的私有方法。所以最终两类资源都是 AssetManager 在兢兢业业的向 Android 系统要资源,为外界服务着。
AssetManager 里有个很重要的方法 addAssetPath(String path)方法,App 启动的时候会把当前 apk 的路径传进去,然后 AssetManager 就能访问这个路径下的所有资源也就是宿主 apk 的资源了。那么 idea 就冒出来了,如果我们把插件的地址也传进这个方法去,是不是就能得到一个能同时访问宿主和插件的所有资源的“超级”AssetManager 了呢?答案是肯定的,这也是插件化对资源的一种解决方案。
下面是一段示例代码展示了获取宿主的 Resources 中的 AssetManager,然后调用 addAssetPath 添加插件路径,最后生成一个新的 Resources 的方法
// 新生成 AssetManager,调用 addAssetPathAssetManager assetManager = resources.getAssets(); // 先通过 Resources 拿到示例代码 Method addAssetPath = assetManager.getClass().getMethod(“addAssetPath”, String.class);addAssetPath.invoke(assetManager, dexPath1);mAssetManager = assetManager;
// 根据新生成的 AssetManager 生成 ResourcesmResources = new Resources(mAssetManager, super.getResources().getDisplayMetrics(), super.getResources().getConfiguration());
接下来我们要分别将宿主和插件的原有 Resources 替换成我们上面生成的 Resources。注意这里传入的 application 应该是宿主和插件对应的 Application。
Object contextImpl = RefInvoke.getFieldObject(“android.app.ContextImpl”, application, “getImpl”) // 获取 Application 的 contextLoadedApk loadedApk = (LoadedApk)RefInvoke.getFieldObject(contextImpl, “mPackageInfo”)RefInvoke.setFieldObject(loadedApk, “mResources”, resources);RefInvoke.setFieldObject(application.getBaseContext(), “mResources”, resources);
除了需要替换 Application 的 Resources 对象,我们也需要替换 Activity 的 Resources 对象,宿主和插件的 Resources 都需要替换。这是因为他们都是 Context,只替换 Application 的并不能影响到 Activity。我们可以在 Instrumentation 回调 callActivityOnCreate 的时候去替换。这点在后面 Activity 插件化处理部分再详细讲解。
上面只是展示了使用,想了解更多信息的可以查看 VirtualApk
解决资源冲突
在 Android 插件化系列二: 资源与打包流程中我们提到了插件和宿主分别打包的时候可能会存在资源 id 冲突的情况,上面我们使用了一个超级 Resource 之后,id 如果重复了,运行的时候使用 id 来查找资源就会报错。
为了解决 id 冲突的问题一般有三种方案:
修改 android 打包的 aapt 工具,将插件资源前缀改为 0x02 – 0x7e 之间的数值
进入到哪个插件,就为哪个插件生成新的 AssetManager 和 Resources
其中,方案二比较复杂,并且不利于宿主和插件资源的互相调用。所以我们在上节采用的是超级 Resources 的方案,所以这里我们介绍一下方案一,也就是修改 aapt 工具。
aapt 是 android 打包资源的处理工具,大多数的插件化开源库对齐进行改造无外乎都是两种方式:
修改 aapt 源码,定制 aapt 工具,编译期间修改资源 id 的 PP 段,可以参考 Android 中如何修改编译的资源 ID 值, DynamicApk 就是这样使用的。
修改 aapt 的产物,即 resources.arsc,编译后期重新整理插件 Apk 的资源,编排 ID。VirtualApk 就是采用了这种方案,自定义 gradle transform 插件,hook 了 gradle 打包的 ProcessAndroidResources task,在 apk 资源编译任务完成后,重新设置插件的 resources.arsc 文件中的资源 id, 并更新 R.java 文件。可以参考插件化 - 解决插件资源 ID 与宿主资源 ID 冲突的问题这篇文章。
可以看到 aapt(1)处理插件化的资源并不是很友好,开发和维护难度都比较大,后来 google 推出了 Android App Bundle 这个和插件很类似的 feature,就推出了 aapt2 来支持了资源分包。我们注意官网上这几个 aapt2 的打包参数:
是不是发现官方已经给我们支持好了按 package 区分资源前缀 id,多美好啊哈哈。
当然这里也是有坑的。那就是需要 buildTools 版本大于 28.0.0 在 buildTools 28.0.0 以前,aapt2 自带了资源分区,通过–package-id 参数指定。但是该分区只支持 >0x7f 的 PP 段,而在 Android 8.0 之前,是不支持 >0x7f 的 PP 段资源的,运行时会抛异常。但是当指定了一个 <0x7f 的 PP 段资源后,编译资源时却会报错
error: invalid package ID 0x15. Must be in the range 0x7f-0xff..
所以对于 Android P 之前使用的 buildTools 版本(<28.0.0),我们必须通过修改 aapt2 的源码达到资源分区的目的。而在 28.0.0 以后,aapt2 支持了 <0x7f 预留 PP 段分区的功能,只需要指定参数 –allow-reserved-package-id 即可。
–allow-reserved-package-id –package-id package-id
插件使用宿主资源
在我们为宿主开发插件的时候,经常不可避免的出现插件要使用宿主中资源的情况,如果我们把宿主的资源 copy 一份放在插件中,那无疑会大大增加包的大小,并且这些都是重复资源,是不应该在 App 中存在的。那么我们就得想办法让插件使用宿主的资源。比如这样
前面已经讲到了,我们可以通过为插件和宿主一起构建一个超级 Resources,包括了插件和宿主所有的资源,理论上可以通过资源 id 获取到所有的资源,那么问题来了,插件中的 R 文件是不包含宿主的 R 文件的,我们在编码的时候怎么使用呢?
下面分代码使用和 xml 使用两种使用方式来说解决方案:
代码使用:在插件资源打包任务 processResourcesTask 完成后将宿主的 R.txt 文件 (打包过程中产生,位置在 build/intermediates/symbols/xx/xx/R.txt) 合并到插件的 R.txt 文件,然后再生成 R.java,这样就可以正常的使用 R 文件来索引资源了
xml 使用:我们需要在 aapt2 打包的时候指定 - I 参数。
这样,我们通过 - I 指定宿主的资源包,就可以在 xml 中使用宿主的资源了。
总结
本节我们首先介绍插件代码的 dex 加载,给出了利用反射和面向接口编程来获取插件中的代码的方法,然后介绍了通过自定义 delegate ClassLoader 的方法来更好的加载插件和宿主中的类,接下来介绍了 PMS 如何获取插件的信息以及如何进行自定义 hook,最后讲到了插件使用宿主资源的一些知识。到了这一步,我们已经可以获取到插件的各种信息,可以实现宿主和插件中的代码互通,可以实现插件调用宿主的资源,基本上算是迈出了一大步!但是只有代码和资源是不够的,接下来我们看看怎么处理 android 的四大组件,这一块才是重头戏,也是插件化的精髓
2.2 四大组件支持
android 的四大组件其实有挺多的共通之处,比如他们都接受 ActivityManagerService(AMS)的管理,都需要通过 Binder 机制请求 AMS 服务。并且他们的请求流程也是基本相通的,其中 Activity 又是最重要的组件,出镜最多,同时也是日常开发接触最多的组件,我们将会主要以 Activity 为例,讲解插件化对四大组件的支持,其余三个组件有不同或值得注意的地方我们会另外指出来。当然,针对四大组件的解决方案有很多种,本文限于篇幅只介绍 DroidPlugin 的动态替换方案。
Activity
AndroidManifest.xml 预占位
相信做过 Android 开发的都知道,四大组件基本都是要在 AndroidManifest.xml 中定义的,不然系统就会报错,然后问你 have you declared this activity in your AndroidManifest.xml?【必须在 AndroidMainfest.xml 中定义四大组件】这一点对插件化确实是比较严重的限制,毕竟我们并没有办法提前就把插件中的 Activity 声明进去,但是这个限制也并不是没办法解决的。比如 DroidPlugin 就采用了预占位 Activity 到 AndroidManifest.xml 中的方案。
DroidPlugin 的方案思想很简单,先在 AndroidManifest.xml 中预定义好各种 LaunchMode 的占位 Activity 和其余三大组件。比如
<activity android:name=”.StubSingleTaskActivity1″ android:exported=”true” android:launchMode=”singleTask” android:theme=”@style/Theme.NoActionBar” android:screenOrientation=”portrait” />
<activity android:name=”.StubSingleTopActivity1″ android:exported=”true” android:launchMode=”singleTop” android:theme=”@style/Theme.NoActionBar” android:screenOrientation=”portrait” />
这样的话,我们就要实行狸猫换太子的方法,把本来想要开启的 Activity 换成 StubActivity,然后躲过了系统对【必须在 AndroidMainfest.xml 中定义四大组件】的审查真正的开始 start Activity 的时候再去打开真正的目的 Activity。那么我们怎么去实现这个想法呢,这就要求我们熟悉 Activity 的启动流程了。
startActivity 流程
startActivity 的流程比较繁杂,甚至可以作为一篇单独的文章来讲解。网上有很多的文章在讲解,比较详细牛逼的是老罗的 Android 应用程序的 Activity 启动过程简要介绍和学习计划。大家如果有兴趣的话可以参考。我这里只简明扼要的讲解部分的流程。
首先先看一个流程图
首先我们都是从 startActivity 进去的,辗转发现它调用了 Instrumentation 的 execStartActivity 方法,接着在这个函数里面调用了 ActivityManagerNative 类的 startActivity 方法,请求到了 ActivityManagerService 的服务。这一点就是我们在 Android 插件化系列一: 开篇前言,Binder 机制,ClassLoader 讲到过的 Binder 机制在 Activity 启动过程中的体现。可以看到就是在 AMS 的 startActivity 的方法中校验了 Activity 是否注册,确定了 Activity 的启动模式,AMS 我们没办法改啊,所以咱们得出个结论一定要在校验前的流程里把 Activity 给替换掉。继续往下看,可以看到 ActivityStackSupervisor 把启动的重任最终委托给了 ApplicationThread。
我们在前面的系列一中说过,Binder 机制其实是互为 Client 和 Server 的,在 app 申请 AMS 服务的时候,AMS 是 Server,AMP 是 AMS 在 app 的代理。而在申请到 AMS 服务以后,AMS 需要请求 App 进行后续控制的时候,ApplicationThread 就是 Server,ApplicationThreadProxy 就是 ApplicationThread 在 AMS 侧的代理。
继续往下看,可以看到 ActivityThread 调用了 H 类,最终调用了 handleLaunchActivity 方法,由 Instrumentation 创建出了 Activity 对象,启动流程结束。
“狸猫换太子”
看完了上面的启动流程,大家可以想到,在这个流程中我只要在调用 AMS 前把目标 Activity 替换成 StubActivity(上半场),在 AMS 校验完,马上要打开 Activity 的时候替换为目标 Activity(下半场),这样就可以达到“狸猫换太子”启动目标 Activity 的目的了啊。因为流程较长,参与的类较多,所以我们可以选择的 hook 点也是相当多的,但是我们越早 hook,后续的操作越多越容易出问题,所以我们选择比较后面的流程去 hook。这里选择:
上半场,hook ActivityManagerNative 对于 startActivity 方法的调用
下半场,hook H.mCallback 对象,替换为我们的自定义实现,
hook AMN 下面是一些示例代码,可以看到我们替换掉交给 AMS 的 intent 对象,将里面的 TargetActivity 的暂时替换成已经声明好的替身 StubActivity。
if ("startActivity".equals(method.getName())) { // 只拦截这个方法 // 替换参数, 任你所为; 甚至替换原始 Activity 启动别的 Activity 偷梁换柱
// 找到参数里面的第一个 Intent 对象 Intent raw; int index = 0;
for (int i = 0; i < args.length; i++) {if (args[i] instanceof Intent) {index = i; break;} } raw = (Intent) args[index];
Intent newIntent = new Intent();
// 替身 Activity 的包名, 也就是我们自己的包名 String stubPackage = raw.getComponent().getPackageName();
// 这里我们把启动的 Activity 临时替换为 StubActivity ComponentName componentName = new ComponentName(stubPackage, StubActivity.class.getName()); newIntent.setComponent(componentName);
// 把我们原始要启动的 TargetActivity 先存起来 newIntent.putExtra(AMSHookHelper.EXTRA_TARGET_INTENT, raw);
// 替换掉 Intent, 达到欺骗 AMS 的目的 args[index] = newIntent;
Log.d(TAG, "hook success"); return method.invoke(mBase, args);
}
hook H.mCallback 前面我们说过,ActivityThread 是借助于 H 这个类完成四大组件的操作管理。H 继承自 Handler,我们看看 Handler 处理消息的 dispatchMessage 方法。
public void dispatchMessage(Message msg) {if (msg.callback != null) {handleCallback(msg); } else {if (mCallback != null) {if (mCallback.handleMessage(msg)) {return;} } handleMessage(msg); }}
而 H 的 handleMessage 方法中正是处理 LAUNCH_ACTIVITY,CREATE_SERVICE 等消息的地方。所以我们就会想,在 mCallback.handleMessage 中替换回原来的 Activity 应该就是最晚的时间点了吧。下面是自定义的 Callback 类,反射设置为 ActivityThread 的 H 的 mCallback 就行了。
class MockClass2 implements Handler.Callback {
Handler mBase;
public MockClass2(Handler base) {mBase = base;}
@Override public boolean handleMessage(Message msg) {switch (msg.what) {// ActivityThread 里面 "LAUNCH_ACTIVITY" 这个字段的值是 100 // 本来使用反射的方式获取最好, 这里为了简便直接使用硬编码 case 100: handleLaunchActivity(msg); break;
}
mBase.handleMessage(msg); return true; }
private void handleLaunchActivity(Message msg) { // 这里简单起见, 直接取出 TargetActivity; Object obj = msg.obj;
// 把替身恢复成真身 Intent intent = (Intent) RefInvoke.getFieldObject(obj, "intent");
Intent targetIntent = intent.getParcelableExtra(AMSHookHelper.EXTRA_TARGET_INTENT); intent.setComponent(targetIntent.getComponent()); }}
替换 Resources
还记得我们在第一节留下了一个问题吗,就是 Activity 的资源替换要在 Instrumentation 回调 callActivityOnCreate 的时候进行。这个时间点比较临近 onCreate,Instrumentation 也比较方便去 hook。下面展示这个技术,需要传入超级 Resources。
public void hookInstrumentation(){ Object currentActivityThread = RefInvoke.invokeStaticMethod(“android.app.ActivityThread”, “currentActivityThread”); // 拿到原始的 mInstrumentation 字段 Instrumentation mInstrumentation = (Instrumentation) RefInvoke.getFieldObject(currentActivityThread, “mInstrumentation”); // 创建代理对象 Instrumentation evilInstrumentation = new EvilInstrumentation(mInstrumentation, resources); // 这里的 resources 是我们的超级 Resources RefInvoke.setFieldObject(currentActivityThread, “mInstrumentation”, evilInstrumentation);}
// 这里的 Activity 是在 public class EvilInstrumentation extends Instrumentation {Instrumentation mBase; Resources mRes;
public EvilInstrumentation(Instrumentation base,Resources res) {mBase = base; mRes = res;}
@override public void callActivityOnCreate(Activity activity, Bundle bundle) {// 替换 Resources if (mRes != null) {RefInvoke.setFieldObject(activity.getBaseContext().getClass(), activity.getBaseContext(), "mResources", mRes); } super.callActivityOnCreate(activity, bundle); }}
Service
Service 的处理和 Activity 的基本一样,区别是调用多次 startService 并不会启动多个 Service 实例,而是只有一个实例,所以我们的占位 Service 得多定义一些。
BroadcastReceiver
BroadcastReceiver 的插件化和 Activity 的不太一样。Android 中的广播分为两种:静态广播和动态广播,动态广播不需要和 AMS 交互,就是一个普通类,只要按照前面的 ClassLoader 方案保证他能加载就行了。但是静态广播比较麻烦,除了需要在 AndroidManifest.xml 中进行注册以外,他和 Activity 不一样的是,他还附加了 IntentFilter 信息。而 IntentFilter 信息是随机的,无法被预占位的。这个时候就只能把取出插件中的静态广播改为动态广播了。虽然会有一些小问题,但是影响不大
前面我们讲到了 PackageParser 可以获取到插件的四大组件的信息,存储到 Package 对象中,那么我们就有个思路,通过 PMS 获取到 BroadcastReceiver,然后把其中的静态广播改为动态广播.
public static void preLoadReceiver(Context context, File apkFile) {// 首先调用 parsePackage 获取到 apk 对象对应的 Package 对象 Object packageParser = RefInvoke.createObject("android.content.pm.PackageParser"); Class[] p1 = {File.class, int.class}; Object[] v1 = {apkFile, PackageManager.GET_RECEIVERS}; Object packageObj = RefInvoke.invokeInstanceMethod(packageParser, "parsePackage", p1, v1);
// 读取 Package 对象里面的 receivers 字段, 注意这是一个 List<Activity> (没错, 底层把 <receiver> 当作 <activity> 处理) // 接下来要做的就是根据这个 List<Activity> 获取到 Receiver 对应的 ActivityInfo (依然是把 receiver 信息用 activity 处理了) List receivers = (List) RefInvoke.getFieldObject(packageObj, "receivers");
for (Object receiver : receivers) {registerDynamicReceiver(context, receiver); } }
// 解析出 receiver 以及对应的 intentFilter // 手动注册 Receiver public static void registerDynamicReceiver(Context context, Object receiver) {// 取出 receiver 的 intents 字段 List<? extends IntentFilter> filters = (List<? extends IntentFilter>) RefInvoke.getFieldObject("android.content.pm.PackageParser$Component", receiver, "intents");
try {// 把解析出来的每一个静态 Receiver 都注册为动态的 for (IntentFilter intentFilter : filters) {ActivityInfo receiverInfo = (ActivityInfo) RefInvoke.getFieldObject(receiver, "info");
BroadcastReceiver broadcastReceiver = (BroadcastReceiver) RefInvoke.createObject(receiverInfo.name); context.registerReceiver(broadcastReceiver, intentFilter); } } catch (Exception e) {e.printStackTrace(); } }
ContentProvider
ContentProvider 的插件化方法和 BroadcastReceiver 的很像,但是和 BroadcastReceiver 不同的是,BroadcastReceiver 中的广播叫做注册,但 ContentProvider 是要“安装”。方案是:首先,调用 PackageParser 的 parsePackage 方法,把得到的 Package 对象通过 generateProviderInfo 转换为 ProviderInfo 对象。
public static List<ProviderInfo> parseProviders(File apkFile) throws Exception {// 获取 PackageParser 对象实例 Class<?> packageParserClass = Class.forName("android.content.pm.PackageParser"); Object packageParser = packageParserClass.newInstance();
// 首先调用 parsePackage 获取到 apk 对象对应的 Package 对象 Class[] p1 = {File.class, int.class}; Object[] v1 = {apkFile, PackageManager.GET_PROVIDERS}; Object packageObj = RefInvoke.invokeInstanceMethod(packageParser, "parsePackage",p1, v1);
// 读取 Package 对象里面的 services 字段 // 接下来要做的就是根据这个 List<Provider> 获取到 Provider 对应的 ProviderInfo List providers = (List) RefInvoke.getFieldObject(packageObj, "providers");
// 调用 generateProviderInfo 方法, 把 PackageParser.Provider 转换成 ProviderInfo
// 准备 generateProviderInfo 方法所需要的参数 Class<?> packageParser$ProviderClass = Class.forName("android.content.pm.PackageParser$Provider"); Class<?> packageUserStateClass = Class.forName("android.content.pm.PackageUserState"); Object defaultUserState = packageUserStateClass.newInstance(); int userId = (Integer) RefInvoke.invokeStaticMethod("android.os.UserHandle", "getCallingUserId"); Class[] p2 = {packageParser$ProviderClass, int.class, packageUserStateClass, int.class};
List<ProviderInfo> ret = new ArrayList<>(); // 解析出 intent 对应的 Provider 组件 for (Object provider : providers) {Object[] v2 = {provider, 0, defaultUserState, userId}; ProviderInfo info = (ProviderInfo) RefInvoke.invokeInstanceMethod(packageParser, "generateProviderInfo",p2, v2); ret.add(info); }
return ret; }
然后我们需要调用 ActivityThread 的 installContentProviders 方法把这些 ContentProvider“安装”到宿主中。
public static void installProviders(Context context, File apkFile) throws Exception {List<ProviderInfo> providerInfos = parseProviders(apkFile);
for (ProviderInfo providerInfo : providerInfos) {providerInfo.applicationInfo.packageName = context.getPackageName(); }
Object currentActivityThread = RefInvoke.getStaticFieldObject("android.app.ActivityThread", "sCurrentActivityThread");
Class[] p1 = {Context.class, List.class}; Object[] v1 = {context, providerInfos};
RefInvoke.invokeInstanceMethod(currentActivityThread, "installContentProviders", p1, v1); }
ContentProvider 的插件化还需要注意:
App 安装自己的 ContentProvider 是在进程启动时候进行,比 Application 的 onCreate 还要早,所以我们要在 Application 的 attachBaseContext 方法中手动执行上述操作。
让外界 App 直接调用插件的 App,并不是一件特别好的事情,最好是由 App 的 ContentProvider 作为中转。因为字符串是 ContentProvider 的唯一标志,转发机制就特别适用。
总结
本文首先介绍了插件化中宿主和插件代码和资源互通的方式,然后介绍了四大组件的插件化方法,因为插件化技术太过繁杂,并没有把所有的细节都覆盖到,所介绍的方案也只是当今比较实用,经受过考验的一套,并没有介绍太多的方法。目的是让读者们和我一起,先从整体上理解插件化的机制,然后就容易去区分各种开源库的原理和思路了。
转载 https://mp.weixin.qq.com/s/hg…