Android插件化實踐(1)

紙上得來終覺淺,絕知此事要躬行

前言

作為一個android開發者,一定都知道每個activity都需要在AndroidManifest.xml中顯示的聲明一下,否則在啟動的activity的時候就會拋出ActivityNotFoundException的異常。那么真的就沒有辦法去啟動一個沒有聲明的activity嗎?來讓我們從源碼看起。

activity啟動過程

想要知道能不能啟動一個不在manifest中注冊的Activity,我們先來看一下activity啟動的過程,下面以Android7.1.2的代碼為例。
在啟動activity時會調用startActivity,首先我們來看Activity中的startActivity看起,代碼在frameworks/base/core/java/android/app/Activity.java中。

public void startActivity(Intent intent) {
    this.startActivity(intent, null);
}

之后有調用了兩個參數的startActivity(),又經過一連串的調用,調用到了startActivityForResult()

ublic void startActivity(Intent intent, @Nullable Bundle options) {
    if (options != null) {
        startActivityForResult(intent, -1, options);
    } else {
        startActivityForResult(intent, -1);
    }
}

其中真正啟動activity的方法是通過Instrumentation中的execStartActivity()方法啟動的。mInstrumentation是Activity中的一個成員變量,frameworks/base/core/java/android/app/Instrumentation.java

public void startActivityForResult(@RequiresPermission Intent intent, int requestCode,
            @Nullable Bundle options) {
    if (mParent == null) {
        options = transferSpringboardActivityOptions(options);
        Instrumentation.ActivityResult ar =
            mInstrumentation.execStartActivity(
                this, mMainThread.getApplicationThread(), mToken, this,
                intent, requestCode, options);
        if (ar != null) {
            mMainThread.sendActivityResult(
                mToken, mEmbeddedID, requestCode, ar.getResultCode(),
                ar.getResultData());
        }
        if (requestCode >= 0) {
            mStartedActivity = true;
        }

        cancelInputsAndStartExitTransition(options);
    } else {
        if (options != null) {
            mParent.startActivityFromChild(this, intent, requestCode, options);
        } else {
            mParent.startActivityFromChild(this, intent, requestCode);
        }
    }
}

下面是整個流程中比較關鍵的方法

public ActivityResult execStartActivity(
            Context who, IBinder contextThread, IBinder token, Activity target,
            Intent intent, int requestCode, Bundle options) {
    IApplicationThread whoThread = (IApplicationThread) contextThread;
    ...

    try {
        intent.migrateExtraStreamToClipData();
        intent.prepareToLeaveProcess(who);
        int result = ActivityManagerNative.getDefault()
            .startActivity(whoThread, who.getBasePackageName(), intent,
                    intent.resolveTypeIfNeeded(who.getContentResolver()),
                    token, target != null ? target.mEmbeddedID : null,
                    requestCode, 0, null, options);
        checkStartActivityResult(result, intent);
    } catch (RemoteException e) {
        throw new RuntimeException("Failure from system", e);
    }
    return null;
}

先看checkStartActivityResult()這個方法,這里會檢測activity啟動的各種狀態,其中就包括了"have you declared this activity in your AndroidManifest.xml?"沒有在AndroidManifest.xml注冊activity的異常。

public static void checkStartActivityResult(int res, Object intent) {
    if (res >= ActivityManager.START_SUCCESS) {
        return;
    }

    switch (res) {
        case ActivityManager.START_INTENT_NOT_RESOLVED:
        case ActivityManager.START_CLASS_NOT_FOUND:
            if (intent instanceof Intent && ((Intent)intent).getComponent() != null)
                throw new ActivityNotFoundException(
                        "Unable to find explicit activity class "
                        + ((Intent)intent).getComponent().toShortString()
                        + "; have you declared this activity in your AndroidManifest.xml?");
            throw new ActivityNotFoundException(
                    "No Activity found to handle " + intent);
        case ActivityManager.START_PERMISSION_DENIED:
            throw new SecurityException("Not allowed to start activity "
                    + intent);
        case ActivityManager.START_FORWARD_AND_REQUEST_CONFLICT:
            throw new AndroidRuntimeException(
                    "FORWARD_RESULT_FLAG used while also requesting a result");
        case ActivityManager.START_NOT_ACTIVITY:
            throw new IllegalArgumentException(
                    "PendingIntent is not an activity");
        case ActivityManager.START_NOT_VOICE_COMPATIBLE:
            throw new SecurityException(
                    "Starting under voice control not allowed for: " + intent);
        case ActivityManager.START_VOICE_NOT_ACTIVE_SESSION:
            throw new IllegalStateException(
                    "Session calling startVoiceActivity does not match active session");
        case ActivityManager.START_VOICE_HIDDEN_SESSION:
            throw new IllegalStateException(
                    "Cannot start voice activity on a hidden session");
        case ActivityManager.START_CANCELED:
            throw new AndroidRuntimeException("Activity could not be started for "
                    + intent);
        default:
            throw new AndroidRuntimeException("Unknown error code "
                    + res + " when starting " + intent);
    }
}

