android 插件加载机制之一

来源:互联网 发布:洛阳市青峰网络 编辑:程序博客网 时间:2024/06/01 10:24

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

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

1, 概述

上文中我们地完成了『启动没有在AndroidManifest.xml中显式声明的Activity』的任务;

通过Hook AMS和拦截ActivityThread中H类对于组件调度我们成功地绕过了AndroidMAnifest.xml的限制。

但是我们启动的『没有在AndroidManifet.xml中显式声明』的Activity和宿主程序存在于同一个Apk中;

通常情况下,插件均以独立的文件存在甚至通过网络获取,这时候插件中的Activity能否成功启动呢?

要启动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);

也就是说,系统通过ClassLoader加载了需要的Activity类并通过反射调用构造函数创建出了Activity对象。

如果Activity组件存在于独立于宿主程序的文件之中,系统的ClassLoader怎么知道去哪里加载呢?

因此,如果不做额外的处理,插件中的Activity对象甚至都没有办法创建出来,谈何启动?

因此,要使存在于独立文件或者网络中的插件被成功启动,首先就需要解决这个插件类加载的问题。

本文将围绕此问题展开,完成『启动没有在AndroidManifest.xml中显示声明,并且存在于外部插件中的Activity』的任务。

阅读本文之前,可以先clone一份understand-plugin-framework,参考此项目的classloader-hook模块。本编文章的源码基于android 6.0.

2, ClassLoader机制

Java的ClassLoader机制:

Java虚拟机把描述类的数据从Class文件加载到内存,并对数据进行校检、转换解析和初始化的,

最终形成可以被虚拟机直接使用的Java类型,这就是虚拟机的类加载机制。

与那些在编译时进行链连接工作的语言不同,在Java语言里面,类型的加载、

连接和初始化都是在程序运行期间完成的,这种策略虽然会令类加载时稍微增加一些性能开销,

但是会为Java应用程序提供高度的灵活性,Java里天生可以同代拓展的语言特性就是依赖运行期动态加载和动态链接这个特点实现的。

例如,如果编写一个面相接口的应用程序,可以等到运行时在制定实际的实现类;

用户可以通过Java与定义的和自定义的类加载器,让一个本地的应用程序可以在运行时从

网络或其他地方加载一个二进制流作为代码的一部分,这种组装应用程序的方式目前已经广泛应用于Java程序之中。

从最基础的Applet,JSP到复杂的OSGi技术,都使用了Java语言运行期类加载的特性。

Java的类加载是一个相对复杂的过程;它包括加载、验证、准备、解析和初始化五个阶段;对于开发者来说,可控性最强的是加载阶段;加载阶段主要完成三件事:

1,根据一个类的全限定名来获取定义此类的二进制字节流

2,将这个字节流所代表的静态存储结构转化为JVM方法区中的运行时数据结构

3,在内存中生成一个代表这个类的java.lang.Class对象,作为方法区这个类的各种数据的访问入口。

『通过一个类的全限定名获取描述此类的二进制字节流』这个过程被抽象出来,就是Java的类加载器模块,也即JDK中ClassLoader API。

Android Framework提供了DexClassLoader这个类,简化了『通过一个类的全限定名获取描述次类的二进制字节流』这个过程;

我们只需要告诉DexClassLoader一个dex文件或者apk文件的路径就能完成类的加载。因此本文的内容用一句话就可以概括:

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

3. 思路分析

Android系统使用了ClassLoader机制来进行Activity等组件的加载;apk被安装之后,

APK文件的代码以及资源会被系统存放在固定的目录(比如/data/app/package_name/base.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);

这里可以很明显地看到,系统通过待启动的Activity的类名className,然后使用ClassLoader对象cl把这个类加载进虚拟机,

最后使用反射创建了这个Activity类的实例对象。要想干预这个ClassLoader(告知它我们的路径或者替换他),

我们首先得看看这玩意到底是个什么来头。(从哪里创建的)cl这个ClasssLoader对象通过r.packageInfo对象的

getClassLoader()方法得到,r.packageInfo是一个LoadedApk类的对象;那么,LoadedApk到底是个什么东西??

我们查阅LoadedApk类的文档,只有一句话,不过说的很明白:

Local state maintained about a currentlyloaded .apk.

LoadedApk对象是APK文件在内存中的表示。 Apk文件的相关信息,诸如Apk文件的代码和资源,

甚至代码里面的Activity,Service等组件的信息我们都可以通过此对象获取。

