android 插件化之Activity生命周期之二

来源:互联网 发布:unity3d全套视频教程 编辑:程序博客网 时间:2024/06/19 02:18

------本文转载自  Android插件化原理解析——Activity生命周期管理 这一系列的文章实在是写的好!

3.Hook Activity

3.1 简要分析

通过上文的分析,我们已经对Activity的启动过程了如指掌了;就让我们干点坏事吧。

对与『必须在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』

3.2实战过程

具体来说,我们打算实现如下功能:在MainActivity中启动一个并没有在AndroidManifest.xml中声明的TargetActivity;

按照上文分析,我们需要声明一个替身Activity,我们叫它StubActivity;

那么,我们的AndroidManifest.xml如下:

<manifest xmlns:android="http://schemas.android.com/apk/res/android"          package="com.weishu.intercept_activity.app">    <application            android:allowBackup="true"            android:label="@string/app_name"            android:icon="@mipmap/ic_launcher"            >        <activity android:name=".MainActivity">            <intent-filter>                <action android:name="android.intent.action.MAIN"/>                <category android:name="android.intent.category.LAUNCHER"/>            </intent-filter>        </activity>        <!-- 替身Activity, 用来欺骗AMS  -->        <activity android:name=".StubActivity"/>    </application></manifest>

OK,那么我们启动TargetActivity很简单,就是个startActivity调用的事

startActivity(new Intent(MainActivity.this, TargetActivity.class));

如果你直接这么运行,肯定会直接抛出ActivityNotFoundException然后直接退出;

我们接下来要做的就是让这个调用成功启动TargetActivity。

3.3使用替身Activity绕过AMS

由于AMS进程会对Activity做显式声明验证,因此在

启动Activity的控制权转移到AMS进程之前,我们需要想办法临时把TargetActivity

替换成替身StubActivity;在这之间有很长的一段调用链,我们可以轻松Hook掉;选择什么地方

Hook是一个很自由的事情,但是Hook的步骤越后越可靠——Hook得越早,后面的调用就越复杂,越容易出错。

 

我们可以选择在进入AMS进程的入口进行Hook,具体来说也就是Hook AMS在本进程的代理对象ActivityManagerNative。

我们Hook掉ActivityManagerNative对于startActivity方法的调用,替换掉交给AMS的intent对象,

将里面的TargetActivity的暂时替换成已经声明好的替身StubActivity;这种Hook方式 前文 讲述的很详细,不赘述;替换的关键代码如下:

if ("startActivity".equals(method.getName())) {    // 只拦截这个方法    // 替换参数, 任你所为;甚至替换原始Activity启动别的Activity偷梁换柱    // API 23:    // public final Activity startActivityNow(Activity parent, String id,    // Intent intent, ActivityInfo activityInfo, IBinder token, Bundle state,    // Activity.NonConfigurationInstances lastNonConfigurationInstances) {    // 找到参数里面的第一个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.weishu.intercept_activity.app";    // 这里我们把启动的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);
通过这个替换过程,在ActivityManagerNative的startActivity调用之后,system_server端收到Binder驱动的消息,

开始执行ActivityManagerService里面真正的startActivity方法;

这时候AMS看到的intent参数里面的组件已经是StubActivity了,因此可以成功绕过检查,这时候如果不做后面的Hook,直接调用

startActivity(new Intent(MainActivity.this, TargetActivity.class));

3.4拦截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如下:

public void dispatchMessage(Message msg) {    if (msg.callback != null) {        handleCallback(msg);    } else {        if (mCallback != null) {            if (mCallback.handleMessage(msg)) {                return;            }        }        handleMessage(msg);    }}

从这个方法可以看出来,Handler类消息分发的过程如下:

1,如果传递的Message本身就有callback,那么直接使用Message对象的callback方法;

2,如果Handler类的成员变量mCallback存在,那么首先执行这个mCallback回调;

3,如果mCallback的回调返回true,那么表示消息已经成功处理;直接结束。

4,如果mCallback的回调返回false,那么表示消息没有处理完毕,会继续使用Handler类的handleMessage方法处理消息。

那么,ActivityThread中的Handler类H是如何实现的呢?H的部分源码如下:

public void handleMessage(Message msg) {    if (DEBUG_MESSAGES) Slog.v(TAG, ">>> handling: " + codeToString(msg.what));    switch (msg.what) {        case LAUNCH_ACTIVITY: {            Trace.traceBegin(Trace.TRACE_TAG_ACTIVITY_MANAGER, "activityStart");            ActivityClientRecord r = (ActivityClientRecord)msg.obj;            r.packageInfo = getPackageInfoNoCheck(                    r.activityInfo.applicationInfo, r.compatInfo);            handleLaunchActivity(r, null);            Trace.traceEnd(Trace.TRACE_TAG_ACTIVITY_MANAGER);        } break;        case RELAUNCH_ACTIVITY: {            Trace.traceBegin(Trace.TRACE_TAG_ACTIVITY_MANAGER, "activityRestart");            ActivityClientRecord r = (ActivityClientRecord)msg.obj;            handleRelaunchActivity(r);            Trace.traceEnd(Trace.TRACE_TAG_ACTIVITY_MANAGER);// 以下略}}

可以看到H类仅仅重载了handleMessage方法;通过dispathMessage的消息分发过程得知,

我们可以拦截这一过程:把这个H类的mCallback替换为我们的自定义实现,

这样dispathMessage就会首先使用这个自定义的mCallback,然后看情况使用H重载的handleMessage。

这个Handler.Callback是一个接口,我们可以使用动态代理或者普通代理完成Hook,

这里我们使用普通的静态代理方式;创建一个自定义的Callback类:

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类的对象:

// 先获取到当前的ActivityThread对象Class<?> activityThreadClass = Class.forName("android.app.ActivityThread");Field currentActivityThreadField = activityThreadClass.getDeclaredField("sCurrentActivityThread");currentActivityThreadField.setAccessible(true);Object currentActivityThread = currentActivityThreadField.get(null);// 由于ActivityThread一个进程只有一个,我们获取这个对象的mHField 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));

到这里,我们已经成功地绕过AMS,完成了『启动没有在AndroidManifest.xml中显式声明的Activity』的过程;

瞒天过海,这种玩弄系统与股掌之中的快感你们能体会到吗?

3.5 僵尸or活人?——能正确收到生命周期回调吗

虽然我们完成了『启动没有在AndroidManifest.xml中显式声明的Activity 』,

但是启动的TargetActivity是否有自己的生命周期呢,我们还需要额外的处理过程吗?

实际上TargetActivity已经是一个有血有肉的Activity了:它具有自己正常的生命周期;

这个过程是如何完成的呢?我们以onDestroy为例简要分析一下:

从Activity的finish方法开始跟踪,最终会通过ActivityManagerNative到AMS然后接着通过

ApplicationThread到ActivityThread,然后通过H转发消息到ActivityThread的handleDestroyActivity,

接着这个方法把任务交给performDestroyActivity完成。

在真正分析这个方法之前,需要说明一点的是:不知读者是否感受得到,App进程与AMS交互几乎

都是这么一种模式,几个角色ActivityManagerNative, ApplicationThread, ActivityThread以及Handler类H分工明确,

读者可以按照这几个角色的功能分析AMS的任何调用过程,屡试不爽;

这也是我的初衷——希望分析插件框架的过程中能帮助深入理解Android Framework。

好了继续分析performDestroyActivity,关键代码如下:

ActivityClientRecord r = mActivities.get(token);// ...略mInstrumentation.callActivityOnDestroy(r.activity);

这里通过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有它自己完整而独立的生命周期!

4 小节

本文讲述了『启动一个并没有在AndroidManifest.xml中显示声明的Activity』的解决办法,

我们成功地绕过了Android的这个限制,这个是插件Activity管理技术的基础;

但是要做到启动一个插件Activity问题远没有这么简单。

首先,在Android中,Activity有不同的启动模式;我们声明了一个替身StubActivity,

肯定没有满足所有的要求;因此,我们需要在AndroidManifest.xml中声明一系列的有不同launchMode的Activity,

还需要完成替身与真正Activity launchMode的匹配过程;这样才能完成启动各种类型Activity的需求,

关于这一点,在 DroidPlugin 的com.morgoo.droidplugin.stub包下面可以找到。

另外,每启动一个插件的Activity都需要一个StubActivity,但是AndroidManifest.xml中肯定只能声明有限个,

如果一直startActivity而不finish的话,那么理论上就需要无限个StubActivity;这个问题该如何解决呢?

事实上,这个问题在技术上没有好的解决办法。但是,如果你的App startActivity了十几次,

而没有finish任何一个Activity,这样在Activity的回退栈里面有十几个Activity,用户难道按back十几次回到主页吗?

有这种需求说明你的产品设计有问题;一个App一级页面,二级页面..到五六级的页面已经影响体验了,

所以,每种LauchMode声明十个StubActivity绝对能满足需求了。

最后,在本文所述例子中,TargetActivity与StubActivity存在于同一个Apk,因此系统的ClassLoader

能够成功加载并创建TargetActivity的实例。但是在实际的插件系统中,要启动的目标Activity肯定存在于一个单独的文件中,

系统默认的ClassLoader无法加载插件中的Activity类——系统压根儿就不知道要加载的插件在哪,谈何加载?因此还有一个很重要的问题需要处理:

我们要完成插件系统中类的加载,这可以通过自定义ClassLoader实现。

解决了『启动没有在AndroidManifest.xml中显式声明的,并且存在于外部文件中的Activity』的问题,插件系统对于Activity的管理才算得上是一个完全体。

0 0
原创粉丝点击
热门问题 老师的惩罚 人脸识别 我在镇武司摸鱼那些年 重生之率土为王 我在大康的咸鱼生活 盘龙之生命进化 天生仙种 凡人之先天五行 春回大明朝 姑娘不必设防,我是瞎子 怀孕三个月胎盘低置怎么办 怀孕第一个月打针了怎么办 唐氏筛查神经管缺陷高风险怎么办 门诊处方笺丢了怎么办 孕中期睡觉手麻怎么办 怀孕2个月了没胎心胎芽怎么办 怀孕腿疼的厉害怎么办 孕妇老是失眠多梦怎么办 孕妇会失眠多梦怎么办 怀孕5个月睡不着怎么办 6个月孕妇失眠怎么办 彩超脉络丛囊肿怎么办 双侧脉络丛囊肿怎么办 唐筛神经管缺陷高风险怎么办 雌激素低怎么办吃什么东西补 我怀了狗的孩子怎么办 结婚2年不要孩子怎么办 备孕一直没怀孕怎么办 刚生的婴儿打嗝怎么办 小孩40天黄疸高怎么办 婴儿身高长得慢怎么办 四个月的宝宝哭怎么办 孕39周羊水偏多怎么办 孕39周羊水浑浊怎么办 孕晚期羊水过少怎么办 怀孕脐带绕颈一周怎么办 nt检查宝宝趴着怎么办 四维胎儿有问题怎么办 怀孕70天没有胎心怎么办 怀孕20天不想要怎么办 换轮胎胎压监测怎么办 怀孕了吐的厉害该怎么办 怀孕吐完嗓子疼怎么办 怀孕16周不想要怎么办 怀孕四个月胎盘低置怎么办 孕37周胎盘三级怎么办 孕37周胎盘老化怎么办 怀孕22周胎盘1级怎么办 婴儿吃奶粉吐奶怎么办 宝宝吐奶又拉稀怎么办 羊水穿刺21三体怎么办