Hook一個Activity的啟動過程

在前一篇文章Activity啟動過程分析中葛峻,通過源碼分析的方式介紹了Activity的大致啟動過程膨报。今天就來實(shí)戰(zhàn)一下此蜈,一個是加深對Activity啟動過程的理解,另外一個就是讓我們知道閱讀源碼有助于拓寬視野袄秩,提升開發(fā)能力阵翎。

首先先拋出需求:

  1. 我們想啟動一個Activity A頁面逢并,但是想要進(jìn)入這個A頁面必須是已經(jīng)登錄過的,如果沒有登錄的話就啟動登錄頁面B郭卫,并且在B頁面登錄成功之后需要跳轉(zhuǎn)到頁面A

  2. 提升一下難度砍聊,Activity頁面A、B均沒有在清單文件中注冊贰军,但是要完成正常的跳轉(zhuǎn)(這是為插件化的研究做準(zhǔn)備)

在閱讀本文之前玻蝌,可以先clone一份 apk-plugin-technology,參考此項(xiàng)目的binder-hook模塊词疼。運(yùn)行一下Demo俯树,讓你有個更感性的認(rèn)識

Hook技術(shù)

Hook,就是鉤子贰盗,也就是說可以干預(yù)某些代碼的正常執(zhí)行流程许饿。關(guān)于Hook的詳細(xì)介紹,可以自行搜索相關(guān)文章舵盈。

完成Hook過程陋率,需要注意3點(diǎn),也可以說是3個步驟:

  1. 尋找Hook點(diǎn)秽晚,Hook點(diǎn)一般要選擇類的靜態(tài)成員變量瓦糟,因?yàn)殪o態(tài)成員變量一般不容易發(fā)生變化,只需要Hook一次就好了赴蝇。如果想要Hook的成員變量不是靜態(tài)的菩浙,那么可以找這個變量所持有的引用中是否有靜態(tài)的成員變量。而且最好是public的扯再,public的一般不容易發(fā)生變化芍耘。如果是非public的,就要考慮適配問題了熄阻。

  2. 選擇合適的代理方式,一般來說我們不可能Hook一個對象的所有方法倔约,所以就要通過代理的方式來Hook秃殉,如果是想要Hook的方法,就要走我們自己的邏輯浸剩,如果是不需要Hook的方法钾军,還是要調(diào)用原對象的方法。

  3. 用代理對象替換原始對象

這個過程可能還有點(diǎn)不是很清晰绢要,沒關(guān)系吏恭,繼續(xù)往下看就明白了。

Activity啟動過程尋找Hook點(diǎn)

我們知道重罪,啟動一個Activity樱哼,可以通過Activity本身的startActivity方法哀九,也可以調(diào)用Context的startActivity方法,雖然Activity也是繼承自Context搅幅,但是Activity重寫了相關(guān)的啟動方法阅束,這是因?yàn)锳ctivity本身有Activity任務(wù)棧,而其它的Context茄唐,比如Service中并沒有任務(wù)棧息裸,所以啟動的時候需要加上一個flag Intent.FLAG_ACTIVITY_NEW_TASK,Context是一個抽象類沪编,其啟動Activity的方法呼盆,實(shí)際上調(diào)用的是ContextImpl的startActivity方法,例如:

@Override
public void startActivity(Intent intent, Bundle options) {
    warnIfCallingFromSystemProcess();

    // Calling start activity from outside an activity without FLAG_ACTIVITY_NEW_TASK is
    // generally not allowed, except if the caller specifies the task id the activity should
    // be launched in.
    if ((intent.getFlags()&Intent.FLAG_ACTIVITY_NEW_TASK) == 0
            && options != null && ActivityOptions.fromBundle(options).getLaunchTaskId() == -1) {
        throw new AndroidRuntimeException(
                "Calling startActivity() from outside of an Activity "
                + " context requires the FLAG_ACTIVITY_NEW_TASK flag."
                + " Is this really what you want?");
    }
    mMainThread.getInstrumentation().execStartActivity(
            getOuterContext(), mMainThread.getApplicationThread(), null,
            (Activity) null, intent, -1, options);
}

可以看到蚁廓,如果不加FLAG_ACTIVITY_NEW_TASK標(biāo)記的話會拋出異常宿亡。
另外,從這個方法結(jié)合我們之前Activity啟動過程分析中所分析的纳令,不管是通過Activity調(diào)用還是通過Context調(diào)用挽荠,最終調(diào)用的均是Instrumentation的execStartActivity方法。

