首先说明下该文是基于Android8.0,目前网上大多是插件化机制方案博客都比较旧,Android 8.0较之前的改了很多,所以之前方案原理依然使用,但是实现代码需要改动
Android插件化技术是一项很实用的技术,一些大的厂商都在使用。该技术简言之就是运行一个未安装的apk。apk一般来说是肯定需要先下载到本地执行安装后才能正常使用。但是一个apk如果功能很多,比如支付宝,那么它的apk体积会很大。但是实际上它的apk也并没有多大,主要就是因为它的每个小功能都当作一个独立插件,使用的时候才去动态加载。这就是Android插件化技术。这个技术的优点有很多,比如缩小apk体积、插件与宿主独立开发等等。想具体了解插件化技术的前身今世可以看下这个Android插件化:从入门到放弃
DroidPlugin是360公司开发的一个十分成熟Android插件化方案,这里我们只简要学习一下其中的原理,能更好的理解整个Android的Framework层,对以后无论是Android开发还是其他Android相关工作都有很大帮助(吧)。关于Android最重要的我认为有两步:
Step 1 :Activity的生命周期管理
Step 2 :插件的加载机制
Step 1 在于如何启动一个未在AndroidManifest.xml注册的Activity。Step 2 在于宿主如何去加载插件的apk从而启动其中的Activity。(这里首先明确两个概念宿主与插件,宿主即是要启动插件的App,比如支付宝;插件是那个未安装的apk,比如支付宝里面的小插件-‘我的快递’等等。)
本文主要讲述基于Android 8.0系统如何启动一个未注册Activity的原理,(也就是说这个Activity在宿主的Apk里,只是没有注册,并不是插件的Activity,如何启动插件下一步会讲)具体实现可以用java反射动态代理的方式实现,我是基于Xposed HOOK的方法做的,可能大家都不用这种方法,所以下面的代码部分我没有贴我的,而是用的weishu这位大神的,下面会贴上他的链接。主要在原理解析。
AndroidManifest.xml的限制
相信大家在Android开发过程中都遇到过下面这个BUG,一旦大家在写了一个Activity但是没有在AndroidManifest.xml的注册,那么就会遇到这个问题。
E/AndroidRuntime﹕ FATAL EXCEPTION: main
Process: com.xxx.xxx, PID: xxx
android.content.ActivityNotFoundException: Unable to find explicit activity class {com.xxx.xxx.xxx.TargetActivity};
have you declared this activity in your AndroidManifest.xml?
启动Activity确实非常简单,但是Android却有一个限制:必须在AndroidManifest.xml中显示声明使用的Activity。这个硬性要求很大程度上限制了插件系统的发挥:假设我们需要启动一个插件的Activity,插件使用的Activity是无法预知的,这样肯定也不会在Manifest文件中声明;如果插件新添加一个Activity,主程序的AndroidManifest.xml就需要更新;既然双方都需要修改升级,何必要使用插件呢?这已经违背了动态加载的初衷:不修改插件框架而动态扩展功能。
但是我们可以耍个障眼法:既然AndroidManifest文件中必须声明,那么我就声明一个(或者有限个)替身Activity好了,当需要启动插件的某个Activity的时候,先让系统以为启动的是AndroidManifest中声明的那个替身,暂时骗过系统;然后到合适的时候又替换回我们需要启动的真正的Activity。所以首先要了解Activity的启动过程。
Activity插件化原理
我们开发的时候启动一个Activity就调startActivity就可以了,那么startyActivity这个调用背后发生了什么?这就需要看源码一步一步看下去了。这里可以百度看看其他人的博客,自己对着博客和源码一步一步看下去,就能完整的理解这个过程了。
大致启动过程如下所示,应用进程通过startActivity启动activity之后会通过ActivityManagerProxy向系统进程(ActivityManagerService所在进程)发送Binder通信,让AMS启动一个Activity,之后AMS会检测Acticity是否注册,然后再通过Binder请求让应用启动Activity。这个检测过程发生在AMS所在的进程system_server。
App进程会委托AMS进程完成Activity生命周期的管理以及任务栈的管理;这个通信过程AMS是Server端,App进程通过持有AMS的client代理ActivityManagerNative完成通信过程;
AMS进程完成生命周期管理以及任务栈管理后,会把控制权交给App进程,让App进程完成Activity类对象的创建,以及生命周期回调;这个通信过程也是通过Binder完成的,App所在server端的Binder对象存在于ActivityThread的内部类ApplicationThread;AMS所在client通过持有IApplicationThread的代理对象完成对于App进程的通信。
所以我们只要在AMP向AMS发送请求之前替换我们的请求信息,把我们要启动的插件Activity替换成我们提前声明好的替身Activity就好,这样AMS检测就不会出问题。然后等AMS向应用进程返回消息之后我们再把插件Activity替换回来即可。如图:
插件化实现
Step 1 将真实Activity替换为替身Activity
这里直接说HOOK点,Android 8.0之前普遍HOOK ActivityManagerProxy这个类,Android 8.0之后Android系统不再使用代理模式与AMS通信而是使用AIDL方式 [Proxy改AIDL] (https://blog.csdn.net/qi1017269990/article/details/78879512)所以HOOK的地方改为了 android.app.IActivityManager.Stub.Proxy
if ("startActivity".equals(method.getName())) {
{
// 找到参数里面的第一个Intent 对象
Intent raw;
int index = 0;
for (int i = 0; i < args.length; i++) {
if (args[i] instanceof Intent) {
index = i;
break;
}
}
raw = (Intent) args[index];
Intent newIntent = new Intent();
// 这里包名直接写死,如果再插件里,不同的插件有不同的包 传递插件的包名即可
String targetPackage = "com.xxx.xxx.xx";
// 这里我们把启动的Activity临时替换为 StubActivity
ComponentName componentName = new ComponentName(targetPackage, StubActivity.class.getCanonicalName());
newIntent.setComponent(componentName);
// 把我们原始要启动的TargetActivity先存起来
newIntent.putExtra(HookHelper.EXTRA_TARGET_INTENT, raw);
// 替换掉Intent, 达到欺骗AMS的目的
args[index] = newIntent;
Log.d(TAG, "hook success");
return method.invoke(mBase, args);
}
return method.invoke(mBase, args);
Step 2 将替身Activity重新换为真实Activity
行百里者半九十。现在我们的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里面将替身恢复成真身。
/* package */ class ActivityThreadHandlerCallback implements Handler.Callback {
Handler mBase;
public ActivityThreadHandlerCallback(Handler base) {
mBase = base;
}
@Override
public boolean handleMessage(Message msg) {
switch (msg.what) {
// ActivityThread里面 "LAUNCH_ACTIVITY" 这个字段的值是100
// 本来使用反射的方式获取最好, 这里为了简便直接使用硬编码
case 100:
handleLaunchActivity(msg);
break;
}
mBase.handleMessage(msg);
return true;
}
private void handleLaunchActivity(Message msg) {
// 这里简单起见,直接取出TargetActivity;
Object obj = msg.obj;
// 根据源码:
// 这个对象是 ActivityClientRecord 类型
// 我们修改它的intent字段为我们原来保存的即可.
/* switch (msg.what) {
/ case LAUNCH_ACTIVITY: {
/ Trace.traceBegin(Trace.TRACE_TAG_ACTIVITY_MANAGER, "activityStart");
/ final ActivityClientRecord r = (ActivityClientRecord) msg.obj;
/
/ r.packageInfo = getPackageInfoNoCheck(
/ r.activityInfo.applicationInfo, r.compatInfo);
/ handleLaunchActivity(r, null);
*/
try {
// 把替身恢复成真身
Field intent = obj.getClass().getDeclaredField("intent");
intent.setAccessible(true);
Intent raw = (Intent) intent.get(obj);
Intent target = raw.getParcelableExtra(HookHelper.EXTRA_TARGET_INTENT);
raw.setComponent(target.getComponent());
} catch (NoSuchFieldException e) {
e.printStackTrace();
} catch (IllegalAccessException e) {
e.printStackTrace();
}
}
}
这个Callback类的使命很简单:把替身StubActivity恢复成真身TargetActivity;有了这个自定义的Callback之后我们需要把ActivityThread里面处理消息的Handler类H的的mCallback修改为自定义callback类的对象:
Class> activityThreadClass = Class.forName("android.app.ActivityThread");
Field currentActivityThreadField = activityThreadClass.getDeclaredField("sCurrentActivityThread");
currentActivityThreadField.setAccessible(true);
Object currentActivityThread = currentActivityThreadField.get(null);
// 由于ActivityThread一个进程只有一个,我们获取这个对象的mH
Field mHField = activityThreadClass.getDeclaredField("mH");
mHField.setAccessible(true);
Handler mH = (Handler) mHField.get(currentActivityThread);
// 设置它的回调, 根据源码:
// 我们自己给他设置一个回调,就会替代之前的回调;
// public void dispatchMessage(Message msg) {
// if (msg.callback != null) {
// handleCallback(msg);
// } else {
// if (mCallback != null) {
// if (mCallback.handleMessage(msg)) {
// return;
// }
// }
// handleMessage(msg);
// }
// }
Field mCallBackField = Handler.class.getDeclaredField("mCallback");
mCallBackField.setAccessible(true);
mCallBackField.set(mH, new ActivityThreadHandlerCallback(mH));
这个时候就大功告成了。
小结
这篇文章只是告诉如何启动一个未注册的Activity,只说了如何让它启动起来,对于它的整个生命周期管理比如destory等的处理方式其实是一样的。本文重在原理解析。
最后,在本文所述例子中,插件Activity与替身Activity存在于同一个Apk,因此系统的ClassLoader能够成功加载并创建TargetActivity的实例。但是在实际的插件系统中,要启动的目标Activity肯定存在于一个单独的apk中,系统默认的ClassLoader无法加载插件中的Activity类——系统压根儿就不知道要加载的插件在哪,谈何加载?因此还有一个很重要的问题需要处理:插件系统中类的加载,解决了『启动没有在AndroidManifest.xml中显式声明的,并且存在于外部文件中的Activity』的问题,插件系统对于Activity的管理才算得上是一个完全体。
不知道有没有时间填这个坑,希望step 2不会等太久。
最后, 同学点个赞吧!!! 加个关注好么
参考文章
Android插件化原理解析