【android每日一问】插件化原理解析(转载,原文链接找不到了)

937 阅读19分钟

Java的类加载

了解插件化之前,先来简单的了解呀一下java的类加载的过程。 它包括加载,验证,准备,解析,初始化五个阶段,对于开发者来说,可控性最强的就是加载阶段。

  • 加载阶段主要完成三件事:
  1. 根据一个类的全限定名(全限定名 = 包名 + 类型)来获取定义此类的二进制字节流。
  2. 将这个字节流所代表的静态存储结构转化为JVM方法区中的运行时数据结构。
  3. 在内存中生成一个代表这个类的java.lang.Class对象,作为方法区这个类的各种数据的访问入口。

根据一个类的全限定名(全限定名 = 包名 + 类型)来获取定义此类的二进制字节流这个过程被抽象出来就是Java的类加载器模块,也就是我们所说的ClassLoader API。

Android Framework提供了DexClassLoader这个类,简化了根据一个类的全限定名(全限定名 = 包名 + 类型)来获取定义此类的二进制字节流这个过程,我们只需要告诉DexClassLoader一个dex文件或者apk文件的路径就能完成类的加载。

综上所述,插件的原理也就是加载机制可以用一句话来概括:

将插件的dex或者apk文件告诉合适的DexClassLoader,借助它来完成插件类的加载。

实现插件化

首先我们直到了Android系统使用了ClassLoader机制来进行Activity等组件的加载,apk被安装后,APK文件的代码以及都被系统放到固定的目录(如:/data/app/package_name/base-1.apk),系统在进行类加载的时候,会自动区这一个或几个特定的路径来寻找这个类,但是系统并不知道存在于插件中的Activity信息,因此正常情况我们肯定无法加载插件中的类,也就没有办法创建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);

系统通过类名classname,使用ClassLoader对象cl把这个类加载进虚拟机,最后使用反射创建了这个Activity的实例。 所以我们现在要做的就是想办法干预这个cl(替换掉或者告诉它要加载的路径),接下来我们看看这个cl是怎么来的?

r.packageInfo.getClassLoader()这个方法得到的cl,r.packageInfo是一个LoadedApk类的对象。

LoadedApk

Local state maintained about a currently loaded .apk.

  • LoadedApk对象是APK文件在内存中的表示. Apk的相关信息,包括Apk的代码,资源,代码里的Activity,Service等组件的信息我们都可以通过此对象获取。

接下来我们看一下r.packageInfo是从哪里来的:

顺着performLaunchActivity上溯,辗转handleLaunchActivity回到了H类的LAUNCH_ACTIVITY消息,找到了r.packageInfo的来源:

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

继续看getPackageInfoNoCheck方法:

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

直接调用了getPackageInfo方法,跟进去看:

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

接下来我们分析一下上面这些代码:

  • 首先判断调用方和获取App信息的一方是不是同一个userId,如果是同一个User,那么可以共享缓存数据(包括代码数据和资源数据)。
  • 接下来尝试获取缓存,如果没有缓存,才通过LoadedApk的构造函数创建LoadedAPk对象,创建成功后如果是同一个user还会放入缓存中。

通过上述分析,我们只需要想办法拿到这份缓存数据,修改里面的ClassLoader,自己控制类加载的过程,这样加载插件中的Activity类的问题就解决了。这也是我们之前说的第一种方式。

方案一:Hook掉ClassLoader

通过分析得知,在获取LoadedApk的过程中使用了一份缓存数据,缓存在一个Map中,通过包名到LoadedApk的一个映射,正常情况下我们的插件肯定不会存在于这个对象里,但是如果我们手动把插件信息添加到里面呢?这样系统在查找缓存的时候,直接命中我们添加的ClassLoadeer,这样我们就直接接管了类加载的过程。

通过查找我们发现缓存对象mPackages存在于ActivityThread类中。

首先我们获取这个对象:

// 先获取到当前的ActivityThread对象
Class<?> activityThreadClass = Class.forName("android.app.ActivityThread");
Method currentActivityThreadMethod = activityThreadClass.getDeclaredMethod("currentActivityThread");
currentActivityThreadMethod.setAccessible(true);
Object currentActivityThread = currentActivityThreadMethod.invoke(null);

