android 插件加载机制之二

Posted Achillisjack

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了android 插件加载机制之二相关的知识,希望对你有一定的参考价值。

------本文转载自 Android插件化原理解析——插件加载机制 

    这一系列的文章实在是写的好!

5 Hook ClassLoader

从上述分析中我们得知,在获取LoadedApk的过程中使用了一份缓存数据;

这个缓存数据是一个Map,从包名到LoadedApk的一个映射。正常情况下,我们的插件肯定不会存在于这个对象里面;

但是如果我们手动把我们插件的信息添加到里面呢?系统在查找缓存的过程中,会直接命中缓存!

进而使用我们添加进去的LoadedApk的ClassLoader来加载这个特定的Activity类!这样我们就能接管我们自己插件类的加载过程了!

这个缓存对象mPackages存在于ActivityThread类中;老方法,我们首先获取这个对象:

// 先获取到当前的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);

拿到这个Map之后接下来怎么办呢?我们需要填充这个map,把插件的信息塞进这个map里面,

以便系统在查找的时候能命中缓存。但是这个填充这个Map我们出了需要包名之外,

还需要一个LoadedApk对象;如何创建一个LoadedApk对象呢?

我们当然可以直接反射调用它的构造函数直接创建出需要的对象,但是万一哪里有疏漏,构造参数填错了怎么办?

又或者Android的不同版本使用了不同的参数,导致我们创建出来的对象与系统创建出的对象不一致,无法work怎么办?

因此我们需要使用与系统完全相同的方式创建LoadedApk对象;从上文分析得知,

系统创建LoadedApk对象是通过getPackageInfo来完成的,因此我们可以调用这个函数来创建LoadedApk对象;

但是这个函数是private的,我们无法使用。

有的童鞋可能会有疑问了,private不是也能反射到吗?我们确实能够调用这个函数,

但是private表明这个函数是内部实现,或许那一天Google高兴,把这个函数改个名字我们就直接GG了;

但是public函数不同,public被导出的函数你无法保证是否有别人调用它,因此大部分情况下不会修改;

我们最好调用public函数来保证尽可能少的遇到兼容性问题。

(当然,如果实在木有路可以考虑调用私有方法,自己处理兼容性问题,这个我们以后也会遇到)

间接调用getPackageInfo这个私有函数的public函数有同名的getPackageInfo系列和getPackageInfoNoCheck;

简单查看源代码发现,getPackageInfo除了获取包的信息,还检查了包的一些组件;

为了绕过这些验证,我们选择使用getPackageInfoNoCheck获取LoadedApk信息。

5.1 构建插件LoadedApk对象

我们这一步的目的很明确,通过getPackageInfoNoCheck函数创建出我们需要的LoadedApk对象,以供接下来使用。

这个函数的签名如下:

