现如今插件化的思想和应用在Android上越来越多了,各式各样的方案也是层出不穷,这篇文章旨在告诉大家插件化的核心思想是什么,又有什么样的实现方式。
前言
首先,这篇文章的题目为什么不沿用我之前xxxx!xxxxx这样的风格呢,因为我觉得这样风格太中二了。。
其次,我写这篇文章的原因是因为前些时候看到有大神写了一篇文章Android 插件化的 过去 现在 未来,里面的内容很不错,特别是有一些关于原理的东西,让我回想起当时看几个插件化框架的源码的时候产生的心得和体会,这里也是写出来给大家做一个分享吧。
插件化介绍
在开始真正讲解插件化之前,让我先告诉那些不了解插件化是什么的同学[什么是插件化]。
所谓插件化,就是让我们的应用不必再像原来一样把所有的内容都放在一个apk中,可以把一些功能和逻辑单独抽出来放在插件apk中,然后主apk做到[按需调用],这样的好处是一来可以减少主apk的体积,让应用更轻便,二来可以做到热插拔,更加动态化。
在后文中,我首先会对插件化的实现原理进行一个分析,接着我挑了其中两个比较有代表性的,[Small]和[DroidPlugin]来分析他们的实现有什么区别。
原理分析
在分析原理之前,让我们先想想做插件化会遇到的挑战。大家可以想一想,如果我要做一个插件apk,里面会包含什么?首先想到的肯定是activity和对应的资源,我们要做的事就是在主apk中调用插件apk中的activity,并且加载对应的资源。当然这只是其中的一个挑战,这里我不会带大家分析所有的原理,因为这样一天一夜都讲不完,所以我选取了其中最具代表性的一点:[主apk如何加载插件apk中的activity]。
首先,我们回想一下平时我们是怎么唤起一个activity的:
1 | Intent intent = new Intent(ActivityA.this,ActivityB.class); |
我们调用的是activity的startActivity方法,而我们都知道我们的activity是继承自ContextThemeWrapper的,而ContextThemeWrapper只是一个包装类,真正的逻辑在ContextImpl中,让我们看看其中做了什么。
1 |
|
其中调用了mMainThread.getInstrumentation()获取一个Instrumentation对象,这个对象大家可以看作是activity的管家,对activity的操作都会调用它去执行。让我们看看它的execStartActivity方法。
1 | public ActivityResult execStartActivity(Context who, IBinder contextThread, IBinder token, Activity target, |
这个方法最关键的一点就是调用了ActivityManagerNative.getDefault()去获取一个ActivityManagerNative对象并且调用了它的startActivity方法。
1 | public abstract class ActivityManagerNative extends Binder implements IActivityManager |
从它的定义就可以看出它是和aidl相关的,对于aidl这里我不细讲,大家可以自行查阅相关资料,概括来说就是Android中一种跨进程的通信方式。
那既然是跨进程的,通过ActivityManagerNative跨到了哪个进程呢?答案是ActivityManagerService,简称AMS,它是ActivityManagerNative的实现类。是Android framework层最最最重要的几个类之一,是运行在Android内核进程的。下面让我们看看AMS的startActivity方法。
1 |
|
可以看到调用了startActivityAsUser。而在startActivityAsUser方法中调用了ActivityStackSupervisor的startActivityMayWait方法。
1 | final int startActivityMayWait(IApplicationThread caller, int callingUid,String callingPackage, Intent intent, String resolvedType, IBinder resultTo,String resultWho, int requestCode, int startFlags, String profileFile, |
这个方法内容很多,前面主要是对一些权限的判断,这个我们等等再讲,而在判断完权限之后,调用了startActivityLocked方法。
在调用了startActivityLocked方法之后,是一系列和ActivityStack这个类的交互,这其中的过程我这里不分析了,从ActivityStack这个类的名字就可以看出它是和Activity栈相关的,交互的主要目的也就是处理activity栈的需求。最后会调用到realStartActivityLocked方法。
1 | final boolean realStartActivityLocked(ActivityRecord r,ProcessRecord app, boolean andResume, boolean checkConfig) |
这么长的方法,看的头都晕了,不过没关系,我们看重点的。
1 | app.thread.scheduleLaunchActivity(new Intent(r.intent), r.appToken,System.identityHashCode(r), r.info,new Configuration(mService.mConfiguration), r.compat, |
重点来了,调用了ApplicationThread的scheduleLaunchActivity方法。大家千万不要被这个类的名字所迷惑了,以为他是一个线程。其实它不仅是线程,还是一个Binder对象,也是和aidl相关的,实现了IApplicationThread接口,定义在ActivityThread类的内部。那我们为什么要用它呢?回想一下,刚刚在Instrumentation里调用了AMS的startActivity之后的所有操作,都是在系统进程中进行的,而现在我们要返回到我们自己的app进程,同样是跨进程,我们需要一个aidl框架去完成,所以这里才会有ApplicationThread。让我们看看具体的方法吧。
1 | public final void scheduleLaunchActivity(Intent intent, IBinder token, int ident,ActivityInfo info, Configuration curConfig, CompatibilityInfo compatInfo,int procState, Bundle state, List<ResultInfo> pendingResults,List<Intent> pendingNewIntents, boolean notResumed, boolean isForward,String profileName, ParcelFileDescriptor profileFd, boolean autoStopProfiler) { |
在最后调用了queueOrSendMessage(H.LAUNCH_ACTIVITY, r)这个方法。
而这里的H其实是一个handler,queueOrSendMessage方法的作用就是通过handler去send一个message。
1 | public void handleMessage(Message msg) { |
我们看H的handleMessage,如果message是我们刚刚发送的LAUNCH_ACTIVITY,则调用handleLaunchActivity方法。而在这个方法中,调用了performLaunchActivity去创建一个Activity。
1 | private Activity performLaunchActivity(ActivityClientRecord r, Intent customIntent) { |
方法比较长,其中核心的内容是:
1 | try { |
又调用了Instrumentation的newActivity去创建一个Activity。
至此,启动一个activity的过程就分析完了,让我们来总结一下。
(1) 我们app中是使用了Activity的startActivity方法,具体调用的是ContextImpl的同名函数。
(2) ContextImpl中会调用Instrumentation的execStartActivity方法。
(3) Instrumentation通过aidl进行跨进程通信,最终调用AMS的startActivity方法。
(4) 在系统进程中AMS中先判断权限,然后通过调用ActivityStackSupervisor和ActivityStack进行一系列的交互用来确定Activity栈的使用方式。
(5) 通过ApplicationThread进行跨进程通信,转回到app进程。
(6) 通过ActivityThread中的H(一个handler)传递消息,最终调用Instrumentation来创建一个Activity。
其实如果大家看过其他有关Android系统的调用,比如启动一个Service之类的,整个过程都是大同小异的,无非就是跨进程和AMS,WMS或者PMS进行通信,然后通过ApplicationThread回到app进程最后通过handler传递消息。
好了,说了这么多,这和我们的插件化有什么关系呢?大家想一想,如果我们要在主apk中启动一个插件apk的Activity,上面的哪一步会出问题?大家好好想一想,想一想,一想,想。。。。
没错!就是(4),第四步,权限验证会通不过,为啥呢?因为我们主apk的manifest中没有定义插件apk的Activity啊!
让我们回到代码,看看ActivityStackSupervisor的startActivityMayWait方法,关于权限验证的内容都在里面。
1 | if (err == ActivityManager.START_SUCCESS && aInfo == null) { |
在这个方法中有这么一段,而在Instrumentation中会去检查这个值。
1 | public static void checkStartActivityResult(int res, Object intent) { |
如果找不到对应的Activity,直接抛出错误。
唔。。怎么办呢?找不到Activity,也许你会觉得直接在主apk的manifest中实现定义就好了,但是这是不可能的,因为你怎么知道插件apk中有什么Activity呢?如果以后要动态的修改插件apk中的Activity,难道你的主apk也要对应的一次次修改吗?
不要慌,办法都是人想出来的,让我们看看Samll和DroidPlugin的解决办法吧。
思考解决方案
首先我们要明确,要解决的核心问题是[如何能让没有在manifst中注册的Activity能启动起来]。由于权限验证机制是系统做的,我们肯定是没办法修改的,既然我们没办法修改,那是不是考虑去欺骗呢?也就是说可以在manifest中预先定义好几个Activity,俗称占坑,比如名字就叫ActivityA,ActivityB,在校验权限之前把我们插件apk中的Activity替换成定义好的Activity,这样就能顺利通过校验,而在之后真正生成Activity的地方再换回来,瞒天过海。
那怎么去欺骗呢?回归前面的代码,其实答案已经呼之欲出了——我们可以有两种选择,hook Instrumentation或者hook ActivityManagerNative。这也正好对应了Small和DroidPlugin的实现方案。
Small实现方式
首先,让我们看一下Small的实现方式,大家可以去它的GitHub上下载源码。
上文提到,Samll的实现方式是hook Instrumentation,让我们从代码上来看,我们看它的ApkBundleLauncher类,它内部有一个setUp方法。
1 |
|
可以看到它通过反射获取了Instrumentation并且赋值成了自定义的InstrumentationWrapper。
而在自己定义的InstrumentationWrapper中,会去重写两个重要的方法,那就是execStartActivity和newActivity这两个方法。为什么会选择这两个方法呢?因为我们前面说过,在第一个方法中,系统会去校验权限,而第二个方法则是真正生成Activity实例的方法,我们通过重写两个方法,可以做到我们之前提到的[在execStartActivity的时候把Activity替换成自己的],而[在newActivity又换回成真正的Activity],从而做到[欺骗]的效果。
1 | /** @Override V21+ |
可以看到Wrapper首先根据sdk版本重写了两个不同的方法,这里我们只关注一个就可以,先看wrapIntent方法。
1 | private void wrapIntent(Intent intent) { |
这个方法的神奇之处在于它先获取了真正的Activity的信息并且保存起来,然后调用了dequeueStubActivity方法去生成一个[占坑]的Activity。
1 | private String dequeueStubActivity(ActivityInfo ai, String realActivityClazz) { |
可以看到其中根据真正的Activity的launchMode等因素生成一个占坑Activity,而这些占坑Activity都是定义在manifest中的。
1 | <application> |
通过这样的方式我们就顺利的完成了[瞒天过海]的第一步,在之后AMS的权限校验中就能顺利通过了。接着让我们来看newActivity方法。
1 |
|
这里就更简单了,通过unwrapIntent获取真正的Activity并且调用父类的newActivity方法,也就是Instrumentation去生成一个Activity。
到这儿,Small的解决方案就基本讲完了,原理是很简单的,就是通过反射替换掉Instrumentation,然后在里面做文章。
DroidPlugin实现方式
DroidPlugin是另外一种插件化框架,它采取的方案是hook整个ActivityManagerNative。
首先让我们看它其中的IActivityManagerHook类。
1 | public class IActivityManagerHook extends ProxyHook |
它继承自ProxyHook。
1 | public abstract class ProxyHook extends Hook implements InvocationHandler { |
ProxyHook实现了InvocationHandler接口,也就是说它是用于动态代理的,在invoke方法中先通过mHookHandles去获取对应的hookedMethodHandler,这里的mHookHandles在我们对应的情况下是IActivityManagerHookHandle。
1 | public class IActivityManagerHookHandle extends BaseHookHandle { |
可以看到在init中生成了很多类,而我们在invoke方法中就会根据对应的方法名拿到对应的类。
回想一下,我们在这里对应的是什么方法?根据前面的文章,我们知道是startActivity方法,进而拿到的是startActivity类。
回到invoke方法,拿到HookedMethodHandler后,会执行它的doInnerHook方法。
1 | public synchronized Object doHookInner(Object receiver, Method method, Object[] args) throws Throwable { |
可以看到这个方法也是AOP的,在真正的调用method.invoke之前和之后会对应的调用beforeInvoke和afterInvoke。这两个方法是有待HookedMethodHandler的子类去实现的,这里是startActivity这个子类。
1 |
|
根据sdk版本进入不同的方法,这里我们只看APIHigh。
1 | protected boolean doReplaceIntentForStartActivityAPIHigh(Object[] args) throws RemoteException { |
又是一个逻辑比较多的方法,但是很好理解,其实和Small做的事是差不多的,就是生成一个占坑的Intent,把真正的activity当作参数放在Intent中。
到这儿,就完成了第一步,我们要做的就是在对应的地方Hook掉这个AMS,至于怎么Hook大家自己去看源码,Hook的技巧不是重点,重点是Hook了什么。
那DroidPlugin是在哪里把Activity还原了呢?回想一下前文startActivity步骤6:
(6) 通过ActivityThread中的H(一个handler)传递消息,最终调用Instrumentation来创建一个Activity。
通过Handler去传递消息,让我们看看Handler的源码。
1 | public void dispatchMessage(Message msg) { |
在dispatchMessage中,有一个CallBack,如果它不为空,就使用它去处理而不走Handler的handleMessage方法。DroidPlugin正是利用了这一点,自己去生成了一个CallBack并且通过反射注入到了ActivityThread的H类中。
1 |
|
这是对应CallBack的handleMessage方法,如果message是LAUNCH_ACTIVITY,直接调用了handleLaunchActivity方法。
1 | private boolean handleLaunchActivity(Message msg) { |
代码依旧很长,但是核心就是拿到真正的Activity并且创建。
至此,DroidPlugin的逻辑也就理完了,和Small不同,它是hook了整个AMS并且通过反射替换了H类的CallBack,做到了[瞒天过海]。
两种方式的比较
那么这两种方式孰优孰劣呢?
我认为,DroidPlugin的方式是更加[屌]的,大家可以看上面IActivityManagerHookHandle这个类,在init方法中put的那些类,和Activity相关的只有一小部分,更多的是service,broadcast等等的操作。这是Small使用[Hook Instrumentation]所做不到的,因为Instrumentation只是Activity的管家,如果涉及到service这样的,它就无能为力了。
但是从另外一个方面说,Service的动态注册这样的需求其实是不多的,Hook Instrumentation基本已能满足大部分的场景的,另外Hook AMS需要的知识储备是要多得多的,拿我来说吧,看DroidPlugin的源码比看Small的源码困难太多了。。
这些都是要大家自己斟酌的。