Android插件化原理

插件化技术,实际上要解决的就是两个问题:
1、代码加载
2、资源加载

代码加载

类的加载可以使用Java的ClassLoader机制,但是对于Android来说,并不是说类加载进来就可以用了,很多组件都是有“生命”的;因此对于这些有血有肉的类,必须给它们注入活力,也就是所谓的组件生命周期管理

另外,如何管理加载进来的类也是一个问题。假设多个插件依赖了相同的类,是抽取公共依赖进行管理还是插件单独依赖?这就是ClassLoader的管理问题。

ClassLoader详细可参考:类加载器ClassLoader及Dex/Class

Context的处理

通常我们通过Context对象访问资源,光创建出Resource对象还不够,因此还需要一些额外的工作。 对资源访问的不同实现方式也需要不同的额外工作。以VirtualAPK的处理方式为例:

第一步:创建Resource

1
2
3
4
5
6
7
8
9
10
11
if (Constants.COMBINE_RESOURCES) {
//插件和主工程资源合并时需要hook住主工程的资源
Resources resources = ResourcesManager.createResources(context, apk.getAbsolutePath());
ResourcesManager.hookResources(context, resources);
return resources;
} else {
//插件资源独立,该resource只能访问插件自己的资源
Resources hostResources = context.getResources();
AssetManager assetManager = createAssetManager(context, apk);
return new Resources(assetManager, hostResources.getDisplayMetrics(), hostResources.getConfiguration());
}

第二步:hook主工程的Resource
对于合并式的资源访问方式,需要替换主工程的Resource,下面是具体替换的代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
public static void hookResources(Context base, Resources resources) { 
try {
//替换了主工程context中LoadedApk的mResource对象。
ReflectUtil.setField(base.getClass(), base, "mResources", resources);
Object loadedApk = ReflectUtil.getPackageInfo(base);
ReflectUtil.setField(loadedApk.getClass(), loadedApk, "mResources", resources);

//将新的Resource添加到主工程ActivityThread的mResourceManager中
Object activityThread = ReflectUtil.getActivityThread(base);
Object resManager = ReflectUtil.getField(activityThread.getClass(), activityThread, "mResourcesManager");
if (Build.VERSION.SDK_INT < 24) {
Map<Object, WeakReference<Resources>> map = (Map<Object, WeakReference<Resources>>) ReflectUtil.getField(resManager.getClass(), resManager, "mActiveResources");
Object key = map.keySet().iterator().next();
map.put(key, new WeakReference<>(resources));
} else {
// still hook Android N Resources, even though it's unnecessary, then nobody will be strange.
Map map = (Map) ReflectUtil.getFieldNoException(resManager.getClass(), resManager, "mResourceImpls");
Object key = map.keySet().iterator().next();
Object resourcesImpl = ReflectUtil.getFieldNoException(Resources.class, resources, "mResourcesImpl");
map.put(key, new WeakReference<>(resourcesImpl));
}
} catch (Exception e) {
e.printStackTrace();
}
}

注意下上述代码hook了几个地方,包括以下几个hook点:
1.替换了主工程context中LoadedApk的mResource对象。
2.将新的Resource添加到主工程ActivityThread的mResourceManager中,并且根据Android版本做了不同处理。

第三步:关联resource和Activity

1
2
3
4
Activity activity = mBase.newActivity(plugin.getClassLoader(), targetClassName, intent);
activity.setIntent(intent);
//设置Activity的mResources属性,Activity中访问资源时都通过mResources
ReflectUtil.setField(ContextThemeWrapper.class, activity, "mResources", plugin.getResources());

上述代码是在Activity创建时被调用的(后面会介绍如何hook Activity的创建过程),在activity被构造出来后,需要替换其中的mResources为插件的Resource。由于独立式时主工程的Resource不能访问插件的资源,所以如果不做替换,会产生资源访问错误。

做完以上工作后,则可以在插件的Activity中放心的使用setContentView,inflater等方法加载布局了。

四大组件支持

Android开发中有一些特殊的类,是由系统创建的,并且由系统管理生命周期。如常用的四大组件,Activity,Service,BroadcastReceiver和ContentProvider。 仅仅构造出这些类的实例是没用的,还需要管理组件的生命周期。其中以Activity最为复杂,不同框架采用的方法也不尽相同。下面以Activity为例详细介绍插件化如何支持组件生命周期的管理。 大致分为两种方式:

  • ProxyActivity代理
  • 预埋StubActivity,hook系统启动Activity的过程

ProxyActivity代理

