插件化 & 热修复系列——ClassLoader 方案设计,开源至上
// 创建一个新数组
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 加载,存在的短板为“插件之间或者插件与宿主之间使用的类库有相同的时候,那么就会加载乱序等问题”
谁用了这个方案?
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 {
// 这一步是因为 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 的话实现非常麻烦,有可能需要重启进程。
谁用了?
腾讯视频等事业群中的 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)插件不需要加载宿主的类实现:
评论