public final LoadedApk getPackageInfoNoCheck(ApplicationInfo ai,
            CompatibilityInfo compatInfo) {

因此,为了调用这个函数,我们需要构造两个参数。其一是ApplicationInfo,其二是CompatibilityInfo;

第二个参数顾名思义,代表这个App的兼容性信息,比如targetSDK版本等等,这里我们只需要提取出app的信息,

因此直接使用默认的兼容性即可;在CompatibilityInfo类里面有一个公有字段

DEFAULT_COMPATIBILITY_INFO代表默认兼容性信息;因此,我们的首要目标是获取这个ApplicationInfo信息。

5.2 构建插件ApplicationInfo信息

我们首先看看ApplicationInfo代表什么,这个类的文档说的很清楚:

Information you can retrieve about aparticular application. This corresponds to information collected from theAndroidManifest.xml’s <application> tag.

也就是说,这个类就是AndroidManifest.xml里面的这个标签下面的信息;

这个AndroidManifest.xml无疑是一个标准的xml文件,因此我们完全可以自己使用parse来解析这个信息。

那么,系统是如何获取这个信息的呢?其实Framework就有一个这样的parser,也即PackageParser;

理论上,我们也可以借用系统的parser来解析AndroidMAnifest.xml从而得到ApplicationInfo的信息。

但遗憾的是,这个类的兼容性很差;Google几乎在每一个Android版本都对这个类动刀子,

如果坚持使用系统的解析方式,必须写一系列兼容行代码!!DroidPlugin就选择了这种方式。

我们决定使用PackageParser类来提取ApplicationInfo信息, 看起来有我们需要的方法 generateApplication;

确实如此,依靠这个方法我们可以成功地拿到ApplicationInfo。

由于PackageParser是@hide的,因此我们需要通过反射进行调用。我们根据这个generateApplicationInfo方法的签名:

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

可以写出调用generateApplicationInfo的反射代码:

Class<?> packageParserClass = Class.forName("android.content.pm.PackageParser");
// 首先拿到我们得终极目标: generateApplicationInfo方法
// API 23 !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
// public static ApplicationInfo generateApplicationInfo(Package p, int flags,
//    PackageUserState state) {
// 其他Android版本不保证也是如此.
Class<?> packageParser$PackageClass = Class.forName("android.content.pm.PackageParser$Package");
Class<?> packageUserStateClass = Class.forName("android.content.pm.PackageUserState");
Method generateApplicationInfoMethod = packageParserClass.getDeclaredMethod("generateApplicationInfo",
        packageParser$PackageClass,int.class, packageUserStateClass);

要成功调用这个方法,还需要三个参数;因此接下来我们需要一步一步构建调用此函数的参数信息。

5.3 构建PackageParser.Package

generateApplicationInfo方法需要的第一个参数是PackageParser.Package;

从名字上看这个类代表某个apk包的信息,我们看看文档怎么解释:

Representation of a full package parsed fromAPK files on disk. A package consists of a single base APK, and zero or moresplit APKs.

果然,这个类代表从PackageParser中解析得到的某个apk包的信息,是磁盘上apk文件在内存中的数据结构表示;

因此,要获取这个类,肯定需要解析整个apk文件。PackageParser中解析apk的核心方法是parsePackage,

这个方法返回的就是一个Package类型的实例,因此我们调用这个方法即可;使用反射代码如下:

// 首先, 我们得创建出一个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);

这样,我们就得到了generateApplicationInfo的第一个参数;第二个参数是解析包使用的flag,我们直接选择解析全部信息,也就是0;

5.4 构建PackageUserState

第三个参数是PackageUserState,代表不同用户中包的信息。由于Android是一个多任务多用户系统,

因此不同的用户同一个包可能有不同的状态;这里我们只需要获取包的信息,因此直接使用默认的即可;

至此,generateApplicaionInfo的参数我们已经全部构造完成,直接调用此方法即可得到我们需要的applicationInfo对象;

在返回之前我们需要做一点小小的修改:使用系统系统的这个方法解析得到的ApplicationInfo对象

中并没有apk文件本身的信息,所以我们把解析的apk文件的路径设置一下(ClassLoa der依赖dex文件以及apk的路径):

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

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

5.5 替换ClassLoader

5.5.1 获取LoadedApk信息

方才为了获取ApplicationInfo我们费了好大一番精力;回顾一下我们的初衷:

我们最终的目的是调用getPackageInfoNoCheck得到LoadedApk的信息,

并替换其中的mClassLoader然后把把添加到ActivityThread的mPackages缓存中;

从而达到我们使用自己的ClassLoader加载插件中的类的目的。

现在我们已经拿到了getPackageInfoNoCheck这个方法中至关重要的第一个参数applicationInfo;

上文提到第二个参数CompatibilityInfo代表设备兼容性信息,直接使用默认的值即可;

因此,两个参数都已经构造出来,我们可以调用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);

我们成功地构造出了LoadedAPK, 接下来我们需要替换其中的ClassLoader,然后把它添加进ActivityThread的mPackages中:

String odexPath = Utils.getPluginOptDexDir(applicationInfo.packageName).getPath();
String libDir = Utils.getPluginLibDir(applicationInfo.packageName).getPath();
ClassLoader classLoader = new CustomClassLoader(apkFile.getPath(), odexPath, libDir, ClassLoader.getSystemClassLoader());
Field mClassLoaderField = loadedApk.getClass().getDeclaredField("mClassLoader");
mClassLoaderField.setAccessible(true);
mClassLoaderField.set(loadedApk, classLoader);

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

WeakReference weakReference = new WeakReference(loadedApk);
mPackages.put(applicationInfo.packageName, weakReference);

我们的这个CustomClassLoader非常简单,直接继承了DexClassLoader,什么都没有做;