ProxyActivity代理的方式最早是由dynamic-load-apk提出的,其思想很简单,在主工程中放一个ProxyActivy,启动插件中的Activity时会先启动ProxyActivity,在ProxyActivity中创建插件Activity,并同步生命周期。下图展示了启动插件Activity的过程:

具体的过程如下:

  1. 首先需要通过统一的入口(如图中的PluginManager)启动插件Activity,其内部会将启动的插件Activity信息保存下来,并将intent替换为启动ProxyActivity的intent。
  2. ProxyActivity根据插件的信息拿到该插件的ClassLoader和Resource,通过反射创建PluginActivity并调用其onCreate方法。
  3. PluginActivty调用的setContentView被重写了,会去调用ProxyActivty的setContentView。由于ProxyActivity重写了getResource返回的是插件的Resource,所以setContentView能够访问到插件中的资源。同样findViewById也是调用ProxyActivity的。
    4.ProxyActivity中的其他生命周期回调函数中调用相应PluginActivity的生命周期。

理解ProxyActivity代理方式主要注意两点:

  • ProxyActivity中需要重写getResouces,getAssets,getClassLoader方法返回插件的相应对象。生命周期函数以及和用户交互相关函数,如onResume,onStop,onBackPressedon,KeyUponWindow,FocusChanged等需要转发给插件。
  • PluginActivity中所有调用context的相关的方法,如setContentView,getLayoutInflater,getSystemService等都需要调用ProxyActivity的相应方法。

缺点:

  1. 插件中的Activity必须继承PluginActivity,开发侵入性强
  2. 如果想支持Activity的singleTask,singleInstance等launchMode时,需要自己管理Activity栈,实现起来很繁琐。
  3. 插件中需要小心处理Context,容易出错。
  4. 如果想把之前的模块改造成插件需要很多额外的工作。

该方式虽然能够很好的实现启动插件Activity的目的,但是由于开发式侵入性很强,dynamic-load-apk之后的插件化方案很少继续使用该方式,而是通过hook系统启动Activity的过程,让启动插件中的Activity像启动主工程的Activity一样简单。

hook方式

在介绍hook方式之前,先用一张图简要的介绍下系统是如何启动一个Activity的:

上图列出的是启动一个Activity的主要过程,具体步骤如下:

  1. Activity1调用startActivity,实际会调用Instrumentation类的execStartActivity方法,Instrumentation是系统用来监控Activity运行的一个类,Activity的整个生命周期都有它的影子。
  2. 通过跨进程的binder调用,进入到ActivityManagerService中,其内部会处理Activity栈。之后又通过跨进程调用进入到Activity2所在的进程中。
  3. ApplicationThread是一个binder对象,其运行在binder线程池中,内部包含一个H类,该类继承于类Handler。ApplicationThread将启动Activity2的信息通过H对象发送给主线程。
  4. 主线程拿到Activity2的信息后,调用Instrumentation类的newActivity方法,其内通过ClassLoader创建Activity2实例。

下面介绍如何通过hook的方式启动插件中的Activity,需要解决以下两个问题:

  • 插件中的Activity没有在AndroidManifest中注册,如何绕过检测。
  • 如何构造Activity实例,同步生命周期

解决方法有很多种,以VirtualAPK为例,核心思路如下:

  1. 先在Manifest中预埋StubActivity,启动时hook上图第1步,将Intent替换成StubActivity。
  2. hook第10步,通过插件的ClassLoader反射创建插件Activity
  3. 之后Activity的所有生命周期回调都会通知给插件Activity
替换系统Instrumentation

VirtualAPK在初始化时会调用hookInstrumentationAndHandler,该方法hook了系统的Instrumentaiton类,由上文可知该类和Activity的启动息息相关。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
private void hookInstrumentationAndHandler() { 
try {
//获取Instrumentation对象
Instrumentation baseInstrumentation = ReflectUtil.getInstrumentation(this.mContext);
//构造自定义的VAInstrumentation
final VAInstrumentation instrumentation = new VAInstrumentation(this, baseInstrumentation);
//设置ActivityThread的mInstrumentation和mCallBack
Object activityThread = ReflectUtil.getActivityThread(this.mContext);
ReflectUtil.setInstrumentation(activityThread, instrumentation);
ReflectUtil.setHandlerCallback(this.mContext, instrumentation);
this.mInstrumentation = instrumentation;
} catch (Exception e) {
e.printStackTrace();
}
}

该段代码将主线程中的Instrumentation对象替换成了自定义的VAInstrumentation类。在启动和创建插件activity时,该类都会偷偷做一些手脚。

hook activity启动过程