接着获取mPackages这个对象

// 获取到 mPackages 这个静态成员变量, 这里缓存了dex包的信息
Field mPackagesField = activityThreadClass.getDeclaredField("mPackages");
mPackagesField.setAccessible(true);
Map mPackages = (Map) mPackagesField.get(currentActivityThread);

拿到我们想要的Map,接下来就是把插件的信息插入map当中,也就是说我们要插入我们需要的包名以及一个LoadedApk对象。那么如何创建一个LoadedApk对象呢?

我们可以用与系统完全相同的方式来创建LoadedApk对象,系统创建是通过getPackageInfo来完成的,但是这个函数是个私有函数,不到万不得已我们尽量不去反射私有函数调用,避免产生很多兼容性问题。

发现有同名的public函数getPackageInfogetPackageInfoNoCheck,前者除了获取包的信息,还检查了包的一些组件,为了绕过这些检查,我们选择getPackageInfoNoCheck来获取LoadedApk对象。

构建插件LoadedApk对象

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

public final LoadedApk getPackageInfoNoCheck(ApplicationInfo ai,CompatibilityInfo compatInfo) {
  • 我们需要传递两个参数ApplicationInfo和CompatibilityInfo
  • CompatibilityInfo这个参数代表App的兼容性信息,比如taregtSDK版本等等,我们只需要取出app的信息,因此直接使用默认的兼容性即可,在CompatibilityInfo类中有一个共有字段DEFAULT_COMPATIBILITY_INFO代表默认兼容性信息。

接下来我们的目标就是获取ApplicationInfo对象。

构建插件ApplicationInfo对象

首先看看ApplicationInfo是什么?

nformation you can retrieve about a particular application. 
This corresponds to information collected from the AndroidManifest.xml’s <application> tag.

显而易见,这个类就是xml里面<application>标签下的信息,AndroidManifest.xml是一个标准的xml文件,因此我们完全可以自己使用parse来解析这些信息。

但是,系统是如何获取这些信息的呢?

在Framework中就有一个这样的parser,叫做PackageParser,所以我们可以通过系统的parser来解析AndroidManifest.xml从而得到ApplicationInfo的信息,但是这里需要注意的是,这个类的兼容性很差,Google几乎每个版本都会对这个类做修改,你需要写大量的兼容代码来保证。 如下(DroidPlugin做的兼容处理):

DroidPlugin.png

经查发现PackageParser有方法generateApplication可以成功的拿到ApplicationInfo。由于其是@hide的,因此我们需要通过反射调用。

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

反射代码如下:

Class<?> packageParserClass = Class.forName("android.content.pm.PackageParser");
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);

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

构建PackageParser.Package

generateApplicationInfo方法需要的第一个参数是PackageParser.Package;从名字上看这个类代表某个apk包的信息,我们看看文档怎么解释:

Representation of a full package parsed from APK files on disk. A package consists of a single base APK, and zero or more split 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;

构建PackageUserState

第三个参数是PackageUserState,代表不同用户中包的信息。由于Android是一个多任务多用户系统,因此不同的用户同一个包可能有不同的状态;这里我们只需要获取包的信息,因此直接使用默认的即可.