public ActivityResult execStartActivity(
        Context who, IBinder contextThread, IBinder token, Activity target,
        Intent intent, int requestCode, Bundle options) {
   ...
    try {
      ... 
        int result = ActivityManagerNative.getDefault()
                .startActivity(whoThread, who.getBasePackageName(), intent,
                        intent.resolveTypeIfNeeded(who.getContentResolver()),
                        token, target != null ? target.mEmbeddedID : null,
                        requestCode, 0, null, options);
        ...
    } catch (RemoteException e) {
        throw new RuntimeException("Failure from system", e);
    }
    return null;
}

execStartActivity中會調(diào)用ActivityManagerNative.getDefault()方法平绩,

static public IActivityManager getDefault() {
    return gDefault.get();
}

private static final Singleton<IActivityManager> gDefault = new Singleton<IActivityManager>() {
    protected IActivityManager create() {
        IBinder b = ServiceManager.getService("activity");
        if (false) {
            Log.v("ActivityManager", "default service binder = " + b);
        }
        IActivityManager am = asInterface(b);
        if (false) {
            Log.v("ActivityManager", "default service = " + am);
        }
        return am;
    }
};

static public IActivityManager asInterface(IBinder obj) {
    if (obj == null) {
        return null;
    }
    IActivityManager in =
            (IActivityManager)obj.queryLocalInterface(descriptor);
    if (in != null) {
        return in;
    }

    return new ActivityManagerProxy(obj);
}

ActivityManagerNative的gDefault 是一個靜態(tài)變量圈匆,它是Singleton的一個匿名類,Singleton類其實(shí)就是用于獲取單例對象的捏雌,gDefault的get方法獲取的是IActivityManager的一個實(shí)現(xiàn)類跃赚。知道Binder的應(yīng)該知道,這個獲取的實(shí)際上是ActivityManagerProxy對象性湿,如果不明白的建議先去看看 Binder學(xué)習(xí)概要這篇文章纬傲。

ActivityManagerProxy對應(yīng)的server端就是ActivityManagerService,也就是真正負(fù)責(zé)管理啟動Activity的地方肤频。我們啟動一個Activity就是調(diào)用的ActivityManagerService的startActivity方法叹括。

那么我們想一想,是否可以在gDefault這個方法做點(diǎn)文章呢宵荒。我們推理一下這個邏輯:

  1. hook 的是Activity的啟動過程
  2. ActivityManagerService負(fù)責(zé)管理啟動汁雷,但是它在Server端,我們拿不到报咳,但是通過gDefault我們可以拿到它在本地的代理對象ActivityManagerProxy對象侠讯。
  3. 我們需要為這個ActivityManagerProxy對象創(chuàng)建一個代理對象,當(dāng)它調(diào)用startActivity方法的時候暑刃,需要做一些處理厢漩,比如按照需求1,判斷被跳轉(zhuǎn)的頁面是否需要登錄岩臣,如果需要登錄的話就更改這個Intent溜嗜,跳轉(zhuǎn)到登錄頁面宵膨。當(dāng)調(diào)用其它的方法的時候,直接使用原始的ActivityManagerProxy對象去處理粱胜。

講到這柄驻,其實(shí)我們就可以解決需求1了。但是我想把需求2一起解決了焙压,這樣的話上面的邏輯就有點(diǎn)不夠完善鸿脓,畢竟我們所要啟動的Activity A和登錄頁面B都是沒有在AndroidManifest.xml中聲明的,啟動一個未聲明的Activity肯定會報一個ActivityNotFoundException涯曲。

再來回想一下Activity的啟動過程:

調(diào)用startActivity方法啟動一個目標(biāo)Activity的時候野哭,實(shí)際上會通過Instrumentation進(jìn)行啟動,再通過ActivityManagerService的本地代理對象調(diào)用ActivityManagerService的方法來啟動一個Activity幻件,這是一個IPC過程拨黔,在ActivityManagerService中會校驗(yàn)被啟動的Activity的合法性,如果合法绰沥,會通過IPC過程調(diào)用ApplicationThread的方法篱蝇,進(jìn)而調(diào)用ActivityThread的handleLaunchActivity方法創(chuàng)建Activity,并執(zhí)行Activity的生命周期方法徽曲。

看到?jīng)]零截,Activity的啟動過程是分兩步的

  • ActivityManagerService去校驗(yàn)被啟動Activity合法性,并做好啟動Activity的必要準(zhǔn)備
  • 在ActivityThread中真正的創(chuàng)建Activity秃臣,并完成Activity的啟動階段的生命周期回調(diào)涧衙。

既然沒法辦通過AMS去啟動一個未注冊的Activity,那么我們換一個思路來:

  1. 我們找一個在AndroidManifest.xml中聲明一個代理頁面ProxyActivity奥此,當(dāng)發(fā)起請求A頁面的時候弧哎,我們hook ActivityManagerProxy的startActivity方法,把A頁面的Intent替換為請求啟動ProxyActivity頁面的Intent稚虎,這樣的話至少可以通過ActivityManagerService校驗(yàn)這一關(guān)撤嫩。
  2. 當(dāng)調(diào)用回我們ActivityThread的內(nèi)部中的時候,做一下處理祥绞,把代理頁面ProxyActivity對應(yīng)的Intent替換成我們想要啟動的Activity A對應(yīng)的Intent非洲,當(dāng)然了,在這一過程還需要判斷是否需要登錄蜕径,如果需要登錄的話,就需要替換成B頁面败京。示例代碼如下:
