写点什么

插件化 & 热修复系列——ClassLoader 方案设计,开源至上

用户头像
Android架构
关注
发布于: 2 小时前

// 创建一个新数组


Object[] newDexElements = (Object[]) Array.newInstance(hostDexElements.getClass().getComponentType(),hostDexElements.length + pluginDexElements.length);


// 拷贝


System.arraycopy(hostDexElements, 0, newDexElements,0, hostDexElements.length);


System.arraycopy(pluginDexElements, 0,newDexElements,hostDexElements.length, pluginDexElements.length);


// 赋值


dexElementsField.set(hostPathList, newDexElements);

特点

此乃单 ClassLoader 方案,插件和宿主程序的类全部都通过宿主的 ClasLoader 加载,存在的短板为“插件之间或者插件与宿主之间使用的类库有相同的时候,那么就会加载乱序等问题”


方案 2:替换 PathClassloader 的 parent



谁用了这个方案?

微店、Instant-Run

知识基础

安装在手机里的 apk(宿主)的 ClassLoader 链路关系


1)代码:


ClassLoader classLoader = getClassLoader();


ClassLoader parentClassLoader = classLoader.getParent();


ClassLoader pParentClassLoader = parentClassLoader.getParent();


2)关系:


classLoader:dalvik.system.PathClassLoader


parentClassLoader:java.lang.BootClassLoader


pParentClassLoader:null


可以看出,当前的 classLoader 是 PathClassLoader,parent 的 ClassLoader 是 BootClassLoader,而 BootClassLoader 没有 parent 的 ClassLoader

实现思想

如何利用上面的宿主链路基础原理设计?


ClassLoader 的构造方法中有一个参数是 parent;


如果把 PathClassLoader 的 parent 替换成我们插件的 classLoader;


再把插件的 classLoader 的 parent 设置成 BootClassLoader;


加上父委托的机制,查找插件类的过程就变成:BootClassLoader->插件的 classLoader->PathClassLoader

代码实现

public static void loadApk(Context context, String apkPath) {


File dexFile = context.getDir("dex", Context.MODE_PRIVATE);


File apkFile = new File(apkPath);


//找到 PathClassLoader


ClassLoader classLoader = context.getClassLoader();


//构建插件的 ClassLoader


//PathClassLoader 的父亲 传递给 插件的 ClassLoader


//到这里,顺序为:BootClassLoader->插件的 classLoader


DexClassLoader dexClassLoader = new DexClassLoader(apkFile.getAbsolutePath(),dexFile.getAbsolutePath(), null,classLoader.getParent());


try {


//PathClassLoader 的父亲设置为 插件的 ClassLoader


//顺序为:BootClassLoader->插件的 classLoader->PathClassLoader


Field fieldClassLoader = ClassLoader.class.getDeclaredField("parent");


if (fieldClassLoader != null) {


fieldClassLoader.setAccessible(true);


fieldClassLoader.set(classLoader, dexClassLoader);


}


} catch (Exception e) {


e.printStackTrace();


}


}

特点

此乃单 ClassLoader 方案,插件和宿主程序的类全部都通过宿主的 ClasLoader 加载,存在的短板为“插件之间或者插件与宿主之间使用的类库有相同的时候,那么就会加载乱序等问题”


方案 3:利用 LoadedApk 的缓存机制



谁用了这个方案?

360 的 DroidPlugin

实现原理

java.lang.ClassLoader cl = r.packageInfo.getClassLoader();


activity = mInstrumentation.newActivity(cl, component.getClassName(), r.intent);


StrictMode.incrementExpectedActivityCount(activity.getClass());


r.intent.setExtrasClassLoader(cl);


上面代码做了两件事:


1)系统用 packageInfo.getClassLoader()来加载已安装 app 的 Activity


2)实例化的 Activity


其中 packageInfo 为 LoadedApk 类型,是 APK 文件在内存中的表示,Apk 文件的相关信息,诸如 Apk 文件的代码和资源,甚至代码里面的 Activity,Service 等组件的信息我们都可以通过此对象获取。


packageInfo 怎么生成的?通过阅读源码得出:


1)先在 ActivityThread 中的 mPackages 缓存(Map,key 为包名,value 为 LoadedApk)中获取


2)如果缓存没有,new LoadedApk 生成一个,然后放到缓存 mPackages 中


基于上面系统的原理,实现的关键点步骤:


1)构建插件 ApplicationInfo 信息


ApplicationInfo applicationInfo = (ApplicationInfo) generateApplicationInfoMethod.invoke(packageParser,packageObj, 0, defaultPackageUserState);


String apkPath = apkFile.getPath();


applicationInfo.sourceDir = apkPath;


applicationInfo.publicSourceDir = apkPath;


2)构建 CompatibilityInfo


Class<?> compatibilityInfoClass = Class.forName("android.content.res.CompatibilityInfo");


Field defaultCompatibilityInfoField = compatibilityInfoClass.getDeclaredField("DEFAULT_COMPATIBILITY_INFO");


defaultCompatibilityInfoField.setAccessible(true);


Object defaultCompatibilityInfo = defaultCompatibilityInfoField.get(null);


3)根据 ApplicationInfo 和 CompatibilityInfo,构建插件的 loadedApk