其中啟動activity的方法是ActivityManagerNative.getDefault().startActivity()。ActivityManagerNative繼承了Binder,同時實現了IActivityManager接口,啟動activity用到了Android中binder機制,這里先不做具體討論,代碼在frameworks/base/core/java/android/app/ActivityManagerNative.java。先來看getDefault(),獲取的是一個IActivityManager單例。

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

再看單例里做了什么,獲取了系統的ActivityManagerService(AMS),ActivityManagerNative實際上就是ActivityManagerService(AMS)這個遠程對象的Binder代理對象

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;
    }
};

到這里,就是整個activity啟動的流程了,也找到了ActivityNotFoundException的原因,那么到底有沒有辦法啟動一個沒有在Android.xml中聲明的activity呢?

hook分析

還是從gDefault這個單例看起,有沒有辦法把這個單例替換掉,自己實現startActivity方法來繞過系統對activity的校驗呢?同時我們看到IActivityManager是一個接口,那么是不是有什么辦法可以動態修改這個接口的實現呢?熟悉Java的同學一定會想到動態代理。我們先來實現一個InvocationHandler,代碼如下

public class HookActivityHandler implements InvocationHandler {

    private static final String TAG = "HookActivityHandler";

    private Object mBase;

    public HookActivityHandler(Object base) {
        this.mBase = base;
    }

    @Override
    public Object invoke(Object o, Method method, Object[] objects) throws Throwable {
        if ("startActivity".equalsIgnoreCase(method.getName())) {
            Log.d(TAG, "invoke: startActivity");
        }
        return method.invoke(mBase, objects);
    }
}

接著,我們需要通過反射將ActivityManagerNative中的gDefault中的gDefault換成我們自己的實現,代碼如下

public static final void hookActivityManagerService(ClassLoader classLoader) {
    try {
        Class<?> activityManagerNativeClass = Class.forName("android.app.ActivityManagerNative");
        if (activityManagerNativeClass == null) {
            return;
        }
        Field gDefaultField = activityManagerNativeClass.getDeclaredField("gDefault");
        if (gDefaultField == null) {
            return;
        }
        gDefaultField.setAccessible(true);
        Object gDefault = gDefaultField.get(null);

        Class<?> singleton = Class.forName("android.util.Singleton");
        if (singleton == null) {
            return;
        }

        Field mInstanceField = singleton.getDeclaredField("mInstance");
        if (mInstanceField == null) {
            return;
        }
        mInstanceField.setAccessible(true);

        Object activityManager = mInstanceField.get(gDefault);
        Class<?> activityManagerInterface = Class.forName("android.app.IActivityManager");
        Object proxy = Proxy.newProxyInstance(classLoader,
                new Class[] {activityManagerInterface}, new HookActivityHandler(activityManager));
        mInstanceField.set(gDefault, proxy);
    } catch (Exception e) {
        e.printStackTrace();
    }
}

然后新建一個正常activity看看能否正常調用到hook的方法中,輸出日志如下。

08-15 23:10:10.476 13208-13208/com.test.hookactivity D/HookActivityHandler: invoke: startActivity 

可以看到,已經成功的將startActivity()方法hook住了,接下來就可以在startActivity的時候做些事情了。在啟動activity的時候,會對activity做校驗,那么我們能不能用一個注冊在AndroidMenifest.xml中的activity作為橋梁,在校驗的時候用這個存在的activity,校驗過后再換會真正想要的activity,來個偷梁換柱。再回到HookActivityHandler中,只看invoke方法。
在startActivity之前先將目標intent取出來并緩存起來,將intent換成已經存在的PlaceHolderActivity,這樣校驗的時候就可以繞過系統對activity的校驗。大家可以試著運行一次代碼,并不會拋出ActivityNotFoundException的異常,驗證了之前的想法。

@Override
public Object invoke(Object o, Method method, Object[] objects) throws Throwable {
    if ("startActivity".equalsIgnoreCase(method.getName())) {
        Log.d(TAG, "invoke: startActivity");
        Intent rawIntent = null;
        int index = 0;

        for (int i = 0; i < objects.length; i++) {
            if (objects[i] instanceof Intent) {
                index = i;
                break;
            }
        }

        rawIntent = (Intent) objects[index];

        Intent newIntent = new Intent();
        String targetPackageName = "com.test.hookactivity";

        ComponentName componentName = new ComponentName(targetPackageName,
                PlaceHolderActivity.class.getCanonicalName());
        newIntent.setComponent(componentName);
        newIntent.putExtra("extra_target_intent", rawIntent);

        objects[index] = newIntent;
        return method.invoke(mBase, objects);
    }

    return method.invoke(mBase, objects);
}

