Hook机制学习(四) -插件加载机制

weishu_博客

一:Classloader加载的基本原理

基本原理:系统通过ClassLoader加载了需要的Activity类并通过反射调用构造函数创建出了Activity对象。

java.lang.ClassLoader cl = r.packageInfo.getClassLoader();
activity = mInstrumentation.newActivity(
        cl, component.getClassName(), r.intent);
StrictMode.incrementExpectedActivityCount(activity.getClass());
r.intent.setExtrasClassLoader(cl);

必要性: Android系统使用了PathClassLoader来进行Activity等组件的加载;apk被安装之后,APK文件的代码以及资源会被系统存放在固定的目录(比如/data/app/package_name )系统在进行类加载的时候,会自动去这一个或者几个特定的路径来寻找这个类;但是系统并不知道存在于插件中的Activity组件的信息因此正常情况下系统无法加载我们插件中的类。
LoakApk:LoadedApk对象是APK文件在内存中的表示。 Apk文件的相关信息,诸如Apk文件的代码和资源,甚至代码里面的Activity,Service等组件的信息我们都可以通过此对象获取。

二:两种加载方案

1. 构建插件对应的ClassLoader来加载插件

基本原理:
1 先通过反射调用getPackageInfoNoCheck生成LoadApk,在创建该LoadApk对应的ClassLoader的对象,ClassLoader的路径设置为插件的路径,在把该LoadApk保存早ActivityThread的mPackages里面。这样在创建插件组件(如Activity)时,使用的就是构建的插件对应的ClassLoader来加载插件组件。
2 getPackageInfoNoCheck需要三个参数,所以先需要反射出各个参数
r.packageInfo: 为LoadApk,所以要想创建插件对应的ClassLoader,首先要创建插件LoadApk。

java.lang.ClassLoader cl = r.packageInfo.getClassLoader();
activity = mInstrumentation.newActivity(
        cl, component.getClassName(), r.intent);
StrictMode.incrementExpectedActivityCount(activity.getClass());
r.intent.setExtrasClassLoader(cl);

** 原理:** LoadApk的缓存
r.packageInfo是通过getPackageInfoNoCheck方法获取的

final ActivityClientRecord r = (ActivityClientRecord) msg.obj;
r.packageInfo = getPackageInfoNoCheck(
        r.activityInfo.applicationInfo, r.compatInfo);
handleLaunchActivity(r, null);

getPackageInfoNoCheck简单的调用了getPackageInfo()

public final LoadedApk getPackageInfoNoCheck(ApplicationInfo ai,
        CompatibilityInfo compatInfo) {
    return getPackageInfo(ai, compatInfo, null, false, true, false);
}

getPackageInfo:使用mPackages进行LoadedApk缓存

private LoadedApk getPackageInfo(ApplicationInfo aInfo, CompatibilityInfo compatInfo,
        ClassLoader baseLoader, boolean securityViolation, boolean includeCode,
        boolean registerPackage) {
        // 获取userid信息
    final boolean differentUser = (UserHandle.myUserId() != UserHandle.getUserId(aInfo.uid));
    synchronized (mResourcesManager) {
    // 尝试获取缓存信息
        WeakReference ref;
        if (differentUser) {
            // Caching not supported across users
            ref = null;
        } else if (includeCode) {
            ref = mPackages.get(aInfo.packageName);
        } else {
            ref = mResourcePackages.get(aInfo.packageName);
        }

        LoadedApk packageInfo = ref != null ? ref.get() : null;
        if (packageInfo == null || (packageInfo.mResources != null
                && !packageInfo.mResources.getAssets().isUpToDate())) {
                // 缓存没有命中,直接new
            packageInfo =
                new LoadedApk(this, aInfo, compatInfo, baseLoader,
                        securityViolation, includeCode &&
                        (aInfo.flags&ApplicationInfo.FLAG_HAS_CODE) != 0, registerPackage);

        // 省略。。更新缓存
        return packageInfo;
    }
}

做法: 因为LoadApk使用mPackages进行缓存,所以可以通过反射 mPackages,然后把插件对应的LoadApk保存在mPackages

第一步:反射获取ActivityThead中的mPackages

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);

第二步:创建插件对应的LoadApk保存在mPackages
1 采取Hook getPackageInfoNoCheck而不是 getPackageInfo,因为public方法稳定性和兼容性更好。
2 getPackageInfoNoCheck需要准备 两个参数:ApplicationInfo aInfo, CompatibilityInfo compatInfo
第三步:准备ApplicationInfo信息:使用PackageParse来解析Androidmanifest文件中的ApplicationInfo信息。
1 通过generateApplicationInfo来获得Application;需要准备三个参数

public static ApplicationInfo generateApplicationInfo(Package p, int flags,
   PackageUserState state)

1.1 构建PackageParser.Package:这个类代表从PackageParser中解析得到的某个apk包的信息,是磁盘上apk文件在内存中的数据结构表示;因此,要获取这个类,肯定需要解析整个apk文件。
使用PackageParser.parsePackage()来解析。

// 首先, 我们得创建出一个Package对象出来供这个方法调用
// 而这个需要得对象可以通过 android.content.pm.PackageParser#parsePackage 这个方法返回得 Package对象得字段获取得到
// 创建出一个PackageParser对象供使用
Object packageParser = packageParserClass.newInstance();
// 调用 PackageParser.parsePackage 解析apk的信息
Method parsePackageMethod = packageParserClass.getDeclaredMethod("parsePackage", File.class, int.class);

// 实际上是一个 android.content.pm.PackageParser.Package 对象
Object packageObj = parsePackageMethod.invoke(packageParser, apkFile, 0);


1.2 int flags:参数是解析包使用的flag,直接选择解析全部信息,也就是0;
1.3构建PackageUserState:代表不同用户中包的信息。由于Android是一个多任务多用户系统,因此不同的用户同一个包可能有不同的状态;这里我们只需要获取包的信息,因此直接使用默认的即可;

/ 第三个参数 mDefaultPackageUserState 我们直接使用默认构造函数构造一个出来即可
Object defaultPackageUserState = packageUserStateClass.newInstance();

// 万事具备!!!!!!!!!!!!!!
ApplicationInfo applicationInfo = (ApplicationInfo) generateApplicationInfoMethod.invoke(packageParser,
        packageObj, 0, defaultPackageUserState);
String apkPath = apkFile.getPath();
applicationInfo.sourceDir = apkPath;
applicationInfo.publicSourceDir = apkPath;

第三步:替换ClassLoader
1 调用getPackageInfoNoCheck获取LoadedApk

// android.content.res.CompatibilityInfo
Class compatibilityInfoClass = Class.forName("android.content.res.CompatibilityInfo");
Method getPackageInfoNoCheckMethod = activityThreadClass.getDeclaredMethod("getPackageInfoNoCheck", ApplicationInfo.class, compatibilityInfoClass);

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

Object defaultCompatibilityInfo = defaultCompatibilityInfoField.get(null);
ApplicationInfo applicationInfo = generateApplicationInfo(apkFile);

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

2 替换LoadApk的ClassLoader,然后把它添加进ActivityThread的mPackages中。

2. 告诉宿主Classloader插件路径,使用宿主Classloader来加载

基本原理:
1 已安装的Apk使用的是PathClassLoader来加载data/package目录下类,PathClassLoader继承于BaseDexClassLoader,BaseDexClassLoader通过findClass()方案来加载一个类,findClass()调用了pathList.findClass()。
2 DexPathList:通过DexElements来加载
BaseDexClassLoader.findClass();

protected Class findClass(String name) throws ClassNotFoundException {
    List suppressedExceptions = new ArrayList();
    Class c = pathList.findClass(name, suppressedExceptions);
    if (c == null) {
        ClassNotFoundException cnfe = new ClassNotFoundException("Didn't find class \"" + name + "\" on path: " + pathList);
        for (Throwable t : suppressedExceptions) {
            cnfe.addSuppressed(t);
        }
        throw cnfe;
    }
    return c;
}

DexPathList.findClass

public Class findClass(String name, List suppressed) {
   for (Element element : dexElements) {
       DexFile dex = element.dexFile;

       if (dex != null) {
           Class clazz = dex.loadClassBinaryName(name, definingContext, suppressed);
           if (clazz != null) {
               return clazz;
           }
       }
   }
   if (dexElementsSuppressedExceptions != null) {
       suppressed.addAll(Arrays.asList(dexElementsSuppressedExceptions));
   }
   return null;
}

3 把插件的信息保存在dexElements里面:给

public static void patchClassLoader(ClassLoader cl, File apkFile, File optDexFile)
        throws IllegalAccessException, NoSuchMethodException, IOException, InvocationTargetException, InstantiationException, NoSuchFieldException {
    // 获取 BaseDexClassLoader : pathList
    Field pathListField = DexClassLoader.class.getSuperclass().getDeclaredField("pathList");
    pathListField.setAccessible(true);
    Object pathListObj = pathListField.get(cl);

    // 获取 PathList: Element[] dexElements
    Field dexElementArray = pathListObj.getClass().getDeclaredField("dexElements");
    dexElementArray.setAccessible(true);
    Object[] dexElements = (Object[]) dexElementArray.get(pathListObj);

    // Element 类型
    Class elementClass = dexElements.getClass().getComponentType();

    // 创建一个数组, 用来替换原始的数组
    Object[] newElements = (Object[]) Array.newInstance(elementClass, dexElements.length + 1);

    // 构造插件Element(File file, boolean isDirectory, File zip, DexFile dexFile) 这个构造函数
    Constructor constructor = elementClass.getConstructor(File.class, boolean.class, File.class, DexFile.class);
    Object o = constructor.newInstance(apkFile, false, apkFile, DexFile.loadDex(apkFile.getCanonicalPath(), optDexFile.getAbsolutePath(), 0));

    Object[] toAddElementArray = new Object[] { o };
    // 把原始的elements复制进去
    System.arraycopy(dexElements, 0, newElements, 0, dexElements.length);
    // 插件的那个element复制进去
    System.arraycopy(toAddElementArray, 0, newElements, dexElements.length, toAddElementArray.length);

    // 替换
    dexElementArray.set(pathListObj, newElements);

}

两种加载方案的比较

方案一:构建ClassLoader
优点:多ClassLoader机制,每个插件都有一个对应的ClassLoader,隔离性好,比如两个不同的插件使用两个库的不同版本,那么不会出现冲突情况。
缺点:兼容性差,实现过程复杂。
方案二: 补丁方案
优点:实现简单
缺点:单ClassLoader方案,不同的插件都用PathClassLoader加载,一旦插件之间甚至插件与宿主之间使用的类库有冲突,会出现类型冲突的后果。

你可能感兴趣的:(Hook机制学习(四) -插件加载机制)