注册

【插件&热修系列】ClassLoader方案设计

引言


上一个阶段我们开始进入插件/热修的领域,了解了热修的前世今生,下面我们来学习下热修中的ClassLoader方案设计;


ClassLoader主要是用来加载插件用的,在启动插件前首先要把插件加载进来,下面我们通过不同方案分析,了解加载的不同姿势~


方案1:合并Dex(hook方式)


谁用了这个方案?


QQ团队的空间换肤功能


原理


将我们插件dex和宿主apk的class.dex合并,都放到宿主dexElements数组中。App每次启动从该数组中加载。


实战流程


1)获取宿主,dexElements


2)获取插件,dexElements


3)合并两个dexElements


4)将新的dexElements 赋值到 宿主dexElements


代码


Class<?> clazz = Class.forName("dalvik.system.BaseDexClassLoader");
Field pathListField = clazz.getDeclaredField("pathList");
pathListField.setAccessible(true);

Class<?> dexPathListClass = Class.forName("dalvik.system.DexPathList");
Field dexElementsField = dexPathListClass.getDeclaredField("dexElements");
dexElementsField.setAccessible(true);

// 宿主的 类加载器
ClassLoader pathClassLoader = context.getClassLoader();
// DexPathList类的对象
Object hostPathList = pathListField.get(pathClassLoader);
// 宿主的 dexElements
Object[] hostDexElements = (Object[]) dexElementsField.get(hostPathList);

// 插件的 类加载器
ClassLoader dexClassLoader = new DexClassLoader(apkPath, context.getCacheDir().getAbsolutePath(),null, pathClassLoader);
// DexPathList类的对象
Object pluginPathList = pathListField.get(dexClassLoader);
// 插件的 dexElements
Object[] pluginDexElements = (Object[]) dexElementsField.get(pluginPathList);

// 宿主dexElements = 宿主dexElements + 插件dexElements
// 创建一个新数组
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 {
// 这一步是因为 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)插件不需要加载宿主的类实现:


class ApkClassLoader extends DexClassLoader {

............
//1)系统里面找
Class<?> clazz = findLoadedClass(className);
if (clazz == null) {
//2)从自己的dexPath中查找
clazz = findClass(className);
if (clazz == null) {
//3)从parent的parent找(BootClassLoader)ClassLoader中查找。
clazz = mGrandParent.loadClass(className);
}
}

............
}

这个逻辑插件不需要加载宿主的类,所以加载逻辑中不会去加载宿主的类(也就是会经过PathClassLoader),这种情况下,即使插件和宿主用到了同一个类,那么插件加载的时候不会因为委托加载机制而去加载了宿主的,导致插件的加载错了;


代码实现


class ApkClassLoader extends DexClassLoader {
private ClassLoader mGrandParent;
private final String[] mInterfacePackageNames;

ApkClassLoader(InstalledApk installedApk,
ClassLoader parent, String[] mInterfacePackageNames, int grandTimes) {
super(installedApk.apkFilePath, installedApk.oDexPath, installedApk.libraryPath, parent);
ClassLoader grand = parent;
for (int i = 0; i < grandTimes; i++) {
grand = grand.getParent();
}
mGrandParent = grand;
this.mInterfacePackageNames = mInterfacePackageNames;
}

@Override
protected Class<?> loadClass(String className, boolean resolve) throws ClassNotFoundException {
String packageName;
int dot = className.lastIndexOf('.');
if (dot != -1) {
packageName = className.substring(0, dot);
} else {
packageName = "";
}

boolean isInterface = false;
for (String interfacePackageName : mInterfacePackageNames) {
if (packageName.equals(interfacePackageName)) {
isInterface = true;
break;
}
}

if (isInterface) {
return super.loadClass(className, resolve);
} else {
Class<?> clazz = findLoadedClass(className);

if (clazz == null) {
ClassNotFoundException suppressed = null;
try {
clazz = findClass(className);
} catch (ClassNotFoundException e) {
suppressed = e;
}

if (clazz == null) {
try {
clazz = mGrandParent.loadClass(className);
} catch (ClassNotFoundException e) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) {
e.addSuppressed(suppressed);
}
throw e;
}
}
}

return clazz;
}
}

/**
* 从apk中读取接口的实现
*
* @param clazz 接口类
* @param className 实现类的类名
* @param <T> 接口类型
* @return 所需接口
* @throws Exception
*/
<T> T getInterface(Class<T> clazz, String className) throws Exception {
try {
Class<?> interfaceImplementClass = loadClass(className);
Object interfaceImplement = interfaceImplementClass.newInstance();
return clazz.cast(interfaceImplement);
} catch (ClassNotFoundException | InstantiationException
| ClassCastException | IllegalAccessException e) {
throw new Exception(e);
}
}

}

该代码实现不正常的双亲委派逻辑,既能和parent隔离类加载(和宿主),也能通过白名单复用一些宿主的类


特点


1)属于多ClassLoader方案


2)插件可以选择加载宿主的类和绕过宿主加载,选择性强


结尾


哈哈,该篇就写到这里(一起体系化学习,一起成长)


0 个评论

要回复文章请先登录注册