插件化入門篇-如何啟動一個未注冊過的Activity

幾乎所有的插件化都會要的一個需求,啟動一個未注冊的Activiy挫望,即加載插件包中的Activity驾锰,并且主應用并不知道插件應用中會有什么Activity扮碧,這是各個插件化框架主力解決的問題之一磁椒。

今天我們學習一下占坑式插件化框架的啟動Activity原理堤瘤。

關于動態(tài)代理的知識,了解過Retrofit的源碼的或者看過Java設計模式之代理模式的高級使用的,應該都了解了浆熔。本章不做介紹本辐,主介紹hook+反射

Hook是什么?

Hook直白點說就是攔截方法医增,自己對其參數(shù)等進行修改,或者替換返回值慎皱,達到自己不可告人的目的的一件事。

尋找Hook點

對于啟動Activity叶骨,老實說光startActivity便有很多要說茫多,很多文章會帶著你一直追到ActivityManagerService中的若干個方法,最后再調用本地的ActivityThread里面的方法去啟動本進程的Activity忽刽。

所以光上面的流程我們看出天揖,我們把要啟動的Activity信息發(fā)給AMS,其做了各種檢查各種操作后真正讓Activity啟動的還是我們的ActivityThread

startActivity流程

我們startActivity是context的方法跪帝,去找Context實現(xiàn)類class ContextImpl extends Context今膊。

    @Override
    public void startActivities(Intent[] intents) {
        warnIfCallingFromSystemProcess();
        startActivities(intents, null);
    }
        /** @hide */
    @Override
    public void startActivitiesAsUser(Intent[] intents, Bundle options, UserHandle userHandle) {
        if ((intents[0].getFlags()&Intent.FLAG_ACTIVITY_NEW_TASK) == 0) {
            throw new AndroidRuntimeException(
                    "Calling startActivities() from outside of an Activity "
                    + " context requires the FLAG_ACTIVITY_NEW_TASK flag on first Intent."
                    + " Is this really what you want?");
        }
        mMainThread.getInstrumentation().execStartActivitiesAsUser(
            getOuterContext(), mMainThread.getApplicationThread(), null,
            (Activity)null, intents, options, userHandle.getIdentifier());
    }

看到最后調用的是mMainThread.getInstrumentation().execStartActivitiesAsUser方法,不用著急歉甚,直接ctrl鼠標左擊進去万细。是Instrumentation類。

    public void execStartActivitiesAsUser(Context who, IBinder contextThread,
            IBinder token, Activity target, Intent[] intents, Bundle options,
            int userId) {
        IApplicationThread whoThread = (IApplicationThread) contextThread;
        if (mActivityMonitors != null) {
            synchronized (mSync) {
                final int N = mActivityMonitors.size();
                for (int i=0; i<N; i++) {
                    final ActivityMonitor am = mActivityMonitors.get(i);
                    if (am.match(who, null, intents[0])) {
                        am.mHits++;
                        if (am.isBlocking()) {
                            return;
                        }
                        break;
                    }
                }
            }
        }
        try {
            String[] resolvedTypes = new String[intents.length];
            for (int i=0; i<intents.length; i++) {
                intents[i].migrateExtraStreamToClipData();
                intents[i].prepareToLeaveProcess();
                resolvedTypes[i] = intents[i].resolveTypeIfNeeded(who.getContentResolver());
            }
            int result = ActivityManagerNative.getDefault()
                .startActivities(whoThread, who.getBasePackageName(), intents, resolvedTypes,
                        token, options, userId);
            checkStartActivityResult(result, intents[0]);
        } catch (RemoteException e) {
        }
    }

這邊我們看到了纸泄,是調用ActivityManagerNative的方法啟動activity了赖钞。進去這個類我們只能看到一堆的binder通信,調用AMS的方法聘裁,不過此時我們不用關心了雪营,因為我們知道接下來是
AMS的事情。AMS是活在另一個PID的玩意兒衡便,我們只關心我們自己的pid献起,另一個進程的東西我們沒權限干壞事。

不過這邊我們需要注意镣陕,ActivityManagerNative居然是個單類谴餐,那么我們hook它會安全很多,畢竟這個對象是單類呆抑。

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

說是說AMS的事情不用關心岂嗓,但是我們得關心AMS什么時候回調回來,讓我們啟動Activity鹊碍。

