您的位置:首页 > 移动开发 > Android开发

Android 插件化原理解析(6、中)

2016-08-19 10:04 344 查看
接上文

替换ClassLoader

获取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实例能被成功第创建;由于整个流程较为复杂,我们简单梳理一下:

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

H类在handleMessage的时候,调用了getPackageInfoNoCheck方法来获取待启动的组件信息。在这个方法中会优先查找mPackages中的缓存信息,而我们已经手动把插件信息添加进去;因此能够成功命中缓存,获取到独立存在的插件信息。

H类然后调用handleLaunchActivity最终转发到performLaunchActivity方法;这个方法使用从getPackageInfoNoCheck中拿到LoadedApk中的mClassLoader来加载Activity类,进而使用反射创建Activity实例;接着创建Application,Context等完成Activity组件的启动。

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

04-05 02:49:53.742 11759-11759/com.weishu.upf.hook_classloader E/AndroidRuntime﹕ FATAL EXCEPTION: main

Process: com.weishu.upf.hook_classloader, PID: 11759

java.lang.RuntimeException: Unable to start activity ComponentInfo{com.weishu.upf.ams_pms_hook.app/com.weishu.upf.ams_pms_hook.app.MainActivity}: java.lang.RuntimeException: Unable to instantiate application android.app.Application: java.lang.IllegalStateException: Unable to get package info for com.weishu.upf.ams_pms_hook.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);

}

}

木有办法,我们只有一行一行地查看到底是哪里抛出这个异常的了;所幸代码不多。(所以说,缩小异常范围是一件多么重要的事情!!!)

第一句 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层来回牵扯;这其中的来龙去脉要完全把握清楚还请读者亲自翻阅源码。另外,我在此 对DroidPlugin 作者献上我的膝盖~这其中的玄妙让人叹为观止!

上文给出的方案中,我们全盘接管了插件中类的加载过程,这是一种相对暴力的解决方案;能不能更温柔一点呢?通俗来说,我们可以选择改革,而不是革命——告诉系统ClassLoader一些必要信息,让它帮忙完成插件类的加载。

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

我们再次搬出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插件使用的类在哪里,就能帮助他完成加载!

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

上面说到,我们可以通过告诉宿主程序的ClassLoader插件使用的类,让宿主的ClasLoader完成对于插件类的加载;那么问题来了,我们如何获取到宿主的ClassLoader?宿主程序使用的ClasLoader默认情况下是全局唯一的吗?

答案是肯定的。

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

表示宿主的LoadedApk在Application类中有一个成员变量mLoadedApk,而这个变量是从ContextImpl中获取的;ContextImpl重写了getClassLoader方法,因此我们在Context环境中直接getClassLoader()获取到的就是宿主程序唯一的ClassLoader。
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: