Android插件化Step 1 - 插件Activity的启动

首先说明下该文是基于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。


Activity启动过程

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

Activity启动过程-1

这里直接说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

Activity启动过程-2

行百里者半九十。现在我们的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插件化原理解析

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 206,839评论 6 482
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 88,543评论 2 382
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 153,116评论 0 344
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 55,371评论 1 279
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 64,384评论 5 374
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,111评论 1 285
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,416评论 3 400
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,053评论 0 259
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 43,558评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,007评论 2 325
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,117评论 1 334
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,756评论 4 324
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,324评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,315评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,539评论 1 262
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,578评论 2 355
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,877评论 2 345