ActivityThread看厌殉,一搜里面有個handleLaunchActivity方法食绿,是在Handler里面被調用的,而且ActivityThread也是我們喜歡的對象公罕,因為這個對象存在于整個應用生命周期中器紧。

        public void handleMessage(Message msg) {
            if (DEBUG_MESSAGES) Slog.v(TAG, ">>> handling: " + codeToString(msg.what));
            switch (msg.what) {
                case LAUNCH_ACTIVITY: {   //這個值是常量100
                    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;

看了這么多,我們可算是知道啟動Activity的入口和出口了楼眷,下面我們需要進行欺騙铲汪。

實現(xiàn)欺騙

欺騙系統(tǒng)就欺騙兩個地方,我們在AndroidManifest里面申明一個假Activity摩桶,然后在啟動真實Activity的地方桥状,將Intent里面的Activity替換成我們已經注冊過的。再在ActivityThread launch Activity的時候硝清,替換成我們需要啟動的便實現(xiàn)了啟動一個未注冊過的Activity的效果。

代碼實現(xiàn)

  • 寫一個占坑Activity转晰,在AndroidManifest注冊
        /**
         * 占坑專用
         */
        public class TmpActivity extends Activity {

            @Override
            protected void onCreate(Bundle savedInstanceState) {
                super.onCreate(savedInstanceState);
                setContentView(R.layout.activity_tmp);
            }
        }

        <!--占坑專用Activity-->
        <activity android:name=".TmpActivity"/>
  • attachBaseContext中欺騙應用

    @Override
    protected void attachBaseContext(Context newBase) {
        super.attachBaseContext(newBase);

        try {
            /**
             * 欺騙ActivityManagerNative,將要啟動的Activity替換成我們的占坑Activity
             */
            Class<?> activityManagerNativeClass = Class.forName("android.app" +
                    ".ActivityManagerNative");

            Field gDefaultField = activityManagerNativeClass.getDeclaredField("gDefault");
            gDefaultField.setAccessible(true);

            Object gDefault = gDefaultField.get(null);

            // gDefault是一個 android.util.Singleton對象; 我們取出這個單例里面的字段
            Class<?> singleton = Class.forName("android.util.Singleton");
            Field mInstanceField = singleton.getDeclaredField("mInstance");
            mInstanceField.setAccessible(true);

            // ActivityManagerNative 的gDefault對象里面原始的 IActivityManager對象
            final Object rawIActivityManager = mInstanceField.get(gDefault);

            // 創(chuàng)建一個這個對象的代理對象, 然后替換這個字段, 讓我們的代理對象幫忙干活
            Class<?> iActivityManagerInterface = Class.forName("android.app.IActivityManager");
            Object proxy = Proxy.newProxyInstance(Thread.currentThread().getContextClassLoader(),
                    new Class<?>[] {iActivityManagerInterface}, new InvocationHandler() {


                        @Override
                        public Object invoke(Object proxy, Method method, Object[] args) throws
                                                                                         Throwable {
                            if ("startActivity".equals(method.getName())) {
                                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();

                                // 替身Activity的包名, 也就是我們自己的包名
                                String stubPackage = "com.jerey.activityplugin";

                                // 這里我們把啟動的Activity臨時替換為 StubActivity
                                ComponentName componentName = new ComponentName(stubPackage,
                                        TmpActivity.class
                                                .getName());
                                newIntent.setComponent(componentName);

                                // 把我們原始要啟動的TargetActivity先存起來
                                newIntent.putExtra(EXTRA_TARGET_INTENT, raw);

                                // 替換掉Intent, 達到欺騙AMS的目的
                                args[index] = newIntent;

                                Log.d(TAG, "hook succes{s");
                                return method.invoke(rawIActivityManager, args);
                            }
                            return method.invoke(rawIActivityManager, args);
                        }
                    });
            mInstanceField.set(gDefault, proxy);


            /**
             * 欺騙ActivityThread
             */

            // 先獲取到當前的ActivityThread對象
            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);
            final 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 Handler.Callback() {
                @Override
                public boolean handleMessage(Message msg) {
                    if (msg.what == 100) {
                        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(EXTRA_TARGET_INTENT);
                            raw.setComponent(target.getComponent());

                        } catch (NoSuchFieldException e) {
                            e.printStackTrace();
                        } catch (IllegalAccessException e) {
                            e.printStackTrace();
                        }
                        mH.handleMessage(msg);
                    }
                    return true;
                }
            });


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

上面的代碼芦拿,我們先反射拿到ActivityManagerNative,然后動態(tài)代理IActivityManager,Hook其startActivity方法查邢,在里面替換掉intent蔗崎,并將真實的Intent存放在假Intent的參數(shù)里面。

在系統(tǒng)最后調用打開假Intent的時候扰藕,我們從Intent中取出參數(shù)缓苛,并打開真正想打開的Activity。

  • 打開Activity
    我們和正常使用一樣邓深,startActivity就能打開我們未注冊的Activity了未桥。
findViewById(R.id.btn).setOnClickListener(new View.OnClickListener() {
    @Override
    public void onClick(View v) {
        startActivity(new Intent(MainActivity.this, UnregisterActivity.class));
    }
});

Demo路徑:https://github.com/Jerey-Jobs/AppPluginDemos

總結

上面只是一個Demo,不能支持support包的AppCompatActivity,真正的完整的插件化庫任務是艱巨的芥备!
還要支持其他組件冬耿,都是很麻煩的事情。


本文作者:Anderson/Jerey_Jobs

博客地址 : http://jerey.cn/
簡書地址 : Anderson大碼渣
github地址 : https://github.com/Jerey-Jobs

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯(lián)系作者
  • 序言:七十年代末萌壳,一起剝皮案震驚了整個濱河市亦镶,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌袱瓮,老刑警劉巖缤骨,帶你破解...
    沈念sama閱讀 221,695評論 6 515
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異尺借,居然都是意外死亡绊起,警方通過查閱死者的電腦和手機,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 94,569評論 3 399
  • 文/潘曉璐 我一進店門褐望,熙熙樓的掌柜王于貴愁眉苦臉地迎上來勒庄,“玉大人串前,你說我怎么就攤上這事∈当危” “怎么了荡碾?”我有些...
    開封第一講書人閱讀 168,130評論 0 360
  • 文/不壞的土叔 我叫張陵,是天一觀的道長局装。 經常有香客問我坛吁,道長,這世上最難降的妖魔是什么铐尚? 我笑而不...
    開封第一講書人閱讀 59,648評論 1 297
  • 正文 為了忘掉前任拨脉,我火速辦了婚禮,結果婚禮上宣增,老公的妹妹穿的比我還像新娘玫膀。我一直安慰自己,他們只是感情好爹脾,可當我...
    茶點故事閱讀 68,655評論 6 397
  • 文/花漫 我一把揭開白布帖旨。 她就那樣靜靜地躺著,像睡著了一般灵妨。 火紅的嫁衣襯著肌膚如雪解阅。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 52,268評論 1 309
  • 那天泌霍,我揣著相機與錄音货抄,去河邊找鬼。 笑死朱转,一個胖子當著我的面吹牛蟹地,可吹牛的內容都是我干的。 我是一名探鬼主播肋拔,決...
    沈念sama閱讀 40,835評論 3 421
  • 文/蒼蘭香墨 我猛地睜開眼锈津,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了凉蜂?” 一聲冷哼從身側響起琼梆,我...
    開封第一講書人閱讀 39,740評論 0 276
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎窿吩,沒想到半個月后茎杂,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經...
    沈念sama閱讀 46,286評論 1 318
  • 正文 獨居荒郊野嶺守林人離奇死亡纫雁,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 38,375評論 3 340
  • 正文 我和宋清朗相戀三年煌往,在試婚紗的時候發(fā)現(xiàn)自己被綠了。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 40,505評論 1 352
  • 序言:一個原本活蹦亂跳的男人離奇死亡刽脖,死狀恐怖羞海,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情曲管,我是刑警寧澤却邓,帶...
    沈念sama閱讀 36,185評論 5 350
  • 正文 年R本政府宣布,位于F島的核電站院水,受9級特大地震影響腊徙,放射性物質發(fā)生泄漏。R本人自食惡果不足惜檬某,卻給世界環(huán)境...
    茶點故事閱讀 41,873評論 3 333
  • 文/蒙蒙 一撬腾、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧恢恼,春花似錦民傻、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 32,357評論 0 24
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至和簸,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間碟刺,已是汗流浹背锁保。 一陣腳步聲響...
    開封第一講書人閱讀 33,466評論 1 272
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留半沽,地道東北人爽柒。 一個月前我還...
    沈念sama閱讀 48,921評論 3 376
  • 正文 我出身青樓,卻偏偏與公主長得像者填,于是被迫代替她去往敵國和親浩村。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 45,515評論 2 359

推薦閱讀更多精彩內容