VAInstrumentation类重写了execStartActivity方法,相关代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
public ActivityResult execStartActivity(
//省略了无关参数
Intent intent) {
//转换隐式intent
mPluginManager.getComponentsHandler().transformIntentToExplicitAsNeeded(intent);
if (intent.getComponent() != null) {
//替换intent中启动Activity为StubActivity
this.mPluginManager.getComponentsHandler().markIntentIfNeeded(intent);
}

//调用父类启动Activity的方法}
public void markIntentIfNeeded(Intent intent) {
if (intent.getComponent() == null) {
return;
}

String targetPackageName = intent.getComponent().getPackageName();
String targetClassName = intent.getComponent().getClassName(); // search map and return specific launchmode stub activity
if (!targetPackageName.equals(mContext.getPackageName()) && mPluginManager.getLoadedPlugin(targetPackageName) != null) {
intent.putExtra(Constants.KEY_IS_PLUGIN, true);
intent.putExtra(Constants.KEY_TARGET_PACKAGE, targetPackageName);
intent.putExtra(Constants.KEY_TARGET_ACTIVITY, targetClassName);
dispatchStubActivity(intent);
}
}

execStartActivity中会先去处理隐式intent,如果该隐式intent匹配到了插件中的Activity,将其转换成显式。之后通过markIntentIfNeeded将待启动的的插件Activity替换成了预先在AndroidManifest中占坑的StubActivity,并将插件Activity的信息保存到该intent中。其中有个dispatchStubActivity函数,会根据Activity的launchMode选择具体启动哪个StubActivity。VirtualAPK为了支持Activity的launchMode在主工程的AndroidManifest中对于每种启动模式的Activity都预埋了多个坑位

hook Activity的创建过程

上一步欺骗了系统,让系统以为自己启动的是一个正常的Activity。当来到图的第10步时,再将插件的Activity换回来。此时调用的是VAInstrumentation类的newActivity方法。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
@Override
public Activity newActivity(ClassLoader cl, String className, Intent intent){
try {
cl.loadClass(className);
} catch (ClassNotFoundException e) {
//通过LoadedPlugin可以获取插件的ClassLoader和Resource
LoadedPlugin plugin = this.mPluginManager.getLoadedPlugin(intent);
//获取插件的主Activity
String targetClassName = PluginUtil.getTargetActivity(intent);
if (targetClassName != null) {
//传入插件的ClassLoader构造插件Activity
Activity activity = mBase.newActivity(plugin.getClassLoader(), targetClassName, intent);
activity.setIntent(intent);
//设置插件的Resource,从而可以支持插件中资源的访问
try {
ReflectUtil.setField(ContextThemeWrapper.class, activity, "mResources", plugin.getResources());
} catch (Exception ignored) {
// ignored.
}
return activity;
}
}
return mBase.newActivity(cl, className, intent);
}

由于AndroidManifest中预埋的StubActivity并没有具体的实现类,所以此时会发生ClassNotFoundException。之后在处理异常时取出插件Activity的信息,通过插件的ClassLoader反射构造插件的Activity。

其他操作

插件Activity构造出来后,为了能够保证其正常运行还要做些额外的工作。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
@Override
public void callActivityOnCreate(Activity activity, Bundle icicle) {
final Intent intent = activity.getIntent();
if (PluginUtil.isIntentFromPlugin(intent)) {
Context base = activity.getBaseContext();
try {
LoadedPlugin plugin = this.mPluginManager.getLoadedPlugin(intent);
ReflectUtil.setField(base.getClass(), base, "mResources", plugin.getResources());
ReflectUtil.setField(ContextWrapper.class, activity, "mBase", plugin.getPluginContext());
ReflectUtil.setField(Activity.class, activity, "mApplication", plugin.getApplication());
ReflectUtil.setFieldNoException(ContextThemeWrapper.class, activity, "mBase", plugin.getPluginContext());
// set screenOrientation
ActivityInfo activityInfo = plugin.getActivityInfo(PluginUtil.getComponent(intent));
if (activityInfo.screenOrientation != ActivityInfo.SCREEN_ORIENTATION_UNSPECIFIED) {
activity.setRequestedOrientation(activityInfo.screenOrientation);
}
} catch (Exception e) {
e.printStackTrace();
}

}

mBase.callActivityOnCreate(activity, icicle);
}

这段代码主要是将Activity中的Resource,Context等对象替换成了插件的相应对象,保证插件Activity在调用涉及到Context的方法时能够正确运行。

经过上述步骤后,便实现了插件Activity的启动,并且该插件Activity中并不需要什么额外的处理,和常规的Activity一样。那问题来了,之后的onResume,onStop等生命周期怎么办呢?答案是所有和Activity相关的生命周期函数,系统都会调用插件中的Activity。原因在于AMS在处理Activity时,通过一个token表示具体Activity对象,而这个token正是和启动Activity时创建的对象对应的,而这个Activity被我们替换成了插件中的Activity,所以之后AMS的所有调用都会传给插件中的Activity。

其他组件

四大组件中Activity的支持是最复杂的,其他组件的实现原理要简单很多,简要概括如下:

  • Service:Service和Activity的差别在于,Activity的生命周期是由用户交互决定的,而Service的生命周期是我们通过代码主动调用的,且Service实例和manifest中注册的是一一对应的。实现Service插件化的思路是通过在manifest中预埋StubService,hook系统startService等调用替换启动的Service,之后在StubService中创建插件Service,并手动管理其生命周期。
  • BroadCastReceiver:解析插件的manifest,将静态注册的广播转为动态注册。
  • ContentProvider:类似于Service的方式,对插件ContentProvider的所有调用都会通过一个在manifest中占坑的ContentProvider分发。

小结

VirtualAPK通过替换了系统的Instrumentation,hook了Activity的启动和创建,省去了手动管理插件Activity生命周期的繁琐,让插件Activity像正常的Activity一样被系统管理,并且插件Activity在开发时和常规一样,即能独立运行又能作为插件被主工程调用。

其他插件框架在处理Activity时思想大都差不多,无非是这两种方式之一或者两者的结合。在hook时,不同的框架可能会选择不同的hook点。如360的RePlugin框架选择hook了系统的ClassLoader,即图2中构造Activity2的ClassLoader,在判断出待启动的Activity是插件中的时,会调用插件的ClassLoader构造相应对象。另外RePlugin为了系统稳定性,选择了尽量少的hook,因此它并没有选择hook系统的startActivity方法来替换intent,而是通过重写Activity的startActivity,因此其插件Activity是需要继承一个类似PluginActivity的基类的。不过RePlugin提供了一个Gradle插件将插件中的Activity的基类换成了PluginActivity,用户在开发插件Activity时也是没有感知的。

资源加载

资源加载方案大家使用的原理都差不多,都是用AssetManager的隐藏方法addAssetPath;但是,不同插件的资源如何管理?是公用一套资源还是插件独立资源?共用资源如何避免资源冲突?对于资源加载,有的方案共用一套资源并采用资源分段机制解决冲突(要么修改aapt要么添加编译插件);有的方案选择独立资源,不同插件管理自己的资源。

Android系统通过Resource对象加载资源,下面代码展示了该对象的生成过程:

1
2
3
4
5
6
7
8
//创建AssetManager对象 
AssetManager assets = new AssetManager();
//将apk路径添加到AssetManager中
if (assets.addAssetPath(resDir) == 0){
return null;
}
//创建Resource对象
r = new Resources(assets, metrics, getConfiguration(), compInfo);

因此,只要将插件apk的路径加入到AssetManager中,便能够实现对插件资源的访问。具体实现时,由于AssetManager并不是一个public的类,需要通过反射去创建,并且部分Rom对创建的Resource类进行了修改,所以需要考虑不同Rom的兼容性。

不同版本res加载方式有差异,可参考:https://zhuanlan.zhihu.com/p/34346219

资源路径的处理

和代码加载相似,插件和主工程的资源关系也有两种处理方式:
合并式:addAssetPath时加入所有插件和主工程的路径;
独立式:各个插件只添加自己apk路径

方式 优点 缺点
合并式 插件和主工程能够直接相互访问资源 会引入资源冲突
独立式 资源隔离,不存在资源冲突 资源共享比较麻烦

合并式由于AssetManager中加入了所有插件和主工程的路径,因此生成的Resource可以同时访问插件和主工程的资源。但是由于主工程和各个插件都是独立编译的,生成的资源id会存在相同的情况,在访问时会产生资源冲突。

独立式时,各个插件的资源是互相隔离的,不过如果想要实现资源的共享,必须拿到对应的Resource对象。

解决资源冲突

合并式的资源处理方式,会引入资源冲突,原因在于不同插件中的资源id可能相同,所以解决方法就是使得不同的插件资源拥有不同的资源id。

资源id是由8位16进制数表示,表示为0xPPTTNNNN。PP段用来区分包空间,默认只区分了应用资源和系统资源,TT段为资源类型,NNNN段在同一个APK中从0000递增。

所以思路是修改资源ID的PP段,对于不同的插件使用不同的PP段,从而区分不同插件的资源。具体实现方式有两种:
1.修改aapt源码,编译期修改PP段。
2.修改resources.arsc文件,该文件列出了资源id到具体资源路径的映射。

原理

apk被安装之后,apk的文件代码以及资源会被系统存放在固定的目录比如/data/app/package_name/xxx.apk)中,系统在进行类加载的时候,会自动去这一个或者几个特定的路径来寻找这个类。