private void hookActivityManagerApi25() {
    try {
        // 反射獲取ActivityManagerNative的靜態(tài)成員變量gDefault, 注意兜喻,在8.0的時候這個已經(jīng)更改了
        Class<?> activityManagerNativeClass = Class.forName("android.app.ActivityManagerNative");
        Field gDefaultField = activityManagerNativeClass.getDeclaredField("gDefault");
        gDefaultField.setAccessible(true);
        Object gDefaultObj = gDefaultField.get(null);
        
        // 我們在這里拿到的instanceObj對象一定不為空,如果為空的話就沒辦法使用
        Class<?> singletonClass = Class.forName("android.util.Singleton");
        Field mInstanceField = singletonClass.getDeclaredField("mInstance");
        mInstanceField.setAccessible(true);

        Object instanceObj = mInstanceField.get(gDefaultObj);

        // 需要動態(tài)代理IActivityManager赡麦,把Singleton的成員變量mInstance的值設(shè)置為我們的這個動態(tài)代理對象
        // 但是有一點(diǎn)朴皆,我們不可能完全重寫一個IActivityManager的實(shí)現(xiàn)類
        // 所以還是需要用到原始的IActivityManager對象帕识,只是在調(diào)用某些方法的時候做一些手腳
        Class<?> iActivityManagerClass = Class.forName("android.app.IActivityManager");
        InterceptInvocationHandler interceptInvocationHandler = new InterceptInvocationHandler(instanceObj);
        Object iActivityManagerObj = Proxy.newProxyInstance(context.getClassLoader(), new Class[]{iActivityManagerClass}, interceptInvocationHandler);
        mInstanceField.set(gDefaultObj, iActivityManagerObj);
    } catch (Exception e) {
        e.printStackTrace();
    }
}

class InterceptInvocationHandler implements InvocationHandler {
    Object originalObject;
    public InterceptInvocationHandler(Object originalObject) {
        this.originalObject = originalObject;
    }

    @Override
    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
        LogUtils.d("method:" + method.getName() + " called with args:" + Arrays.toString(args));
        //如果是startActivity方法,需要做一些手腳
        if (METHOD_START_ACTIVITY.equals(method.getName())) {
            Intent newIntent = null;
            int index = 0;
            for (int i = 0; i < args.length; i++) {
                Object arg = args[i];
                if (arg instanceof Intent) {
                    Intent wantedIntent = (Intent) arg;
                    // 加入目標(biāo)Activity沒有在清單文件中注冊遂铡,我們就欺騙ActivityManagerService肮疗,啟動一個代理頁面
                    // 真正啟動頁面,會開始回調(diào)ActivityThread的handleLaunchActivity方法
                    // 調(diào)用這個方法前可以做點(diǎn)文章扒接,啟動我們想要啟動的頁面
                    newIntent = new Intent();
                    ComponentName componentName = new ComponentName(context, ProxyActivity.class);
                    newIntent.setComponent(componentName);
                    //把原始的跳轉(zhuǎn)信息當(dāng)作參數(shù)攜帶給代理類
                    newIntent.putExtra(EXTRA_REAL_WANTED_INTENT, wantedIntent);
                    index = i;
                }
            }
            args[index] = newIntent;
        }
        return method.invoke(originalObject, args);
    }
}

那么在ActivityThread中怎么找Hook點(diǎn)呢伪货?
首先要明確一點(diǎn),我們找找個Hook點(diǎn)是要為了替換之前代理ProxyActivity的Intent的钾怔,有了找個思路碱呼,我們就可以有目的的去尋找了。

AMS啟動一個Activity宗侦,會調(diào)用ApplicationThread的scheduleLaunchActivity方法愚臀,這個方法應(yīng)該是在Activity啟動過程中我們的App最先被AMS調(diào)用的了,在這個方法中第一個參數(shù)就是Intent矾利,這個Intent就是我們發(fā)起請求啟動ProxyActivity的Intent姑裂。