Class<?> activityThreadClass = Class.forName("android.app.ActivityThread");


Method getPackageInfoNoCheckMethod = activityThreadClass.getDeclaredMethod("getPackageInfoNoCheck", ApplicationInfo.class, compatibilityInfoClass);


Object loadedApk = getPackageInfoNoCheckMethod.invoke(currentActivityThread, applicationInfo, defaultCompatibilityInfo);


4)构建插件的 ClassLoader,然后把它替换到插件 loadedApk 的 ClassLoader 中


String odexPath = Utils.getPluginOptDexDir(applicationInfo.packageName).getPath();


String libDir = Utils.getPluginLibDir(applicationInfo.packageName).getPath();


ClassLoader classLoader = new DexClassLoader(apkFile.getPath(), odexPath, libDir, ClassLoader.getSystemClassLoader());


Field mClassLoaderField = loadedApk.getClass().getDeclaredField("mClassLoader");


mClassLoaderField.setAccessible(true);


mClassLoaderField.set(loadedApk, classLoader);


5)把插件 loadedApk 添加进 ActivityThread 的 mPackages 中


// 先获取到当前的 ActivityThread 对象


Class<?> activityThreadClass = Class.forName("android.app.ActivityThread");


Method currentActivityThreadMethod = activityThreadClass.getDeclaredMethod("currentActivityThread");


currentActivityThreadMethod.setAccessible(true);


Object currentActivityThread = currentActivityThreadMethod.invoke(null);


// 获取到 mPackages 这个静态成员变量, 这里缓存了 dex 包的信息


Field mPackagesField = activityThreadClass.getDeclaredField("mPackages");


mPackagesField.setAccessible(true);


Map mPackages = (Map) mPackagesField.get(currentActivityThread);


// 由于是弱引用, 因此我们必须在某个地方存一份, 不然容易被 GC; 那么就前功尽弃了.


sLoadedApk.put(applicationInfo.packageName, loadedApk);


WeakReference weakReference = new WeakReference(loadedApk);


mPackages.put(applicationInfo.packageName, weakReference);


6)绕过系统检查,让系统觉得插件已经安装在系统上了


private static void hookPackageManager() throws Exception {


《Android学习笔记总结+最新移动架构视频+大厂安卓面试真题+项目实战源码讲义》
浏览器打开:qq.cn.hn/FTe 免费领取
复制代码


// 这一步是因为 initializeJavaContextClassLoader 这个方法内部无意中检查了这个包是否在系统安装


// 如果没有安装, 直接抛出异常, 这里需要临时 Hook 掉 PMS, 绕过这个检查.


Class<?> activityThreadClass = Class.forName("android.app.ActivityThread");


Method currentActivityThreadMethod = activityThreadClass.getDeclaredMethod("currentActivityThread");


currentActivityThreadMethod.setAccessible(true);


Object currentActivityThread = currentActivityThreadMethod.invoke(null);


// 获取 ActivityThread 里面原始的 sPackageManager


Field 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 IPackageManagerHookHandler(sPackageManager));


// 1. 替换掉 ActivityThread 里面的 sPackageManager 字段


sPackageManagerField.set(currentActivityThread, proxy);


}

特点

1)自定义了插件的 ClassLoader,并且绕开了 Framework 的检测


2)Hook 的地方也有点多:不仅需要 Hook AMS 和 H,还需要 Hook ActivityThread 的 mPackages 和 PackageManager!


3)多 ClassLoader 构架,每一个插件都有一个自己的 ClassLoader,隔离性好,如果不同的插件使用了同一个库的不同版本,它们相安无事


4)真正完成代码的热加载!


插件需要升级,直接重新创建一个自定的 ClassLoader 加载新的插件,然后替换掉原来的版本即可(Java 中,不同 ClassLoader 加载的同一个类被认为是不同的类)


单 ClassLoader 的话实现非常麻烦,有可能需要重启进程。


方案 4:自定义 ClassLoader 逻辑



谁用了?

腾讯视频等事业群中的 Shadow 热修框架

实现原理

1)先了解下宿主(已经安装 App)的 ClassLoader 链路:


BootClassLoader -> PathClassLoader


2)插件可以加载宿主的类实现:


构建插件的 ClassLoader,名字为 ApkClassLoader,其中父加载器传的是宿主的 ClassLoader,代码片段为:


class ApkClassLoader extends DexClassLoader {


static final String TAG = "daviAndroid";


private ClassLoader mGrandParent;


private final String[] mInterfacePackageNames;


@Deprecated


ApkClassLoader(InstalledApk installedApk,


ClassLoader parent,parent = 宿主 ClassLoader


String[] mInterfacePackageNames,


int grandTimes) {


super(installedApk.apkFilePath, installedApk.oDexPath, installedApk.libraryPath, parent);


在这个流程下,插件查找的流程变为:


BootClassLoader -> PathClassLoader -> ApkClassLoader(其实就是双亲委托)


3)插件不需要加载宿主的类实现:

用户头像

Android架构

关注

还未添加个人签名 2021.10.31 加入

还未添加个人简介

评论

发布
暂无评论
插件化&热修复系列——ClassLoader方案设计,开源至上