------ 本文转载自 Android 插件化原理解析——插件加载机制
这一系列的文章实在是写的好!
从上述分析中我们得知,在获取 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 信息。
我们这一步的目的很明确,通过 getPackageInfoNoCheck 函数创建出我们需要的 LoadedApk 对象,以供接下来使用。
这个函数的签名如下:
- public final LoadedApk getPackageInfoNoCheck(ApplicationInfo ai,
- CompatibilityInfo compatInfo) {
因此,为了调用这个函数,我们需要构造两个参数。其一是 ApplicationInfo,其二是 CompatibilityInfo;
第二个参数顾名思义,代表这个 App 的兼容性信息,比如 targetSDK 版本等等,这里我们只需要提取出 app 的信息,
因此直接使用默认的兼容性即可;在 CompatibilityInfo 类里面有一个公有字段
DEFAULT_COMPATIBILITY_INFO 代表默认兼容性信息;因此,我们的首要目标是获取这个 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);
要成功调用这个方法,还需要三个参数;因此接下来我们需要一步一步构建调用此函数的参数信息。
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;
第三个参数是 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;
方才为了获取 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 中进行的,这里有些蹊跷,我们仔细查看一下。
通过 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);
- }
来源: http://blog.csdn.net/u012439416/article/details/70768359