当然这里可以直接使用DexClassLoader,这里重新创建一个类是为了更有区分度;

以后也可以通过修改这个类实现对于类加载的控制:

public class CustomClassLoader extends DexClassLoader {

    public CustomClassLoader(String dexPath, String optimizedDirectory, String libraryPath, ClassLoader parent) {
        super(dexPath, optimizedDirectory, libraryPath, parent);
    }
}

到这里,我们已经成功地把把插件的信息放入ActivityThread中,这样我们插件中的类能够成功地被加载;

因此插件中的Activity实例能被成功第创建;由于整个流程较为复杂,我们简单梳理一下:

1,在ActivityThread接收到IApplication的scheduleLaunchActivity远程调用之后,将消息转发给H

2,H类在handleMessage的时候,调用了getPackageInfoNoCheck方法来获取待启动的组件信息。

在这个方法中会优先查找mPackages中的缓存信息,而我们已经手动把插件信息添加进去;

因此能够成功命中缓存,获取到独立存在的插件信息。

3,H类然后调用handleLaunchActivity最终转发到performLaunchActivity方法;

这个方法使用从getPackageInfoNoCheck中拿到LoadedApk中的mClassLoader来加载Activity类,

进而使用反射创建Activity实例;接着创建Application,Context等完成Activity组件的启动。

看起来好像已经天衣无缝万事大吉了;但是运行一下会出现一个异常。

错误提示说是无法实例化 Application,而Application的创建也是在performLaunchActivity中进行的,这里有些蹊跷,我们仔细查看一下。

5.5.2 绕过系统检查

通过ActivityThread的performLaunchActivity方法可以得知,Application通过LoadedApk的makeApplication方法创建,

我们查看这个方法,在源码中发现了上文异常抛出的位置:

try {
    java.lang.ClassLoader cl = getClassLoader();
    if (!mPackageName.equals("android")) {
        initializeJavaContextClassLoader();
    }
    ContextImpl appContext = ContextImpl.createAppContext(mActivityThread, this);
    app = mActivityThread.mInstrumentation.newApplication(
            cl, appClass, appContext);
    appContext.setOuterContext(app);
} catch (Exception e) {
    if (!mActivityThread.mInstrumentation.onException(app, e)) {
        throw new RuntimeException(
            "Unable to instantiate application " + appClass
            + ": " + e.toString(), e);
    }
}

木有办法,我们只有一行一行地查看到底是哪里抛出这个异常的了;

所幸代码不多。(所以说,缩小异常范围是一件多么重要的事情!!!)

第一句 getClassLoader() 没什么可疑的,虽然方法很长,但是它木有抛出任何异常

(当然,它调用的代码可能抛出异常,万一找不到只能进一步深搜了;所以我觉得这里应该使用受检异常)。

然后我们看第二句,如果包名不是android开头,那么调用了一个叫做initializeJavaContextClassLoader的方法;我们查阅这个方法:

private void initializeJavaContextClassLoader() {
    IPackageManager pm = ActivityThread.getPackageManager();
    android.content.pm.PackageInfo pi;
    try {
        pi = pm.getPackageInfo(mPackageName, 0, UserHandle.myUserId());
    } catch (RemoteException e) {
        throw new IllegalStateException("Unable to get package info for "
                + mPackageName + "; is system dying?", e);
    }
    if (pi == null) {
        throw new IllegalStateException("Unable to get package info for "
                + mPackageName + "; is package not installed?");
    }

    boolean sharedUserIdSet = (pi.sharedUserId != null);
    boolean processNameNotDefault =
        (pi.applicationInfo != null &&
         !mPackageName.equals(pi.applicationInfo.processName));
    boolean sharable = (sharedUserIdSet || processNameNotDefault);
    ClassLoader contextClassLoader =
        (sharable)
        ? new WarningContextClassLoader()
        : mClassLoader;
    Thread.currentThread().setContextClassLoader(contextClassLoader);
}

这里,我们找出了这个异常的来源:原来这里调用了getPackageInfo方法获取包的信息;

而我们的插件并没有安装在系统上,因此系统肯定认为插件没有安装,这个方法肯定返回null。

所以,我们还要欺骗一下PMS,让系统觉得插件已经安装在系统上了;

至于如何欺骗 PMS,Hook机制之AMS&PMS 有详细解释,这里直接给出代码,不赘述了:

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

