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插件化开发之DexClassLoader动态加载dexjar小Demo
Android知识体系梳理笔记三:动态代理模式---插件加载机制学习笔记
Android 插件化Hook 插件化框架 ( 创建插件应用 | 拷贝插件 APK | 初始化插件包 | 测试插件 DEX 字节码 )