Android插件化原理探究
一、简介
android动态加载插件机制一直以来就是探索的热门领域,各种动态加载框架层出不穷,动态插件机制能有效解决一些线上bug进而避免频繁的版本发布。本文一不对当前流行的框架进行探讨(如果有需要人家已经开源),二不追求去实现这么一个完整的动态加载框架(这一般都是大厂所为,耗时耗力,而且如果真有机会去实现,熟知原理就会有方案可寻),只是总结下相关原理,这样不仅对动态加载有一定的认知,而且对理解Android系统大有裨益。
二、热修复与插件化
通常谈到插件化的时候紧密相连的就事热修复。这里对二者进行一些小小的区分。热修复一般是修复出现bug的类或者方法,该技术一般需要能做到替换原有类或者方法的目的;而插件化可以认为比热修复稍简单些,该技术一般只需要加载一个独立的插件化的apk即可,而不必进行原有类的替换,能满足动态添加的功能,这对一个超级app的开发有很大意义。
如果从java层面入手,二者本质上都会涉及到对类加载的操作、对资源加载的操作等。而本文就从java层面总结下插件化相关的一些技术原理,而不是native hook层次方面的原理(这个需要深入了解native层面下的东西)。
三、插件化要解决的问题
(1)代码的加载。
宿主app如何加载插件apk?因为class loader是全局唯一的,在宿主启动的时候就已经知道该加载宿主的apk了,然而插件的apk如何加载?
(2)组件生命周期的管理
对于普通的类加载,我们很容易借助于class loader完成,但是对于android中的activity等组件来说,这还远远满足不了需求,首先android拒绝加载没有在manifest文件注册的activity,而插件中apk的组件显然不会在宿主manifest文件中注册;其次类似于activity等组件的声明周期怎么维持?
(3)资源的加载
如何进行资源的加载?这里的资源是指插件apk中的资源。因为宿主显然对插件中的资源无感知,当我们应用R.xx.id进行资源查找时,必然会报错。
四、问题解决原理
1、代码的加载
熟悉java的一般都会知道代码的加载最终是由ClassLoader进行加载的,java中的classloader采用的是双亲委派模型,即首先会委托给父加载器进行加载,父加载器加载不了再自己去加载。java类加载器的这个逻辑有助于我们实现对插件apk的加载。
在android中加载类的class loader是DexClassLoader,因此首先我们要做到如何能加载插件apk(因为插件apk可能存在任何地方,宿主ClassLoader显然不会知道他在那里,加载就更无从谈起了)。
(1)自定义ClassLoader
这个原理就是要替换掉宿主的ClassLoader,采用我们自己的ClassLoader,进而实现加载我们插件apk的目的。这里想要替换一般会hook掉系统中的缓存的ClassLoader,进而截断这个过程。这样我们自己的classloader就可以实现针对插件apk进行加载。不过这种方法涉及到大量的对系统代码的反射操作,因为想要构建出可用的classloader,必须要确保合理的入参。所以难度实现起来较大。对于刚刚提到的hook点,这里可以给出一段代码:
public final LoadedApk getPackageInfo(String packageName, CompatibilityInfo compatInfo, int flags, int userId) {
final boolean differentUser = (UserHandle.myUserId() != userId);
synchronized (mResourcesManager) {
WeakReference ref;
if (differentUser) {
// Caching not supported across users
ref =null;
}else if ((flags & Context.CONTEXT_INCLUDE_CODE) != 0) {
ref = mPackages.get(packageName);
}else {
ref = mResourcePackages.get(packageName);
}
LoadedApk packageInfo = ref !=null ? ref.get() : null;
//Slog.i(TAG, "getPackageInfo " + packageName + ": " + packageInfo);
//if (packageInfo != null) Slog.i(TAG, "isUptoDate " + packageInfo.mResDir
// + ": " + packageInfo.mResources.getAssets().isUpToDate());
if (packageInfo != null && (packageInfo.mResources == null
|| packageInfo.mResources.getAssets().isUpToDate())) {
if (packageInfo.isSecurityViolation()
&& (flags&Context.CONTEXT_IGNORE_SECURITY) ==0) {
throw new SecurityException(
"Requesting code from " + packageName
+" to be run in process " + mBoundApplication.processName
+"/" + mBoundApplication.appInfo.uid); }
return packageInfo; } }
ApplicationInfo ai =null;
try {
ai = getPackageManager().getApplicationInfo(packageName,
PackageManager.GET_SHARED_LIBRARY_FILES, userId);
}catch (RemoteException e) {
// Ignore
}
if (ai != null) {
return getPackageInfo(ai, compatInfo, flags); }
return null; }
上述代码实质上是获取缓存的package info,返回的对象类型是LoadApk,LoadApk中包含了package信息,而package信息中就包含了classloader信息。具体可以自行查看。
(2)委托加载
这中方案的原理不在追求替换掉系统的classloader,而是选择了一个相对妥协的方案,即依然运行系统的classloader去进行类加载,那么此时又如何做到加载我们插件apk的呢?
这个就又涉及到android系统类加载的过程了,上文提到android提供了DexClassLoader进行类加载,实际上他继承于BaseDexClassLoader,在其findClass(类加载器建议通过该方法查找class)中有一端下面代码(源码地址参见:http://androidxref.com/6.0.1_r10/xref/libcore/dalvik/src/main/java/dalvik/system/BaseDexClassLoader.java):
Override
protected Class findClass(String name) throws ClassNotFoundException {
List suppressedExceptions =new ArrayList();
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;
}
从代码中可以看出,这里主要通过pathList完成了类的查找。pathList类型是DexPathList,查找代码(http://androidxref.com/6.0.1_r10/xref/libcore/dalvik/src/main/java/dalvik/system/DexPathList.java),实现如下:
public Class findClass(String name, List 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; }
从代码中可以看出,这里findClass会遍历dexElements数组完成最终的加载,对!这就是这种方案的突破口!我们只需要将我们插件的dex插入到这个数组中就可以实现对我们插件apk的加载(通过反射去构建dexElements,将宿主和插件apk的dex一起copy进去即可)!
至此,上面两种方式可以实现对插件apk的加载。
(3)生命周期的管理
对于普通的类加载,我们很容易借助于class loader完成,但是对于android中的activity等组件来说,这还远远满足不了需求,因为android拒绝加载没有在manifest文件注册的activity,而插件中apk的组件显然不会再宿主manifest文件中注册,因此这是首要面临的问题。下面给出两种流行的方案。
4、生命周期的管理
对于生命周期的管理,目前我得知有两种方案
(1)hook机制。
类似于解决classloader加载class类一样,hook住关键点,进而达到欺骗Framework的目的。这里首先会在manifest中注册代理activity(此处暂时称为ProxyActivity),在跳转的时候跳转的指向的是ProxyActivity,而在真正launch的时候launch的是真正的目标Actitity(此处暂时称为TargetActivity)。话是这么说,但是如何做到无缝替换?其实这种方案就是从系统源码入手,在startActivity开始,到进入system_server进程ActivityManagerService之前保持目标activity指向ProxyActivity,以解决activity在manifet中未注册问题;在从ActivityManagerService进程回到当前启动进程时用TargetActivity替换掉ProxyActivity已达到欺骗AMS的目的,具体可查阅相关资料。
这种做法可以使activity有完整的生命周期,因为都是有AMS再进行管理。
(2)代理机制
这种做法在ProxyActivity的注册上与上述方法一致,但在声明周期的管理上则是有动态加载框架实现了一系列代理目标组件的生命周期接口来完成的。当启动目标Activity的时候,实际上是有代理Activity完全代理了其声明周期。
3、资源的加载实现原理(取自DL框架对于资源加载原理描述)
加载的方法是通过反射,通过调用AssetManager中的addAssetPath方法,我们可以将一个apk中的资源加载到Resources中,由于addAssetPath是隐藏api我们无法直接调用,所以只能通过反射,其入参传递的路径可以是zip文件也可以是一个资源目录,而apk就是一个zip,所以直接将apk的路径传给它,资源就加载到AssetManager中了,然后再通过AssetManager来创建一个新的Resources对象,这个对象就是我们可以使用的apk中的资源了,这样我们的问题就解决了。