Android插件化 学习
原文:http://weishu.me/2016/01/28/understand-plugin-framework-overview/
插件化技术听起来高深莫测,实际上要解决的就是两个问题:
- 代码加载
- 资源加载
代码加载
类的加载可以使用Java的ClassLoader
机制,但是对于Android来说,并不是说类加载进来就可以用了,很多组件都是有“生命”的;因此对于这些有血有肉的类,必须给它们注入活力,也就是所谓的组件生命周期管理;
另外,如何管理加载进来的类也是一个问题。假设多个插件依赖了相同的类,是抽取公共依赖进行管理还是插件单独依赖?这就是ClassLoader的管理问题;
资源加载
资源加载方案大家使用的原理都差不多,都是用AssetManager
的隐藏方法addAssetPath
;但是,不同插件的资源如何管理?是公用一套资源还是插件独立资源?共用资源如何避免资源冲突?对于资源加载,有的方案共用一套资源并采用资源分段机制解决冲突(要么修改aapt
要么添加编译插件);有的方案选择独立资源,不同插件管理自己的资源。
--------------------------------------------分割线------------------------------------------------
Activity生命周期管理
瞒天过海——启动不在AndroidManifest.xml中声明的Activity
原文:http://weishu.me/2016/03/21/understand-plugin-framework-activity-management/
简要分析
通过上文的分析,我们已经对Activity的启动过程了如指掌了;就让我们干点坏事吧 :D
对与『必须在AndroidManifest.xml中显示声明使用的Activity』这个问题,上文给出了思路——瞒天过海;我们可以在AndroidManifest.xml里面声明一个替身Activity,然后在合适的时候把这个假的替换成我们真正需要启动的Activity就OK了。
那么问题来了,『合适的时候』到底是什么时候?在前文Hook机制之动态代理中我们提到过Hook过程最重要的一步是寻找Hook点;如果是在同一个进程,startActivity
到Activity真正启动起来这么长的调用链,我们随便找个地方Hook掉就完事儿了;但是问题木有这么简单。
Activity启动过程中很多重要的操作(正如上文分析的『必须在AndroidManifest.xml中显式声明要启动的Activity』)都不是在App进程里面执行的,而是在AMS所在的系统进程system_server完成,由于进程隔离的存在,我们对别的进程无能为力;所以这个Hook点就需要花点心思了。
这时候Activity启动过程的知识就派上用场了;虽然整个启动过程非常复杂,但其实一张图就能总结:
先从App进程调用startActivity
;然后通过IPC调用进入系统进程system_server,完成Activity管理以及一些校检工作,最后又回到了APP进程完成真正的Activioty对象创建。
由于这个检验过程是在AMS进程完成的,我们对system_server进程里面的操作无能为力,只有在我们APP进程里面执行的过程才是有可能被Hook掉的,也就是第一步和第三步;具体应该怎么办呢?
既然需要一个显式声明的Activity,那就声明一个!可以在第一步假装启动一个已经在AndroidManifest.xml里面声明过的替身Activity,让这个Activity进入AMS进程接受检验;最后在第三步的时候换成我们真正需要启动的Activity;这样就成功欺骗了AMS进程,瞒天过海!
说到这里,是不是有点小激动呢?我们写个demo验证一下:『启动一个并没有在AndroidManifest.xml中显示声明的Activity』
实战过程
具体来说,我们打算实现如下功能:在MainActivity中启动一个并没有在AndroidManifest.xml中声明的TargetActivity;按照上文分析,我们需要声明一个替身Activity,我们叫它StubActivity;
那么,我们的AndroidManifest.xml如下:
1
|
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
|
OK,那么我们启动TargetActivity很简单,就是个startActivity
调用的事:
1
|
startActivity(new Intent(MainActivity.this, TargetActivity.class));
|
如果你直接这么运行,肯定会直接抛出ActivityNotFoundException然后直接退出;我们接下来要做的就是让这个调用成功启动TargetActivity。
1.狸猫换太子——使用替身Activity绕过AMS
由于AMS
进程会对Activity做显式声明验证,因此在
启动Activity的控制权转移到AMS
进程之前,我们需要想办法临时把TargetActivity替换成替身StubActivity;在这之间有很长的一段调用链,我们可以轻松Hook掉;选择什么地方Hook是一个很自由的事情,但是Hook的步骤越后越可靠——Hook得越早,后面的调用就越复杂,越容易出错。
我们可以选择在进入AMS
进程的入口进行Hook,具体来说也就是Hook AMS
在本进程的代理对象ActivityManagerNative。如果你不知道如何Hook掉这个AMS的代理对象,请查阅我之前的文章 Hook机制之AMS&PMS
我们Hook掉ActivityManagerNative对于startActivity方法的调用,替换掉交给AMS的intent对象,将里面的TargetActivity的暂时替换成已经声明好的替身StubActivity;这种Hook方式 前文 讲述的很详细,不赘述;替换的关键代码如下:
1
|
if ("startActivity".equals(method.getName())) {
|
通过这个替换过程,在ActivityManagerNative的startActivity调用之后,system_server端收到Binder驱动的消息,开始执行ActivityManagerService里面真正的startActivity
方法;这时候AMS看到的intent
参数里面的组件已经是StubActivity
了,因此可以成功绕过检查,这时候如果不做后面的Hook,直接调用
1
|
startActivity(new Intent(MainActivity.this, TargetActivity.class));
|
也不会出现上文的ActivityNotFoundException
2.借尸还魂——拦截Callback恢复真身
行百里者半九十。现在我们的startActivity
启动一个没有显式声明的Activity已经不会抛异常了,但是要真正正确地把TargetActivity启动起来,还有一些事情要做。其中最重要的一点是,我们用替身StubActivity临时换了TargetActivity,肯定需要在『合适的』时候替换回来;接下来我们就完成这个过程。
在AMS进程里面我们是没有办法换回来的,因此我们要等AMS把控制权交给App所在进程,也就是上面那个『Activity启动过程简图』的第三步。AMS进程转移到App进程也是通过Binder调用完成的,承载这个功能的Binder对象是IApplicationThread;在App进程它是Server端,在Server端接受Binder远程调用的是Binder线程池,Binder线程池通过Handler将消息转发给App的主线程;(我这里不厌其烦地叙述Binder调用过程,希望读者不要反感,其一加深印象,其二懂Binder真的很重要)我们可以在这个Handler里面将替身恢复成真身。
这里不打算讲述Handler 的原理,我们简单看一下Handler是如何处理接收到的Message的,如果我们能拦截这个Message的接收过程,就有可能完成替身恢复工作;Handler类的dispathMesage
如下:
1
|
public void dispatchMessage(Message msg) {
|
从这个方法可以看出来,Handler类消息分发的过程如下:
- 如果传递的Message本身就有callback,那么直接使用Message对象的callback方法;
- 如果Handler类的成员变量
mCallback
存在,那么首先执行这个mCallback
回调; - 如果
mCallback
的回调返回true
,那么表示消息已经成功处理;直接结束。 - 如果
mCallback
的回调返回false
,那么表示消息没有处理完毕,会继续使用Handler类的handleMessage
方法处理消息。
那么,ActivityThread中的Handler类H
是如何实现的呢?H
的部分源码如下:
1
|
public void handleMessage(Message msg) {
|
可以看到H
类仅仅重载了handleMessage
方法;通过dispathMessage的消息分发过程得知,我们可以拦截这一过程:把这个H
类的mCallback
替换为我们的自定义实现,这样dispathMessage
就会首先使用这个自定义的mCallback
,然后看情况使用H
重载的handleMessage
。
这个Handler.Callback
是一个接口,我们可以使用动态代理或者普通代理完成Hook,这里我们使用普通的静态代理方式;创建一个自定义的Callback类:
1
|
/* package */ class ActivityThreadHandlerCallback implements Handler.Callback {
|
这个Callback类的使命很简单:把替身StubActivity恢复成真身TargetActivity;有了这个自定义的Callback之后我们需要把ActivityThread里面处理消息的Handler类H
的的mCallback
修改为自定义callback类的对象:
1
|
// 先获取到当前的ActivityThread对象
|
到这里,我们已经成功地绕过AMS
,完成了『启动没有在AndroidManifest.xml中显式声明的Activity』的过程;瞒天过海,这种玩弄系统与股掌之中的快感你们能体会到吗?
3.僵尸or活人?——能正确收到生命周期回调吗
虽然我们完成了『启动没有在AndroidManifest.xml中显式声明的Activity 』,但是启动的TargetActivity是否有自己的生命周期呢,我们还需要额外的处理过程吗?
实际上TargetActivity已经是一个有血有肉的Activity了:它具有自己正常的生命周期;可以运行Demo代码验证一下。
这个过程是如何完成的呢?我们以onDestroy
为例简要分析一下:
从Activity的
finish
方法开始跟踪,最终会通过ActivityManagerNative到AMS
然后接着通过ApplicationThread到ActivityThread,然后通过H
转发消息到ActivityThread的handleDestroyActivity,接着这个方法把任务交给performDestroyActivity完成。
在真正分析这个方法之前,需要说明一点的是:不知读者是否感受得到,App进程与AMS
交互几乎都是这么一种模式,几个角色 ActivityManagerNative, ApplicationThread, ActivityThread以及Handler类H
分工明确,读者可以按照这几个角色的功能分析AMS
的任何调用过程,屡试不爽;这也是我的初衷——希望分析插件框架的过程中能帮助深入理解Android Framework。
好了继续分析performDestroyActivity,关键代码如下:
1
|
ActivityClientRecord r = mActivities.get(token);
|
这里通过mActivities
拿到了一个ActivityClientRecord,然后直接把这个record里面的Activity交给Instrument类完成了onDestroy的调用。
在我们这个demo的场景下,r.activity是TargetActivity还是StubActivity?按理说,由于我们欺骗了AMS
,AMS
应该只知道StubActivity
的存在,它压根儿就不知道TargetActivity是什么,为什么它能正确完成对TargetActivity生命周期的回调呢?
一切的秘密在token
里面。AMS
与ActivityThread
之间对于Activity的生命周期的交互,并没有直接使用Activity对象进行交互,而是使用一个token来标识,这个token是binder对象,因此可以方便地跨进程传递。Activity里面有一个成员变量mToken
代表的就是它,token可以唯一地标识一个Activity对象,它在Activity的attach
方法里面初始化;
在AMS
处理Activity的任务栈的时候,使用这个token标记Activity,因此在我们的demo里面,AMS
进程里面的token对应的是StubActivity,也就是AMS
还在傻乎乎地操作StubActivity(关于这一点,你可以dump出任务栈的信息,可以观察到dump出的确实是StubActivity)。但是在我们App进程里面,token对应的却是TargetActivity!因此,在ActivityThread执行回调的时候,能正确地回调到TargetActivity相应的方法。
为什么App进程里面,token对应的是TargetActivity呢?
回到代码,ActivityClientRecord是在mActivities
里面取出来的,确实是根据token取;那么这个token是什么时候添加进去的呢?我们看performLaunchActivity就完成明白了:它通过classloader加载了TargetActivity,然后完成一切操作之后把这个activity添加进了mActivities
!另外,在这个方法里面我们还能看到对Ativityattach
方法的调用,它传递给了新创建的Activity一个token对象,而这个token是在ActivityClientRecord构造函数里面初始化的。
至此我们已经可以确认,通过这种方式启动的Activity有它自己完整而独立的生命周期!
小节
本文讲述了『启动一个并没有在AndroidManifest.xml中显示声明的Activity』的解决办法,我们成功地绕过了Android的这个限制,这个是插件Activity管理技术的基础;但是要做到启动一个插件Activity问题远没有这么简单。
在本文所述例子中,TargetActivity与StubActivity存在于同一个Apk,因此系统的ClassLoader能够成功加载并创建TargetActivity的实例。但是在实际的插件系统中,要启动的目标Activity肯定存在于一个单独的文件中,系统默认的ClassLoader无法加载插件中的Activity类——系统压根儿就不知道要加载的插件在哪,谈何加载?因此还有一个很重要的问题需要处理:我们要完成插件系统中类的加载,这可以通过自定义ClassLoader实现。
解决了『启动没有在AndroidManifest.xml中显式声明的,并且存在于外部文件中的Activity』的问题,插件系统对于Activity的管理才算得上是一个完全体。
篇幅所限,欲知后事如何,请听下回分解!
--------------------------------------------分割线------------------------------------------------
插件加载机制
原文:http://weishu.me/2016/04/05/understand-plugin-framework-classloader/
思路分析
Android系统使用了ClassLoader机制来进行Activity等组件的加载;apk被安装之后,APK文件的代码以及资源会被系统存放在固定的目录(比如/data/app/package_name/base-1.apk )系统在进行类加载的时候,会自动去这一个或者几个特定的路径来寻找这个类;但是系统并不知道存在于插件中的Activity组件的信息(插件可以是任意位置,甚至是网络,系统无法提前预知),因此正常情况下系统无法加载我们插件中的类;因此也没有办法创建Activity的对象,更不用谈启动组件了。
解决这个问题有两个思路,要么全盘接管这个类加载的过程;要么告知系统我们使用的插件存在于哪里,让系统帮忙加载;这两种方式或多或少都需要干预这个类加载的过程。
1.激进方案:Hook掉ClassLoader,自己操刀
从上述分析中我们得知,在获取LoadedApk的过程中使用了一份缓存数据;这个缓存数据是一个Map
,从包名到LoadedApk的一个映射。正常情况下,我们的插件肯定不会存在于这个对象里面;但是如果我们手动把我们插件的信息添加到里面呢?系统在查找缓存的过程中,会直接命中缓存!进而使用我们添加进去的LoadedApk的ClassLoader来加载这个特定的Activity类!这样我们就能接管我们自己插件类的加载过程了!
。。。、、、
2.保守方案:委托系统,让系统帮忙加载
我们再次搬出ActivityThread中加载Activity类的代码:
1
|
java.lang.ClassLoader cl = r.packageInfo.getClassLoader();
|
我们知道 这个r.packageInfo中的r
是通过getPackageInfoNoCheck获取到的;在『激进方案』中我们把插件apk手动添加进缓存,采用自己加载办法解决;如果我们不干预这个过程,导致无法命中mPackages中的缓存,会发生什么?
查阅 getPackageInfo方法如下:
1
|
private LoadedApk getPackageInfo(ApplicationInfo aInfo, CompatibilityInfo compatInfo,
|
可以看到,没有命中缓存的情况下,系统直接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。
LoadedApk的ClassLoader到底是什么?
现在我们确保了『使用宿主ClassLoader帮助加载插件类』可行性;那么我们应该如何完成这个过程呢?
知己知彼,百战不殆。
不论是宿主程序还是插件程序都是通过LoadedApk的getClassLoader()方法返回的ClassLoader进行类加载的,返回的这个ClassLoader到底是个什么东西??这个方法源码如下:
1
|
public ClassLoader getClassLoader() {
|
可以看到,非android
开头的包和android
开头的包分别使用了两种不同的ClassLoader,我们只关心第一种;因此继续跟踪ApplicationLoaders类:
1
|
public ClassLoader getClassLoader(String zip, String libPath, ClassLoader parent)
|
可以看到,应用程序使用的ClassLoader都是PathClassLoader类的实例。那么,这个PathClassLoader是什么呢?从Android SDK给出的源码只能看出这么多:
1
|
public class PathClassLoader extends BaseDexClassLoader {
|
SDK没有导出这个类的源码,我们去androidxref上面看;发现其实这个类真的就这么多内容;我们继续查看它的父类BaseDexClassLoader;ClassLoader嘛,我们查看findClass或者defineClass方法,BaseDexClassLoader的findClass方法如下:
1
|
protected Class<?> findClass(String name) throws ClassNotFoundException {
|
可以看到,查找Class的任务通过pathList
完成;这个pathList
是一个DexPathList
类的对象,它的findClass
方法如下:
1
|
public Class findClass(String name, List<Throwable> suppressed) {
|
这个DexPathList内部有一个叫做dexElements的数组,然后findClass的时候会遍历这个数组来查找Class;如果我们把插件的信息塞进这个数组里面,那么不就能够完成类的加载过程吗?!!
给默认ClassLoader打补丁
通过上述分析,我们知道,可以把插件的相关信息放入BaseDexClassLoader的表示dex文件的数组里面,这样宿主程序的ClassLoader在进行类加载,遍历这个数组的时候,会自动遍历到我们添加进去的插件信息,从而完成插件类的加载!
接下来,我们实现这个过程;我们会用到一些较为复杂的反射技术哦~不过代码非常短:
1
|
public static void patchClassLoader(ClassLoader cl, File apkFile, File optDexFile)
|
短短的二十几行代码,我们就完成了『委托宿主ClassLoader加载插件类』的任务;因此第二种方案也宣告完成!我们简要总结一下这种方式的原理:
- 默认情况下performLacunchActivity会使用替身StubActivity的ApplicationInfo也就是宿主程序的CLassLoader加载所有的类;我们的思路是告诉宿主ClassLoader我们在哪,让其帮助完成类加载的过程。
- 宿主程序的ClassLoader最终继承自BaseDexClassLoader,BaseDexClassLoader通过DexPathList进行类的查找过程;而这个查找通过遍历一个dexElements的数组完成;我们通过把插件dex添加进这个数组就让宿主ClasLoader获取了加载插件类的能力。
小结
本文中我们采用两种方案成功完成了『启动没有在AndroidManifest.xml中显示声明,并且存在于外部插件中的Activity』的任务。
『激进方案』中我们自定义了插件的ClassLoader,并且绕开了Framework的检测;利用ActivityThread对于LoadedApk的缓存机制,我们把携带这个自定义的ClassLoader的插件信息添加进mPackages
中,进而完成了类的加载过程。
『保守方案』中我们深入探究了系统使用ClassLoader findClass的过程,发现应用程序使用的非系统类都是通过同一个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采用的『保守方案』那么,有没有两种优点兼顾的方案呢??答案自然是有的。