其实,用shadow来运行我们的业务插件已经很长一段时间了,但是呢,因为shadow这一块是开源了,而我们使用的版本维护者经常不on call,所以有种受制于人的赶脚。还是想抽出时间自己上手分析一遍,这样遇到问题也可以快速响应解决了。
首先看一下,一般情况下是如何实现插件能力的。 ps.这里,我将不再在文章内部贴太多实现代码,我希望可以先理清整体的思路,当然,所有相关代码我都会放在最后以供取阅。
既然是插件,那么就需要和宿主应用保持一定的距离,不能参与宿主的打包和安装,却要在宿主需要的时候出现。但是如果不参与宿主的编译打包安装,那么系统便无法感知有这么个插件的存在,继而想要通过系统方法调用插件的能力也不太可能,毕竟系统也没有承认过它呀。这也引申出插件化技术的几个关键技术点
1. 类加载/资源 加载
插件和宿主既然是两个独立的apk,那么他们的上下文也是隔离开来的。宿主无法通过自己的classLoader找到插件apk中的信息,反之亦然。同样需要使插件能够加载到插件包内的资源,同时做好与宿主资源的隔离。
2. 与系统、宿主应用的通信
没有在系统中进行安装,也就无法实现安卓中四大组件的功能。插件的activity等信息都没有向系统注册过,也就无法被系统启动从而注册上一系列生命周期了。
shadow接入指引可以看这篇文章,写的很详细
Android Tencent Shadow 插件接入指南 - cps666 - 博客园
那么依次看下,传统插件思路 和shadow是如何解决这些问题的
1. 类加载/资源 加载
1.1类加载
了解在JVM中,java代码被编译成.class文件,然后交给jvm来执行。 一般的,类加载分为: <math xmlns="http://www.w3.org/1998/Math/MathML"> 加载、链接、初始化 \color{red}{加载、链接、初始化} </math>加载、链接、初始化三个过程
在安卓系统中,提供DexClassLoader,支持 传入apk地址或者dex文件地址,即可以加载类文件。
所以,如果我们需要加载插件的class,那么就需要告诉ClassLoader插件的dex文件地址。
java
// 创建DexClassLoader实例
val dexClassLoader = DexClassLoader(
apkFile.path, // APK文件的路径
optimizedDexOutputPath, // 优化后的DEX文件存放路径
null, // 库文件搜索路径,通常为null
parentClassLoader // 父ClassLoader
)
但是,宿主也有自己的classLoader,如何让宿主apk启动插件时用插件的classLoader呢?
我们通过阅读 <math xmlns="http://www.w3.org/1998/Math/MathML"> B a s e C l a s s L o a d e r \color{red}{BaseClassLoader} </math>BaseClassLoader的findClass方法可以得出,classLoader会通过遍历pathList中的dexElements来完成类的查找,那么我们只需要将插件的dexList添加到其中,也就能够通过宿主的classLoader加载到插件的类了。很多安卓的热修复/补丁 方案也是基于这个原理来实现的。
这样也有弊端,比如当插件和宿主依赖了同一个库的不同版本时,可能会存在版本冲突导致异常。
当然,也可以重新实现一份插件的classLoader,实现多classLoader架构,使得插件apk总是使用插件的loader来加载。不过这种方式比较麻烦,需要向系统的packages中添加插件apk信息,而常规情况该信息的添加是由系统来处理的,因此需要hook很多系统操作。
shadow采用的方案也是重新实现一份插件的classloader,并将宿主的loader作为插件的父loader
java
//创建插件loader
PluginClassLoader(
apk.absolutePath, // dex文件地址
odexDir, // odex优化后路径
installedApk.libraryPath, //库文件搜索路径,通常为null
hostClassLoader, //宿主loader
hostParentClassLoader,
loadParameters.hostWhiteList // 宿主配置白名单
)
在插件的基类中,重写getClassloader修改为自定义实现的classloader
java
@Override
public ClassLoader getClassLoader() {
if (hostActivityDelegate != null) {
return hostActivityDelegate.getClassLoader();
} else {
return super.getClassLoader();
}
}
1.2资源加载:
处理完上面的问题后,我们已经可以成功在宿主中启动插件apk的actvity了,马上又会发现,展示插件内容时,插件引用的R资源文件会抛出 <math xmlns="http://www.w3.org/1998/Math/MathML"> N o t f o u n d \color{red}{Not found} </math>Not found的错误。这也很好理解,因为我们是用宿主来启动的插件,所以系统加载资源时,会用宿主的resource来进行查找。但是不建议将插件资源放到宿主中去,这样一来耦合过重,二来也会有资源冲突的问题。
一般方案是在插件中,重新定义resource,使得插件apk执行时总是用处理过的resource进行资源加载。反射的做法是调用AssetManager中的addAssetPath传入插件的apk路径
shadow这里采用了一种非反射的做法,即直接获取插件apk的resource,然后修改插件基类的getResource方法。
java
// 获取插件apk的resource
val archiveFilePath = File(this.getFileStreamPath("tool-debug.apk").path).path
// getPackageArchiveInfo 它用于从 APK 文件中检索包信息,而不需要安装该 APK。
val packageArchiveInfo = packageManager.getPackageArchiveInfo(
archiveFilePath,
PackageManager.GET_ACTIVITIES
or PackageManager.GET_META_DATA
or PackageManager.GET_SERVICES
or PackageManager.GET_PROVIDERS
or PackageManager.GET_SIGNATURES
)
mPluginResources = packageArchiveInfo?.let { it1 -> packageManager.getResourcesForApplication(it1.applicationInfo) }
2. 与系统的通信(以activity为例)
常规的我们启动一个activity,需要先实现该activity,然后在AndroidManifest.xml中注册相关信息,然后再通过intent来启动。(又可分显式和隐式)
整个启动流程很复杂,简单概括下就是 通过intent设置要启动的activity,然后由AMS找到该activity的相关信息,反射创建一个实例出来,然后再通过生命周期派发,再由WMS将该activity显示到屏幕界面上
如果没有注册,那么AMS则不会启动该activity。常见的是通过一些hook和代理技术,在通知AMS启动和创建时,用一个已经注册的代理activity替换非法activity,从而达到"狸猫换太子"的效果。
<math xmlns="http://www.w3.org/1998/Math/MathML"> 建议实际实现一次,可以对整个 a c t i v i t y 的启动流程有更深入的了解 . \color{red}{建议实际实现一次,可以对整个activity的启动流程有更深入的了解.} </math>建议实际实现一次,可以对整个activity的启动流程有更深入的了解.
同时这里还涉及到实际需要启动的插件Activity在另外的plugin.apk中,即1. 类加载中的相关问题,不再赘述。
这种方案也是主流的插件hook方案,不过缺点也显而易见,即使用反射无可避免的兼容性问题。
反观shadow在这里的实现,则更为巧妙。同样的也是使用 一个ProxyActivity来让系统启动,但无需图中两次替换操作,即减少两次反射hook,代理的ProxyActivity也真正的被启动起来了。
这时你肯定会想,那插件的activity呢?
当代理activity启动后,再从插件apk中找到入口activity,直接实例化后add到页面上展示。如果你试过直接new activity,会发现实际根本无法在业务层直接创建activity,activity需要由系统来创建,并完成 <math xmlns="http://www.w3.org/1998/Math/MathML"> 状态、资源、上下文等的初始化和分配。 \color{red}{状态、资源、上下文等的初始化和分配。} </math>状态、资源、上下文等的初始化和分配。然而,shadow通过修改插件编译生成的字节码的方式(gradle插件),将插件APK内的业务activity 都修改继承自shadow框架的ShadowActivity,不同于正常的ac,插件内的ac已经不再继承自安卓系统的ac,所以可以直接new出来展示。
自然,ShadowActivity也需要添加系统ac常用的生命周期方法,当我们操作插件ac时,实际上操作的是ShadowActivity,而相关操作又会被 ProxyActivity接收并发送给系统进行处理;当系统通知 ProxyActivity相关回调时, ProxyActivity又会将消息发送给ShadowActivity,进而再通知的插件内的activity。这是一种很好的委托模式。
基于activity梳理的插件加载运行流程基本就是这样。
3. 总结
概括一下,常见的插件通过hook AMS与handler,绕开系统检查,启动了一个非法的activity(未安装/注册),同时通过修改classloader和resource来实现插件类/资源的加载。而shadow与之的一个最主要的区别则是,通过修改字节码的方式,让插件内的activity不再继承安卓系统的activity,而真正成为一个可以被手动create的对象,再利用委托模式使其通过壳容器与系统、宿主进行交互。
--附
hook 启动插件activity时,需要将其替换为 已注册的代理activity
java
/**
* 使用代理的Activity替换需要启动的未注册的Activity
* 在启动startActivity和AMS检测启动的页面之间,将未注册的Activity替换为已注册的Activity
*/
public static void hookAMS() {
// Android10之前要适配
try {
//反射
Class<?> clazz = Class.forName("android.app.ActivityTaskManager");
Field singletonField = clazz.getDeclaredField("IActivityTaskManagerSingleton");
singletonField.setAccessible(true);
Object singleton = singletonField.get(null);
Class<?> singletonClass = Class.forName("android.util.Singleton");
Field mInstanceField = singletonClass.getDeclaredField("mInstance");
mInstanceField.setAccessible(true);
Method getMethod = singletonClass.getMethod("get");
Object mInstance = getMethod.invoke(singleton);
Class IActivityTaskManagerClass = Class.forName("android.app.IActivityTaskManager");
//动态代理
Object mInstanceProxy = Proxy.newProxyInstance(Thread.currentThread().getContextClassLoader(),
new Class[]{IActivityTaskManagerClass}, new InvocationHandler() {
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
if ("startActivity".equals(method.getName())) {
Log.i("Cath","拦截到调用startActivity方法");
// 获取 Intent 参数在 args 数组中的index值
for (int i = 0; i < args.length; i++) {
Log.i("Cath","第"+i+"个参数是:"+args[i]);
if (args[i] instanceof Intent) {
Intent intent = (Intent) args[i];
// 获取假的activity,在intent中的componentName对象中
ComponentName componentName = intent.getComponent();
Log.i("Cath","要跳转的目标Activity:"+componentName.getClassName());
// 获取代理的activity名称,这个ac是真的有注册过的
ComponentName proxyComponentName =intent.getParcelableExtra("proxyComponentName");
if (proxyComponentName != null) {
Log.i("Cath","拿来伪造的Activity:"+proxyComponentName.getClassName());
intent.putExtra("realComponentName", componentName); // 真正想打开的插件activity也要记录一下
intent.setComponent(proxyComponentName); // 把代理的activity信息设置进去
}
break;
}
}
}
// 原来流程
return method.invoke(mInstance, args);
}
});
// 用代理的对象替换系统的对象
mInstanceField.set(singleton, mInstanceProxy);
} catch (Exception e) {
e.printStackTrace();
}
}
hook AMS通知展示代理activity时,需要再次替换回插件的activity
java
/**
* 需要启动的未注册的Activity 替换回来 ProxyActivity
* 在AMS检测启动的Activity之后,Activity执行生命周期之前,将已注册页面替换成未注册的页面即可
*/
public static void hookHandler() {
try {
Class<?> clazz = Class.forName("android.app.ActivityThread");
Field activityThreadField = clazz.getDeclaredField("sCurrentActivityThread");
activityThreadField.setAccessible(true);
Object activityThread = activityThreadField.get(null);
Field mHField = clazz.getDeclaredField("mH");
mHField.setAccessible(true);
// 获取handler
final Handler mH = (Handler) mHField.get(activityThread);
Field mCallbackField = Handler.class.getDeclaredField("mCallback");
mCallbackField.setAccessible(true);
mCallbackField.set(mH, new Handler.Callback() {
@Override
public boolean handleMessage(Message msg) {
if (msg.what == 159) {
Log.i("Cath", "拦截在handleMessage中启动Activity的消息 ");
// msg.obj = ClientTransaction
try {
// 获取 List<ClientTransactionItem> mActivityCallbacks 对象
Field mActivityCallbacksField = msg.obj.getClass()
.getDeclaredField("mActivityCallbacks");
mActivityCallbacksField.setAccessible(true);
List mActivityCallbacks = (List) mActivityCallbacksField.get(msg.obj); //根据源码得知,这个msg.obj就是ActivityClientRecord实例
for (int i = 0; i < mActivityCallbacks.size(); i++) {
// 打印 mActivityCallbacks 的所有item:
//android.app.servertransaction.WindowVisibilityItem
//android.app.servertransaction.LaunchActivityItem
// 如果是 LaunchActivityItem,则获取该类中的 mIntent 值,即 proxyIntent
if (mActivityCallbacks.get(i).getClass().getName()
.equals("android.app.servertransaction.LaunchActivityItem")) {
Object launchActivityItem = mActivityCallbacks.get(i);
Field mIntentField = launchActivityItem.getClass()
.getDeclaredField("mIntent");
mIntentField.setAccessible(true);
Intent proxyIntent = (Intent) mIntentField.get(launchActivityItem);
// 获取启动插件的 Intent,并替换回来
ComponentName realComponentName = proxyIntent.getParcelableExtra("realComponentName");
// java.lang.ClassNotFoundException: Didn't find class "com.example.tool.MainActivity" on path: DexPathList
Log.i("Cath", "替换回插件的intent" + realComponentName);
if (realComponentName != null) {
proxyIntent.setComponent(realComponentName);
}
}
}
} catch (Exception e) {
e.printStackTrace();
Log.i("Cath", " 获取启动activity的int值 " + e.getMessage());
}
}
return false;
}
});
} catch (Exception e) {
e.printStackTrace();
Log.i("Cath", " 获取启动activity的int值 " + e.getMessage());
}
}
将插件的dex文件合并到宿主中
java
/**
* 插件中的Element融入到宿主中
*/
public static void pluginToAppAction(Activity activity, ClassLoader classLoader, ClassLoader parentClassLoader) throws Exception{
//1. 找到宿主中的dexElements数组
PathClassLoader pathClassLoader = (PathClassLoader) classLoader;
Class<?> baseDexClassLoaderClass = Class.forName("dalvik.system.BaseDexClassLoader");
Field pathListField = baseDexClassLoaderClass.getDeclaredField("pathList");
pathListField.setAccessible(true);
Object dexPathList = pathListField.get(pathClassLoader);
Field dexElementsField = dexPathList.getClass().getDeclaredField("dexElements");
dexElementsField.setAccessible(true);
Object appDexElements = dexElementsField.get(dexPathList);
//2. 找到插件中的dexElements数组
File pluginFile = new File(activity.getFileStreamPath("tool-debug.apk").getPath());
Log.d("Cath", activity.getFileStreamPath("tool-debug.apk").getPath());
if (!pluginFile.exists()) {
Log.i("Cath","插件包不存在");
return;
}
String pluginPath = pluginFile.getAbsolutePath();
File pluginDir = activity.getDir("pluginDir",MODE_PRIVATE);
DexClassLoader dexClassLoader = new DexClassLoader(pluginPath,pluginDir.getAbsolutePath(),null, parentClassLoader);
Class<?> pluginBaseDexClassLoaderClass = Class.forName("dalvik.system.BaseDexClassLoader");
Field pluginPathListField = pluginBaseDexClassLoaderClass.getDeclaredField("pathList");
pluginPathListField.setAccessible(true);
Object pluginDexPathList = pluginPathListField.get(dexClassLoader);
Field dexElementsFieldPlugin = pluginDexPathList.getClass().getDeclaredField("dexElements");
dexElementsFieldPlugin.setAccessible(true);
Object pluginDexElements = dexElementsFieldPlugin.get(pluginDexPathList);
//3. 把两个数组合并成一个新的数组 newElement
int appLength = Array.getLength(appDexElements);
int pluginLength = Array.getLength(pluginDexElements);
int sum = appLength+pluginLength;
//创建一个新的数组 两个参数,一个是类型 一个是长度
Object newDexElement = Array.newInstance(appDexElements.getClass().getComponentType(), sum);
//进行融合
for (int i = 0; i < sum; i++) {
if(i<appLength){
Array.set(newDexElement,i,Array.get(appDexElements,i));
}else {
Array.set(newDexElement,i,Array.get(pluginDexElements,i-appLength));
}
}
//4. 把新的数组设置回宿主中
dexElementsField.set(dexPathList,newDexElement);
//5. 处理布局文件
handlePluginLayout(activity);
}
private static void handlePluginLayout(Activity activity) throws Exception{
AssetManager assetManager = AssetManager.class.newInstance();
// 把插件的路径 给 AssetManager
File pluginFile = new File(activity.getFileStreamPath("tool-debug.apk").getPath());
if (!pluginFile.exists()) {
Log.i("Cath","插件包不存在");
return;
}
String pluginPath = pluginFile.getAbsolutePath();
//执行addAssetPath方法吧路径添加进去 assetManager才能加载资源
Method addAssetPathMethod = assetManager.getClass().getDeclaredMethod("addAssetPath",String.class);
addAssetPathMethod.setAccessible(true);
addAssetPathMethod.invoke(assetManager,pluginPath);
// //宿主的Resources
// Resources r = getResources();
// //创建新的resources用来加载插件中的资源
// resources = new Resources(assetManager,r.getDisplayMetrics(),r.getConfiguration());
}
替换宿主的classLoader,需要自定义一份寻找插件类的classLoader
java
public static boolean hookPackageClassLoader(Context context, ClassLoader appClassLoaderNew) {
try {
Field packageInfoField = Class.forName("android.app.ContextImpl").getDeclaredField("mPackageInfo");
packageInfoField.setAccessible(true);
Object loadedApkObject = packageInfoField.get(context);
Class LoadedApkClass = Class.forName("android.app.LoadedApk");
Method getClassLoaderMethod = LoadedApkClass.getDeclaredMethod("getClassLoader");
ClassLoader appClassLoaderOld = (ClassLoader) getClassLoaderMethod.invoke(loadedApkObject);
Field appClassLoaderField = LoadedApkClass.getDeclaredField("mClassLoader");
appClassLoaderField.setAccessible(true);
appClassLoaderField.set(loadedApkObject, appClassLoaderNew);
return true;
} catch (Throwable ignored) {
Log.d("Cath", "hookPackageClassLoader " + ignored.getMessage());
}
return false;
}