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

2,450 阅读7分钟

引言

上一个阶段我们开始进入插件/热修的领域,了解了热修的前世今生,下面我们来学习下热修中的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)插件可以选择加载宿主的类和绕过宿主加载,选择性强

结尾

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