那么繞過校驗之后怎樣恢復到我們想要的activity呢?從源碼中可以看到,在啟動activity時最后會通過ActivityThread中的handleMessage()方法,代碼如下

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);
            handleLaunchActivity(r, null, "LAUNCH_ACTIVITY");
            Trace.traceEnd(Trace.TRACE_TAG_ACTIVITY_MANAGER);
        } break;

        ...
    }
}

這里的handleLaunchActivity()就是啟動activity真正的方法,這里就是另一個hook點,在handleLaunchActivity()中將目標activity的intent替換回來,首先實現Handler.Callback,代碼如下。通過反射講message中的intent換會原來的intent。

public class ActivityThreadHandlerCallback implements Handler.Callback {

    private Handler mHandler;

    public ActivityThreadHandlerCallback(Handler handler) {
        this.mHandler = handler;
    }

    @Override
    public boolean handleMessage(Message message) {
        int what = message.what;
        switch (what) {
            case 100: //這里對應的是LAUNCH_ACTIVITY
                handleStartActivity(message);
                break;
        }
        mHandler.handleMessage(message);
        return true;
    }

    private void handleStartActivity(Message message) {
        Object object = message.obj;

        try {
            Field intent = object.getClass().getDeclaredField("intent");
            if (intent == null) {
                return;
            }
            intent.setAccessible(true);
            Intent rawIntent = (Intent) intent.get(object);

            Intent targetIntent = rawIntent.getParcelableExtra("extra_target_intent");
            if (targetIntent == null) {
                return;
            }
            rawIntent.setComponent(targetIntent.getComponent());

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

還差最后一步,就是將ActivityThread中的mCallback換成我們hook的callback,還是用老辦法——反射。在ActivityThread中handler定義的變量為mH,所以主要替換的目標是mH,代碼如下

public static final void hookActivityThreadHandler() {
    try {
        Class<?> activityThreadClass = Class.forName("android.app.ActivityThread");
        Field currentActivityThreadField = activityThreadClass.getDeclaredField("sCurrentActivityThread");
        if (currentActivityThreadField == null) {
            return;
        }
        currentActivityThreadField.setAccessible(true);
        Object currentActivityThread = currentActivityThreadField.get(null);

        Field mHField = activityThreadClass.getDeclaredField("mH");
        if (mHField == null) {
            return;
        }
        mHField.setAccessible(true);
        Handler mH = (Handler) mHField.get(currentActivityThread);
        Field mCallbackField = Handler.class.getDeclaredField("mCallback");
        if (mCallbackField == null) {
            return;
        }

        mCallbackField.setAccessible(true);
        mCallbackField.set(mH, new ActivityThreadHandlerCallback(mH));

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

到此,就神不知鬼不覺的繞過系統的校驗,在啟動的時候就是真正我們想要的頁面了,而這個頁面并沒有在AndroidManifest.xml中注冊。

小結

通過以上的方法,用動態代理分別hook了ActivityManagerNative中的IActivityManager和ActivityThread中handler的callback,用這兩個hook點就成功的繞過了系統的校驗,實現了啟動沒有聲明的activity。看似簡單,但是一定要對activity啟動的流程十分熟悉,還要理解android中的binder機制,這就是android插件化框架用到的原理之一。除了動態代理,利用雙親委派機制也可以實現對相關方法的hook。

有些人也許會問,啟動一個沒有在AndroidManifest.xml中的activity有什么用。的確,正常應用開發的確沒什么用。但是如果有一天你想動態下發一個activity的時候,就有用了。應用發布的時候不可能預埋不存在的activity,當有一天因為產品的變化,需求的變更的時候就只能發版解決了。但是如果可以做到動態下發,就可以靜默對應用進行升級,而新增activity就是基礎能力之一。

參考

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 230,578評論 6 544
  • 序言:濱河連續發生了三起死亡事件,死亡現場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發現死者居然都...
    沈念sama閱讀 99,701評論 3 429
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 178,691評論 0 383
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 63,974評論 1 318
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 72,694評論 6 413
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發上,一...
    開封第一講書人閱讀 56,026評論 1 329
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 44,015評論 3 450
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 43,193評論 0 290
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當地人在樹林里發現了一具尸體,經...
    沈念sama閱讀 49,719評論 1 336
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 41,442評論 3 360
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發現自己被綠了。 大學時的朋友給我發了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 43,668評論 1 374
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 39,151評論 5 365
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發生泄漏。R本人自食惡果不足惜,卻給世界環境...
    茶點故事閱讀 44,846評論 3 351
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 35,255評論 0 28
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 36,592評論 1 295
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 52,394評論 3 400
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 48,635評論 2 380

推薦閱讀更多精彩內容