OK, 我们知道这个LoadedApk是何方神圣了;接下来我们要搞清楚的是:这个 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方法很简单,直接调用了getPackageInfo方法:

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对象;

创建成功之后,如果是同一个uid还放入了缓存。

提到缓存数据,看过Hook机制之Binder Hook的童鞋可能就知道了,我们之前成功借助

ServiceManager的本地代理使用缓存的机制Hook了各种Binder;因此这里完全可以如法炮制——

我们拿到这一份缓存数据,修改里面的ClassLoader;自己控制类加载的过程,

这样加载插件中的Activity类的问题就解决了。这就引出了我们加载插件类的第二种方案:

4, 委托系统加载

再次搬出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中的缓存,会发生什么?

查阅 getPackageInfo方法如下:

private LoadedApk getPackageInfo(ApplicationInfo aInfo, CompatibilityInfo compatInfo,        ClassLoader baseLoader, boolean securityViolation, boolean includeCode,        boolean registerPackage) {    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())) {            packageInfo =                new LoadedApk(this, aInfo, compatInfo, baseLoader,                        securityViolation, includeCode &&                        (aInfo.flags&ApplicationInfo.FLAG_HAS_CODE) != 0, registerPackage);            // 略    }}

可以看到,没有命中缓存的情况下,系统直接new了一个LoadedApk;

注意这个构造函数的第二个参数aInfo,这是一个ApplicationInfo类型的对象。

在『激进方案』中我们为了获取独立插件的ApplicationInfo花了不少心思;

那么如果不做任何处理这里传入的这个aInfo参数是什么?

追本溯源不难发现,这个aInfo是从我们的替身StubActivity中获取的!

而StubActivity存在于宿主程序中,所以,这个aInfo对象代表的实际上就是宿主程序的Application信息!

我们知道,接下来会使用new出来的这个LoadedApk的getClassLoader()方法获取到ClassLoader来对插件的类进行加载;

而获取到的这个ClassLoader是宿主程序使用的ClassLoader,因此现在还无法加载插件的类;

那么,我们能不能让宿主的ClasLoader获得加载插件类的能力呢?;

如果我们告诉宿主使用的ClassLoader插件使用的类在哪里,就能帮助他完成加载!

4.1宿主的ClassLoader

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

上面说到,我们可以通过告诉宿主程序的ClassLoader插件使用的类,让宿主的ClasLoader完成对于插件类的加载;

那么问题来了,我们如何获取到宿主的ClassLoader?宿主程序使用的ClasLoader默认情况下是全局唯一的吗?

答案是肯定的。

因为在FrameWork中宿主程序也是使用LoadedApk表示的,如同Activity启动是加载Activity类一样,

宿主中的类也都是通过LoadedApk的getClassLoader()方法得到的ClassLoader加载的;

由类加载机制的『双亲委派』特性,只要有一个应用程序类由某一个ClassLoader加载,

那么它引用到的别的类除非父加载器能加载,否则都是由这同一个加载器加载的(不遵循双亲委派模型的除外)。

表示宿主的LoadedApk在Application类中有一个成员变量mLoadedApk,而这个变量是从ContextImpl中获取的;

ContextImpl重写了getClassLoader方法,因此我们在Context环境中直接getClassLoader()获取到的就是宿主程序唯一的ClassLoader。

4.2 LoadedApk的ClassLoader

LoadedApk的ClassLoader到底是什么?

现在我们确保了『使用宿主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!");    }}

SDK没有导出这个类的源码,我们去androidxref上面看;发现其实这个类真的就这么多内容;

我们继续查看它的父类BaseDexClassLoader;ClassLoader嘛,

我们查看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;

如果我们把插件的信息塞进这个数组里面,那么不就能够完成类的加载过程吗?!!

4.3 给默认ClassLoader打补丁

通过上述分析,我们知道,可以把插件的相关信息放入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加载插件类』的任务;

因此第一种方案也宣告完成!我们简要总结一下这种方式的原理:

1,默认情况下performLacunchActivity会使用替身StubActivity的ApplicationInfo

也就是宿主程序的CLassLoader加载所有的类;我们的思路是告诉宿主ClassLoader我们在哪,让其帮助完成类加载的过程。

2,宿主程序的ClassLoader最终继承自BaseDexClassLoader,BaseDexClassLoader通过DexPathList进行类的查找过程;

而这个查找通过遍历一个dexElements的数组完成;我们通过把插件dex添加进这个数组就让宿主ClasLoader获取了加载插件类的能力。

0 0
原创粉丝点击