@Override
public final void scheduleLaunchActivity(Intent intent, IBinder token, int ident,
                                         ActivityInfo info, Configuration curConfig, Configuration overrideConfig,
                                         CompatibilityInfo compatInfo, String referrer, IVoiceInteractor voiceInteractor,
                                         int procState, Bundle state, PersistableBundle persistentState,
                                         List<ResultInfo> pendingResults, List<ReferrerIntent> pendingNewIntents,
                                         boolean notResumed, boolean isForward, ProfilerInfo profilerInfo) {

    updateProcessState(procState, false);

    ActivityClientRecord r = new ActivityClientRecord();

    r.token = token;
    r.ident = ident;
// 注意這個參數(shù)
    r.intent = intent;
  ...

    sendMessage(H.LAUNCH_ACTIVITY, r);
}

ApplicationThread繼承自ApplicationThreadNative,而ApplicationThreadNative繼承自Binder并實(shí)現(xiàn)了IApplicationThread接口男旗。我們考慮一下舶斧,能否像Hook ActivityManagerProxy那樣,采用一個動態(tài)代理的方式剑肯,創(chuàng)建IApplicationThread的代理類捧毛,當(dāng)調(diào)用IApplicationThread的scheduleLaunchActivity方法的時候,我們更改這個方法的Intent參數(shù)让网,變?yōu)槲覀兿胍哪莻€Intent呀忧,然后就可以按照我們的需求來跳轉(zhuǎn)了。

private class ApplicationThread extends ApplicationThreadNative

public abstract class ApplicationThreadNative extends Binder
        implements IApplicationThread

想法是很好的溃睹,但是很遺憾而账,我們做不到,至于為什么因篇,請接著往下看泞辐。

我們想要Hook ApplicationThread的scheduleLaunchActivity,那么我們先看一下這個ApplicationThread對象是什么時候創(chuàng)建的竞滓。ApplicationThread是ActivityThread的非靜態(tài)內(nèi)部類咐吼,在ActivityThread中,它的創(chuàng)建時機(jī)是在ActivityThread對象初始化的時候商佑,

 final ApplicationThread mAppThread = new ApplicationThread();

由于它沒有采用多態(tài)的方式來創(chuàng)建ApplicationThread锯茄,我們創(chuàng)建的動態(tài)代理對象實(shí)際上是沒有辦法賦值給mAppThread這個變量的,也就是說實(shí)際上這個點(diǎn)我們是沒有辦法hook的。

那么我們接著這個方法來看肌幽,在scheduleLaunchActivity方法中晚碾,通過ActivityThread的一個H類型的成員mH來發(fā)送一個類型為 H.LAUNCH_ACTIVITY (int 型,值為100)的消息喂急,這個H是ActivityThread的非靜態(tài)內(nèi)部類格嘁,實(shí)際上是繼承自Handler的。

private class H extends Handler

至于為什么需要用Handler來切換廊移,在Binder學(xué)習(xí)概要
已經(jīng)介紹過糕簿,因?yàn)閟cheduleLaunchActivity是在Binder線程池中被調(diào)用的,需要用Hander來切換到主線程画机。H.LAUNCH_ACTIVITY類型的消息發(fā)送之后冶伞,H的handleMessage方法會被調(diào)用,在這里就會根據(jù)msg.what的來處理步氏,對應(yīng)LAUNCH_ACTIVITY類型的响禽,會調(diào)用ActivityThread的handleLaunchActivity來創(chuàng)建Activity并完成Activity啟動過程的生命周期回調(diào)榛丢。

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");
            final ActivityClientRecord r = (ActivityClientRecord) msg.obj;

            r.packageInfo = getPackageInfoNoCheck(
                    r.activityInfo.applicationInfo, r.compatInfo);
// 調(diào)用ActivityThread的方法來啟動Activity
            handleLaunchActivity(r, null, "LAUNCH_ACTIVITY");
            Trace.traceEnd(Trace.TRACE_TAG_ACTIVITY_MANAGER);
        } break;
        ....
    }
   ...
}

再往下就要走到ActivityThread的handleLaunchActivity方法中了肪康,難道我們要去Hook ActivityThread的handleLaunchActivity方法?

首先错忱,獲取這個ActivityThread對象是沒有難度的界阁,ActivityThread對象可以在它的類成員變量sCurrentActivityThread獲取侯繁。

private static volatile ActivityThread sCurrentActivityThread;

應(yīng)用的啟動入口是ActivityThread的main方法,在這個方法中會創(chuàng)建ActivityThread對象泡躯,接著又會調(diào)用它的attach方法贮竟,在這個方法中,把ActivityThread對象賦值給其類的靜態(tài)成員變量sCurrentActivityThread较剃。靜態(tài)成員變量就很好說了咕别,通過反射就可以獲取這個對象。

public static void main(String[] args) {
 ...
    ActivityThread thread = new ActivityThread();
    thread.attach(false);
 ...
}

private void attach(boolean system) {
    sCurrentActivityThread = this;
    ...
}

雖然獲取了這個ActivityThread對象写穴,但是我們怎么準(zhǔn)備一個代理對象來代理ActivityThread對象呢惰拱?