但是要知道插件apk是不会被安装的,那么系统也就不会讲我们的代理及资源存在在这个目录下,换句话说系统并不知道我们插件apk中的任何信息,也就根本不可能加载我们插件中的类。我们之前分析过应用的启动过程,其实就是启动了我们的主Activity,然后在ActivityThread的performLaunchActivity方法中创建的Activity对象并回调了attch和onCreate方法。系统通过待启动的Activity的类名className,然后使用ClassLoader对象cl把这个类加载,最后使用反射创建了这个Activity类的实例对象。

有两种实现方法:

  1. 首先如果我们想要加载我们的插件apk我们需要一个Classloader,那么我们知道系统的Classloader是通过LoadedApk对象获得的,而如果我们想要加载我们自己的插件apk,就需要我们自己构建一个LoadedApk对象,然后修改其中的Classloader对象,因为系统的并不知道我们的插件apk的信息,所有我们就要创建自己的ClassLoader对象,然后全盘接管加载的过程,然后通过hook的思想将我们构建的这个LoadedApk对象存入那个叫mPackages的map中,这样的话每次在获取LoadedApk对象时就可以在map中得到了。然后在到创建Activity的时候得到的Classloader对象就是我们自己改造过的cl了,这样就可以加载我们的外部插件了。这种方案需要我们hook掉系统系统的n多个类或者方法,因为创建LoadedApk对象时还需要一个ApplicationInfo的对象,这个对象就是解析AndroidManifest清单得来的,所以还需要我们自己手动解析插件中的AndroidManifest清单,这个过程及其复杂,不过360的DroidPlugin就使用了这种方法。

  2. 既然我们知道如果想启动插件apk就需一个Classloader,那么我们换一种想法,能不能我们将我们的插件apk的信息告诉系统的这个Classloader,然后让系统的Classloader来帮我们加载及创建呢?答案是肯定,之前我们说过讲过android中的Classloader主要分析PathClassLoader和DexClassLoader,系统通过PathClassLoader来加载系统类和主dex中的类。而DexClassLoader则用于加载其他dex文件中的类。他们都是继承自BaseDexClassLoader。(如果没有看过的建议先看看:类加载器ClassLoader及Dex/Class

回顾一下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
//这个类中维护这一个dexElements的数组,在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;
}