OK到这里,我们已经能够成功地加载简单的独立的存在于外部文件系统中的apk了。

至此 关于DroidPlugin 对于Activity生命周期的管理已经完全讲解完毕了;

这是一种极其复杂的Activity管理方案,我们仅仅写一个用来理解的demo就Hook了相当多的东西,

在Framework层来回牵扯;这其中的来龙去脉要完全把握清楚还请读者亲自翻阅源码。

上文给出的方案中,我们全盘接管了插件中类的加载过程,这是一种相对暴力的解决方案。

6.小结

本文中我们采用两种方案成功完成了『启动没有在AndroidManifest.xml中显示声明,并且存在于外部插件中的Activity』的任务。

『激进方案』中我们自定义了插件的ClassLoader,并且绕开了Framework的检测;

利用ActivityThread对于LoadedApk的缓存机制,我们把携带这个自定义的ClassLoader的插件信息添加进mPackages中,进而完成了类的加载过程。

『保守方案』中我们深入探究了系统使用ClassLoaderfindClass的过程,

发现应用程序使用的非系统类都是通过同一个PathClassLoader加载的;

而这个类的最终父类BaseDexClassLoader通过DexPathList完成类的查找过程;我们hack了这个查找过程,从而完成了插件类的加载。

这两种方案孰优孰劣呢?

很显然,『激进方案』比较麻烦,从代码量和分析过程就可以看出来,这种机制异常复杂;

而且在解析apk的时候我们使用的PackageParser的兼容性非常差,我们不得不手动处理每一个版本的apk解析api;

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

『保守方案』则简单得多(虽然原理也不简单),不仅代码很少,而且Hook的地方也不多;

有一点正本清源的意思,从最最上层Hook住了整个类的加载过程。

但是,我们不能简单地说『保守方案』比『激进方案』好。从根本上说,这两种方案的差异在哪呢?

『激进方案』是多ClassLoader构架,每一个插件都有一个自己的ClassLoader,

因此类的隔离性非常好——如果不同的插件使用了同一个库的不同版本,它们相安无事!

『保守方案』是单ClassLoader方案,插件和宿主程序的类全部都通过宿主的ClasLoader加载,

虽然代码简单,但是鲁棒性很差;一旦插件之间甚至插件与宿主之间使用的类库有冲突,那么直接GG。

多ClassLoader还有一个优点:可以真正完成代码的热加载!如果插件需要升级,

直接重新创建一个自定的ClassLoader加载新的插件,然后替换掉原来的版本即可

(Java中,不同ClassLoader加载的同一个类被认为是不同的类);

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

在J2EE领域中广泛使用ClasLoader的地方均采用多ClassLoader架构,比如Tomcat服务器,

Java模块化事实标准的OSGi技术;所以,我们有足够的理由认为选择多ClassLoader架构在大多数情况下是明智之举。

目前开源的插件方案中,DroidPlugin采用的『激进方案』,Small采用的『保守方案』那么,有没有两种优点兼顾的方案呢??

答案自然是有的。

DroidPlugin和Small的共同点是两者都是非侵入式的插件框架;

什么是『非侵入式』呢?打个比方,你启动一个插件Activity,直接使用startActivity即可,

就跟开发普通的apk一样,开发插件和普通的程序对于开发者来说没有什么区别。

如果我们一定程度上放弃这种『侵入性』,那么我们就能实现一个两者优点兼而有之的插件框架!

OK,本文的内容就到这里了;关于『插件机制对于Activity的处理方式』也就此完结。

要说明的是,在本文的『保守方案』其实只处理了代码的加载过程,它并不能加载有资源的apk!

所以目前我这个实现基本没什么暖用;当然我这里只是就『代码加载』进行举例.

以上是关于android 插件加载机制之二的主要内容,如果未能解决你的问题,请参考以下文章

插件化框架解读之Android 资源加载机制详解

Android插件化开发之DexClassLoader动态加载dexjar小Demo

Android知识体系梳理笔记三:动态代理模式---插件加载机制学习笔记

热修复之类加载机制总结

Android 插件化Hook 插件化框架 ( 创建插件应用 | 拷贝插件 APK | 初始化插件包 | 测试插件 DEX 字节码 )

iOS之深入解析CocoaPods的插件机制和如何加载插件整合开发工具