由于ActivityThread沒有繼承或?qū)崿F(xiàn)任何類或接口,好像為它準(zhǔn)備代理對象有點(diǎn)難度啊送。

public final class ActivityThread

難道沒有辦法了嗎偿短?
當(dāng)然不是,否則我還寫這篇文章干嘛馋没?

想一想Handler那塊昔逗,從發(fā)送消息到處理消息,實(shí)際上中間是有一個消息分發(fā)過程的篷朵,也就是Handler的dispatchMessage方法會被調(diào)用纤子,在這個方法中實(shí)際上Handler本身的handleMessage方法是最后才可能會被調(diào)用到的。msg.callback 這塊我們沒辦法處理款票,因?yàn)橄?chuàng)建是我們控制不了控硼,而在else中,mCallback != null 這塊艾少,我們似乎可以給Hander設(shè)置一個mCallback卡乾,由這個Callback先一步處理消息,替換Intent缚够。

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

我們看一下mCallBack的類型幔妨,它是Handler內(nèi)部的一個接口。

public interface Callback {
    public boolean handleMessage(Message msg);
}

那我們趕緊去找找怎么設(shè)置Callback谍椅,不過很遺憾的是設(shè)置Callback只有一種方式就是作為Handler構(gòu)造方法參數(shù)傳遞误堡,但我們的mH對象已經(jīng)創(chuàng)建了。既然正常的路徑?jīng)]辦法了雏吭,那只要采用反射的方式來設(shè)置成員變量了锁施。

private void hookActivityThreadHandler() {
    //需要hook ActivityThread
    try {
        //獲取ActivityThread的成員變量 sCurrentActivityThread
        Class<?> activityThreadClass = Class.forName("android.app.ActivityThread");
        Field sCurrentActivityThread = activityThreadClass.getDeclaredField("sCurrentActivityThread");
        sCurrentActivityThread.setAccessible(true);
        Object activityThreadObj = sCurrentActivityThread.get(null);

        //獲取ActivityThread的成員變量 mH
        Field mHField = activityThreadClass.getDeclaredField("mH");
        mHField.setAccessible(true);
        Handler mHObj = (Handler) mHField.get(activityThreadObj);

        Field mCallbackField = Handler.class.getDeclaredField("mCallback");
        mCallbackField.setAccessible(true);
        mCallbackField.set(mHObj, new ActivityCallback(mHObj));

    } catch (Exception e) {
        e.printStackTrace();
    }
}

private class ActivityCallback implements Handler.Callback {
    private Handler mH;
    public ActivityCallback(Handler mH) {
        this.mH = mH;
    }

    @Override
    public boolean handleMessage(Message msg) {
        if (msg.what == LAUNCH_ACTIVITY) {
            handleLaunchActivity(msg);
        }
        return false;
    }