至此,generateApplicaionInfo的参数我们已经全部构造完成,直接调用此方法即可得到我们需要的applicationInfo对象;在返回之前我们需要做一点小小的修改:使用系统的这个方法解析得到的ApplicationInfo对象中并没有apk文件本身的信息,所以我们把解析的apk文件的路径设置一下(ClassLoader依赖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;

替换ClassLoader

获取LoadedApk信息

我们最终的目的是调用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,这里重新创建一个类是为了更有区分度;以后也可以通过修改这个类实现对于类加载的控制。

到这里,我们已经成功地把把插件的信息放入ActivityThread中,这样我们插件中的类能够成功地被加载;因此插件中的Activity实例能被成功创建。

总结如下:

  • 在ActivityThread接收到IApplication的 scheduleLaunchActivity远程调用之后,将消息转发给H。
  • H类在handleMessage的时候,调用了getPackageInfoNoCheck方法来获取待启动的组件信息。在这个方法中会优先查找mPackages中的缓存信息,而我们已经手动把插件信息添加进去;因此能够成功命中缓存,获取到独立存在的插件信息。
  • H类然后调用handleLaunchActivity最终转发到performLaunchActivity方法;这个方法使用从getPackageInfoNoCheck中拿到LoadedApk中的mClassLoader来加载Activity类,进而使用反射创建Activity实例;接着创建Application,Context等完成Activity组件的启动。

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

    java.lang.RuntimeException: Unable to start activity ComponentInfo{com.com.sohu.test.app/com.sohu.test.app.MainActivity}: 
    java.lang.RuntimeException: Unable to instantiate application android.app.Application: java.lang.IllegalStateException:
    Unable to get package info for com.sohu.test.app; is package not installed?

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

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

经过排查发现:

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,让系统觉得插件已经安装在系统上了.

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

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

上面我们是通过开头我们说的第一种方法实现,下面介绍另一种方案:

方案二:委托系统,让系统帮忙加载

再来看看ActivityThread中加载Activity类的代码:

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

我们知道 这个r.packageInfo中的r是通过getPackageInfoNoCheck获取到的;在上述方案中我们把插件apk手动添加进缓存,采用自己加载办法解决;如果我们不干预这个过程,导致无法命中mPackages中的缓存,会发生什么?

从上一种方案可以知道如果没有命中缓存的情况下,系统直接new了一个LoadedApk;注意这个构造函数的第二个参数aInfo,这是一个ApplicationInfo类型的对象。在方案一中我们为了获取独立插件的ApplicationInfo花了不少心思;那么如果不做任何处理这里传入的这个aInfo参数是什么?

追本溯源不难发现,这个aInfo是从我们的替身StubActivity中获取的,而StubActivity存在于宿主程序中,所以,这个aInfo对象代表的实际上就是宿主程序的Application信息。

接下来会使用new出来的这个LoadedApk的getClassLoader()方法获取到ClassLoader来对插件的类进行加载;而获取到的这个ClassLoader是宿主程序使用的ClassLoader,因此现在还无法加载插件的类;那么,我们能不能让宿主的ClasLoader获得加载插件类的能力呢?如果我们告诉宿主使用的ClassLoader插件使用的类在哪里,就能帮助他完成加载!

宿主的ClassLoader在哪里,是唯一的吗?

答案是肯定的!

因为在FrameWork中宿主程序也是使用LoadedApk表示的,如同Activity启动是加载Activity类一样,宿主中的类也都是通过LoadedApk的getClassLoader()方法得到的ClassLoader加载的;由类加载机制的双亲委派特性,只要有一个应用程序类由某一个ClassLoader加载,那么它引用到的别的类除非父加载器能加载,否则都是由这同一个加载器加载的(不遵循双亲委派模型的除外)。

表示宿主的LoadedApk在Application类中有一个成员变量mLoadedApk,而这个变量是从ContextImpl中获取的;ContextImpl重写了getClassLoader方法,因此我们在Context环境中直接getClassLoader()获取到的就是宿主程序唯一的ClassLoader。

LoadedApk的ClassLoader到底是什么?

不论是宿主程序还是插件程序都是通过LoadedApk的getClassLoader()方法返回的ClassLoader进行类加载的,返回的这个ClassLoader到底是个什么?这个方法源码如下:

public ClassLoader getClassLoader() {
    synchronized (this) {
        if (mClassLoader != null) {
            return mClassLoader;
        }

        if (mIncludeCode && !mPackageName.equals("android")) {
            // 略...
            mClassLoader = ApplicationLoaders.getDefault().getClassLoader(zip, lib,
                    mBaseClassLoader);

            StrictMode.setThreadPolicy(oldPolicy);
        } else {
            if (mBaseClassLoader == null) {
                mClassLoader = ClassLoader.getSystemClassLoader();
            } else {
                mClassLoader = mBaseClassLoader;
            }
        }
        return mClassLoader;
    }
}

可以看到,非android开头的包和android开头的包分别使用了两种不同的ClassLoader,我们只关心第一种;因此继续跟踪ApplicationLoaders类:

public ClassLoader getClassLoader(String zip, String libPath, ClassLoader parent)
{

    ClassLoader baseParent = ClassLoader.getSystemClassLoader().getParent();

    synchronized (mLoaders) {
        if (parent == null) {
            parent = baseParent;
        }

        if (parent == baseParent) {
            ClassLoader loader = mLoaders.get(zip);
            if (loader != null) {
                return loader;
            }

            Trace.traceBegin(Trace.TRACE_TAG_ACTIVITY_MANAGER, zip);
            PathClassLoader pathClassloader =
                new PathClassLoader(zip, libPath, parent);
            Trace.traceEnd(Trace.TRACE_TAG_ACTIVITY_MANAGER);

            mLoaders.put(zip, pathClassloader);
            return pathClassloader;
        }

        Trace.traceBegin(Trace.TRACE_TAG_ACTIVITY_MANAGER, zip);
        PathClassLoader pathClassloader = new PathClassLoader(zip, parent);
        Trace.traceEnd(Trace.TRACE_TAG_ACTIVITY_MANAGER);
        return pathClassloader;
    }
}

可以看到,应用程序使用的ClassLoader都是PathClassLoader类的实例。那么,这个PathClassLoader是什么呢?从Android SDK给出的源码只能看出这么多:

public class PathClassLoader extends BaseDexClassLoader {
    public PathClassLoader(String dexPath, ClassLoader parent) {
        super((String)null, (File)null, (String)null, (ClassLoader)null);
        throw new RuntimeException("Stub!");
    }

    public PathClassLoader(String dexPath, String libraryPath, ClassLoader parent) {
        super((String)null, (File)null, (String)null, (ClassLoader)null);
        throw new RuntimeException("Stub!");
    }
}

我们继续查看它的父类BaseDexClassLoader:我们查看findClass或者defineClass方法,BaseDexClassLoader的findClass方法如下:

protected Class<?> findClass(String name) throws ClassNotFoundException {
    List<Throwable> suppressedExceptions = new ArrayList<Throwable>();
    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;
}

可以看到,查找Class的任务通过pathList完成;这个pathList是一个DexPathList类的对象,它的findClass方法如下:

public Class findClass(String name, List<Throwable> 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;
}

这个DexPathList内部有一个叫做dexElements的数组,然后findClass的时候会遍历这个数组来查找Class;如果我们把插件的信息塞进这个数组里面,那么不就能够完成类的加载过程吗?

通过上述分析,我们知道,可以把插件的相关信息放入BaseDexClassLoader的表示dex文件的数组里面,这样宿主程序的ClassLoader在进行类加载,遍历这个数组的时候,会自动遍历到我们添加进去的插件信息,从而完成插件类的加载.

接下来,我们实现这个过程:

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加载插件类的任务,因此第二种方案也宣告完成.

总结如下:

  • 默认情况下performLacunchActivity会使用替身StubActivity的ApplicationInfo也就是宿主程序的CLassLoader加载所有的类;我们的思路是告诉宿主ClassLoader我们在哪,让其帮助完成类加载的过程。

  • 宿主程序的ClassLoader最终继承自BaseDexClassLoader,BaseDexClassLoader通过DexPathList进行类的查找过程;而这个查找通过遍历一个dexElements的数组完成;我们通过把插件dex添加进这个数组就让宿主ClasLoader获取了加载插件类的能力。

两种方案的对比

方案一中我们自定义了插件的ClassLoader,并且绕开了Framework的检测;利用ActivityThread对于LoadedApk的缓存机制,我们把携带这个自定义的ClassLoader的插件信息添加进mPackages中,进而完成了类的加载过程。

方案二中我们深入探究了系统使用ClassLoader findClass的过程,发现应用程序使用的非系统类都是通过同一个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的话实现非常麻烦,有可能需要重启进程。

目前开源的插件方案中,DroidPlugin采用的方案一,Small采用的方案二。