类在被加载的时候是通过BaseDexClassLoader的findClass的方法,其实最终调用了DexPathList类的findClass,DexPathList类中维护着dexElements的数组,这个数组就是存放我们dex文件的数组,我们只要想办法将我们插件apk的dex文件插入到这个dexElements中系统就可以知道我们的插件apk信息了,也自然就可以帮我们加载并创建对应的类。但是到这里还有一个问题,那就是Activity必须要在AndroidManifest注册才行,这个检查过程是在系统底层的,我们无法干涉,可是我们的插件apk是动态灵活的,宿主中并不固定的写死注册哪几个Activity,如果写死也就失去了插件的动态灵活性。

但是我们可以换一种方式,我们使用hook思想代理startActivity这个方法,使用占坑的方式,也就是说我们可以提前在AndroidManifest中固定写死一个Activity,这个Activity只不过是一个傀儡,我们在启动我们插件apk的时候使用它去系统层校检合法性,然后等真正创建Activity的时候再通过hook思想拦截Activity的创建方法,提前将信息更换回来创建真正的插件apk。

总结下分析结果:

1.startActivity的时候最终会走到AMS的startActivity方法

2.系统会检查一堆的信息验证这个Activity是否合法

3.然后会回调ActivityThread的Handler里的 handleLaunchActivity

4.在这里走到了performLaunchActivity方法去创建Activity并回调一系列生命周期的方法

5.创建Activity的时候会创建一个LoaderApk对象,然后使用这个对象的getClassLoader来创建Activity

6.我们查看getClassLoader()方法发现返回的是PathClassLoader,然后他继承自BaseDexClassLoader

7.然后我们查看BaseDexClassLoader发现他创建时创建了一个DexPathList类型的pathList对象,然后在findClass时调用了pathList.findClass的方法

8.然后我们查看DexPathList类中的findClass发现他内部维护了一个Element[] dexElements的dex数组,findClass时是从数组中遍历查找的

参考资料

https://blog.csdn.net/yulong0809/article/details/59113935
深入理解Android插件化技术
爱奇艺组件化探索之原理篇