    private void handleLaunchActivity(Message msg) {
        //替換我們真正想要的intent
        try {
            Object activityClientRecord = msg.obj;
            Field intentField = activityClientRecord.getClass().getDeclaredField("intent");
            intentField.setAccessible(true);
            //這個是代理ProxyActivity
            Intent interceptedIntent = (Intent) intentField.get(activityClientRecord);

            //真正想要跳轉(zhuǎn)的 SecondActivity
            Intent realWanted = interceptedIntent.getParcelableExtra(EXTRA_REAL_WANTED_INTENT);
            if (realWanted != null) {
                //如果不需要登錄
                Class<?> real = Class.forName(realWanted.getComponent().getClassName());
                NeedLogin annotation = real.getAnnotation(NeedLogin.class);

                if (annotation != null && !SPHelper.getBoolean("login", false)) {
                    //如果需要登錄并且沒有登錄,跳轉(zhuǎn)登錄頁面
                    Intent loginIntent = new Intent(context, LoginActivity.class);
                    loginIntent.putExtra(EXTRA_REAL_WANTED_INTENT, realWanted);
                    interceptedIntent.setComponent(loginIntent.getComponent());
                } else {
                    interceptedIntent.setComponent(realWanted.getComponent());
                }
            }
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}

到此杖们,就可以通過Hook的方式來啟動一個未在AndroidManifest.xml聲明的Activity了悉抵,并且可以根據(jù)是否需要登錄來跳轉(zhuǎn)到不同的頁面。

繼承AppCompatActivity會遇到的問題

如果你所有的Activity均繼承的是Activity摘完,上面的代碼邏輯已經(jīng)是沒有問題了姥饰。但是,如果你的Activity類繼承的是AppCompatActivity孝治,是會報一個異常:

android.content.pm.PackageManager$NameNotFoundException

報這個異常的原因是在AppCompatActivity的onCreate方法中經(jīng)過層層調(diào)用列粪,會調(diào)用到NavUtils的getParentActivityName方法。在這個方法中會調(diào)用到PackageManager的getActivityInfo方法谈飒,返回的ActivityInfo對象是Activity在AndroidManifest.xml中注冊信息對應(yīng)的一個JavaBean對象岂座,調(diào)用這個方法實(shí)際上會再檢查一次Activity的合法性。

# android.support.v4.app.NavUtils
public static String getParentActivityName(Context context, ComponentName componentName)
        throws NameNotFoundException {
    PackageManager pm = context.getPackageManager();
    ActivityInfo info = pm.getActivityInfo(componentName, PackageManager.GET_META_DATA);
    if (Build.VERSION.SDK_INT >= 16) {
        String result = info.parentActivityName;
        if (result != null) {
            return result;
        }
    }
    if (info.metaData == null) {
        return null;
    }
    String parentActivity = info.metaData.getString(PARENT_ACTIVITY);
    if (parentActivity == null) {
        return null;
    }
    if (parentActivity.charAt(0) == '.') {
        parentActivity = context.getPackageName() + parentActivity;
    }
    return parentActivity;
}

在上面方法中步绸,context.getPackageManager()實(shí)際上會調(diào)用ContextImpl的getPackageManager方法掺逼,而這個實(shí)際上返回的是ApplicationPackageManager對象,這個類是把IPackageManager進(jìn)行了包裝瓤介,實(shí)際上的功能還是由PackageManagerService調(diào)用吕喘。

# android.app.ContextImpl
public PackageManager getPackageManager() {
    if (mPackageManager != null) {
        return mPackageManager;
    }

    IPackageManager pm = ActivityThread.getPackageManager();
    if (pm != null) {
        // Doesn't matter if we make more than one instance.
        return (mPackageManager = new ApplicationPackageManager(this, pm));
    }

    return null;
}

而ApplicationPackageManager 調(diào)用getActivityInfo實(shí)際上調(diào)用的IPackageManagerd的getActivityInfo方法

# android.app.ApplicationPackageManager
@Override
public ActivityInfo getActivityInfo(ComponentName className, int flags)
        throws NameNotFoundException {
    try {
        ActivityInfo ai = mPM.getActivityInfo(className, flags, mContext.getUserId());
        if (ai != null) {
            return ai;
        }
    } catch (RemoteException e) {
        throw e.rethrowFromSystemServer();
    }

    throw new NameNotFoundException(className.toString());
}

這個過程就是經(jīng)過一個IPC過程調(diào)用PackageManagerService的getActivityInfo方法。

為了不報NameNotFoundException異常刑桑,我們需要Hook這個IPackageManager氯质,當(dāng)調(diào)用PackageManager的getActivityInfo的時候,不讓它進(jìn)行IPC調(diào)用祠斧,而是直接返回一個不為null的ActivityInfo對象闻察,這樣就可以解決問題了。

Hook PMS

我們要去Hook PMS,還是遵循之前講的3個步驟辕漂,那么就先來找這個Hook點(diǎn)呢灶,上面我們在貼出的代碼中也看到了,ContextImpl的getPackageManager方法中首先會獲取調(diào)用ActivityThread的靜態(tài)方法getPackageManager來獲取一個IPackageManager對象钉嘹。我們來看一下這個方法鸯乃。

# android.app.ActivityThread
public static IPackageManager getPackageManager() {
    if (sPackageManager != null) {
        //Slog.v("PackageManager", "returning cur default = " + sPackageManager);
        return sPackageManager;
    }
    IBinder b = ServiceManager.getService("package");
    //Slog.v("PackageManager", "default service binder = " + b);
    sPackageManager = IPackageManager.Stub.asInterface(b);
    //Slog.v("PackageManager", "default service = " + sPackageManager);
    return sPackageManager;
}

在這里有一個靜態(tài)變量sPackageManager,如果不為空的話直接就返回了跋涣,這個靜態(tài)變量的類型是接口類型缨睡,那么這個Hook點(diǎn)就很好,靜態(tài)的很好獲取對象陈辱,而接口類型更容易使用代理奖年。

static volatile IPackageManager sPackageManager;

前面講了那么多,怎么去Hook應(yīng)該也知道 了沛贪,我們目前只Hook IPackageManager的getActivityInfo方法陋守,廢話也不多說了,直接貼代碼鹏浅,更直觀嗅义。

private void HookPackageManager() {
    //需要hook ActivityThread
    try {
        //獲取ActivityThread的成員變量 sCurrentActivityThread
        Class<?> activityThreadClass = Class.forName("android.app.ActivityThread");
        Field sPackageManagerField = activityThreadClass.getDeclaredField("sPackageManager");
        sPackageManagerField.setAccessible(true);
        Object iPackageManagerObj = sPackageManagerField.get(null);


        Class<?> iPackageManagerClass = Class.forName("android.content.pm.IPackageManager");
        InterceptPackageManagerHandler interceptInvocationHandler = new InterceptPackageManagerHandler(iPackageManagerObj);
        Object iPackageManagerObjProxy = Proxy.newProxyInstance(context.getClassLoader(), new Class[]{iPackageManagerClass}, interceptInvocationHandler);

        sPackageManagerField.set(null, iPackageManagerObjProxy);

    } catch (Exception e) {
        e.printStackTrace();
    }
}

private class InterceptPackageManagerHandler implements InvocationHandler {
    Object originalObject;

    public InterceptPackageManagerHandler(Object originalObject) {
        this.originalObject = originalObject;
    }

    @Override
    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
        LogUtils.d("method:" + method.getName() + " called with args:" + Arrays.toString(args));
        if (METHOD_GET_ACTIVITY_INFO.equals(method.getName())) {
            return new ActivityInfo();
        }
        return method.invoke(originalObject, args);
    }
}

AMS與ActivityThread通過token進(jìn)行通信

雖然我們啟動了Activity A或者B,但是AMS實(shí)際上還是以為我們啟動的是ProxyActivity隐砸。不信的話之碗,可以使用命令行查看。

adb shell dumpsys activity activities | grep mFocusedActivity

結(jié)果如下季希,可以看到褪那,在AMS端記錄的Activity實(shí)際上是ProxyActivity。

mFocusedActivity: ActivityRecord{4ff4194 u0 com.sososeen09.binder.hook/.ProxyActivity t5057}

那么通過這種Hook方式啟動的Activity 式塌,還具有完整的生命周期嗎博敬?

答案是肯定的。

我們知道Activity是不具有跨進(jìn)程通訊的能力的峰尝,那么AMS是如何管理Activity偏窝,控制Activity的聲明周期的呢?答案就是一個通過一個Ibinder類型的token變量來控制武学。ASM通過這個token來與ApplicationThread進(jìn)行通訊祭往,進(jìn)行控制Activity的聲明周期。在AMS那邊火窒,它以為token表示的是ProxyActivity硼补,但是在客戶端這邊,token實(shí)際上指的是Activity A或者B熏矿。

這個token是在AMS在回到IApplicationThread的scheduleLaunchActivity方法中傳遞過來的第二個參數(shù)已骇。

public final void scheduleLaunchActivity(Intent intent, IBinder token, int ident,
                                         ActivityInfo info, Configuration curConfig, Configuration overrideConfig,
                                         CompatibilityInfo compatInfo, String referrer, IVoiceInteractor voiceInteractor,
                                         int procState, Bundle state, PersistableBundle persistentState,
                                         List<ResultInfo> pendingResults, List<ReferrerIntent> pendingNewIntents,
                                         boolean notResumed, boolean isForward, ProfilerInfo profilerInfo) {

這個會存在ActivityClientRecord中离钝。這個token和對應(yīng)的ActivityClientRecord會以鍵值對的形式存儲在ActivityThread的變量mActivities中。后面再對Activity進(jìn)行生命周期方法調(diào)用的時候褪储,均可以通過AMS端傳過來的token來獲取正確的Activity卵渴。

final ArrayMap<IBinder, ActivityClientRecord> mActivities = new ArrayMap<>();
AMS與ActivityThread中相同token對應(yīng)不同的Activity.png

總結(jié)

前面講了一大堆,我們現(xiàn)在來總結(jié)概括一下這個過程和原理乱豆。
再來看一下需求:

  1. 我們想啟動一個Activity A頁面奖恰,但是想要進(jìn)入這個A頁面必須是已經(jīng)登錄過的,如果沒有登錄的話就啟動登錄頁面B宛裕,并且在B頁面登錄成功之后需要跳轉(zhuǎn)到頁面A

  2. Activity頁面A、B均沒有在清單文件中注冊论泛,但是要完成正常的跳轉(zhuǎn)

為什么我們可以跳轉(zhuǎn)到一個未在AndroidManifest.xml中聲明的Activity中揩尸,而且可以根據(jù)不同的邏輯跳轉(zhuǎn)到不同的頁面呢?

調(diào)用startActivity方法啟動一個目標(biāo)Activity的時候屁奏,實(shí)際上會通過Instrumentation進(jìn)行啟動岩榆,再通過ActivityManagerService的本地代理對象調(diào)用ActivityManagerService的方法來啟動一個Activity,這是一個IPC過程坟瓢,在ActivityManagerService中會校驗(yàn)被啟動的Activity的合法性勇边,如果合法,會通過IPC過程調(diào)用ApplicationThread的方法折联,ApplicationThread是一個Binder對象粒褒,它的方法運(yùn)行是在Binder線程池中的,所以需要采用一個Handler把方法調(diào)用切換到主線程诚镰,ApplicationThread通過發(fā)送消息奕坟,進(jìn)而調(diào)用ActivityThread的handleLaunchActivity方法創(chuàng)建Activity,并執(zhí)行Activity的生命周期方法清笨。

傳遞到ActivityManagerService的被啟動的Activity信息必須是聲明過的月杉,而如果我們想要啟動一個沒有在AndroidManifest.xml中聲明的Activity,可以通過欺上瞞下的方法抠艾,hook ActivityManagerService在本地的代理對象苛萎,如果調(diào)用的是ActivityManagerProxy的startActivity方法,那么就更改這個Intent检号,替換成啟動一個聲明過的ProxyActivity腌歉,當(dāng)ActivityManagerService校驗(yàn)完啟動的合法性之后,會通過ApplicationThread調(diào)用到ActivityThread的一個叫做mH的Handler中來谨敛。當(dāng)Handler收到消息的時候究履,會有一個消息分發(fā)的過程,如果給Handler設(shè)置了一個Callback脸狸,這個Callback的handleMessage方法就會先于Handler本身的handleMessage方法調(diào)用最仑。所以可以想辦法給這個叫做mH的Handler對象設(shè)置Callback藐俺,并且在Callback的handleMessage方法中從Message上面拿到相關(guān)的Intent信息,此時的Intent還是跳轉(zhuǎn)到代理頁面泥彤,可以根據(jù)當(dāng)前是否登錄欲芹,是否需要重定向到登錄頁面等對這個Intent進(jìn)行相應(yīng)的處理,比如設(shè)置為跳轉(zhuǎn)到登錄頁或者真正想要跳轉(zhuǎn)的頁面吟吝,并由后續(xù)的mH的handleMessage來調(diào)用菱父。

更多細(xì)節(jié),請查看項(xiàng)目 apk-plugin-technology

參考

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末浙宜,一起剝皮案震驚了整個濱河市,隨后出現(xiàn)的幾起案子蛹磺,更是在濱河造成了極大的恐慌粟瞬,老刑警劉巖,帶你破解...
    沈念sama閱讀 217,277評論 6 503
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件萤捆,死亡現(xiàn)場離奇詭異裙品,居然都是意外死亡,警方通過查閱死者的電腦和手機(jī)俗或,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,689評論 3 393
  • 文/潘曉璐 我一進(jìn)店門市怎,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人辛慰,你說我怎么就攤上這事区匠。” “怎么了昆雀?”我有些...
    開封第一講書人閱讀 163,624評論 0 353
  • 文/不壞的土叔 我叫張陵辱志,是天一觀的道長。 經(jīng)常有香客問我狞膘,道長揩懒,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 58,356評論 1 293
  • 正文 為了忘掉前任挽封,我火速辦了婚禮已球,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘辅愿。我一直安慰自己智亮,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,402評論 6 392
  • 文/花漫 我一把揭開白布点待。 她就那樣靜靜地躺著阔蛉,像睡著了一般。 火紅的嫁衣襯著肌膚如雪癞埠。 梳的紋絲不亂的頭發(fā)上状原,一...
    開封第一講書人閱讀 51,292評論 1 301
  • 那天聋呢,我揣著相機(jī)與錄音,去河邊找鬼颠区。 笑死削锰,一個胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的毕莱。 我是一名探鬼主播器贩,決...
    沈念sama閱讀 40,135評論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼朋截!你這毒婦竟也來了蛹稍?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 38,992評論 0 275
  • 序言:老撾萬榮一對情侶失蹤质和,失蹤者是張志新(化名)和其女友劉穎稳摄,沒想到半個月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體饲宿,經(jīng)...
    沈念sama閱讀 45,429評論 1 314
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,636評論 3 334
  • 正文 我和宋清朗相戀三年胆描,在試婚紗的時候發(fā)現(xiàn)自己被綠了瘫想。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 39,785評論 1 348
  • 序言:一個原本活蹦亂跳的男人離奇死亡昌讲,死狀恐怖国夜,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情短绸,我是刑警寧澤车吹,帶...
    沈念sama閱讀 35,492評論 5 345
  • 正文 年R本政府宣布,位于F島的核電站醋闭,受9級特大地震影響窄驹,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜证逻,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,092評論 3 328
  • 文/蒙蒙 一乐埠、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧囚企,春花似錦丈咐、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,723評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至银酗,卻和暖如春辆影,著一層夾襖步出監(jiān)牢的瞬間徒像,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 32,858評論 1 269
  • 我被黑心中介騙來泰國打工秸歧, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留厨姚,地道東北人。 一個月前我還...
    沈念sama閱讀 47,891評論 2 370
  • 正文 我出身青樓键菱,卻偏偏與公主長得像谬墙,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子经备,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 44,713評論 2 354

推薦閱讀更多精彩內(nèi)容