Android插件化之startActivity hook的幾種姿勢

關于攔截activity的啟動這個想法還是之前組內(nèi)成員在做業(yè)務時碰到的一個小難題性芬,具體的業(yè)務場景就是關于當前activity需要進行判斷然后點擊完button后跳轉(zhuǎn)下一個activity也需要判斷 然后彈出一個dialog续搀。具體的業(yè)務不細說了,所以看MagiLu在某乎上的回答 最后在對于開發(fā)者的建議里面有句話是這樣說的現(xiàn)在想想深表贊同

很多同學都想向更高階進階殉簸,但在實際的工作中,這是很困難的存皂,我見過技術上很強的人都是用大型項目档叔,不斷解決其中的困難和問題,慢慢喂出來的芋膘。

看到這里 首先說下這篇文章比較基礎 如果是插件化大神的話 就沒有看下去的必要了 出門右轉(zhuǎn)不送鳞青。霸饲。。

先不說插件化了 首先關于startActivity 我們平時會經(jīng)常使用到 在activity內(nèi) 直接startActivity(intent)
其實這里還有一種start方式 我們可能沒怎么用過 getApplicationContext.startActivity(intent) 通過這種方式可以開啟其它程序中的activity

  • 代理
    在說Hook之前先說下代理包括靜態(tài)代理和動態(tài)代理如果不熟悉的可以看下
http://www.reibang.com/p/8507b4364f11
  • 反射
    其次就是java反射方面的知識
    如何通過變量獲取實例對象再進行替換

Hook:我們把修改參數(shù)臂拓,替換返回值厚脉,稱之為Hook
Hook點:靜態(tài)變量和單利;即就是不容易發(fā)生改變的對象胶惰,容易定位 普通的對象容易發(fā)生改變 所以我們可以根據(jù)這個原則去hook

1. 首先我們分析下我們平時直接使用的startActivity()方法

源碼

@Override
    public void startActivity(Intent intent) {
        this.startActivity(intent, null);
    }
@Override
    public void startActivity(Intent intent, @Nullable Bundle options) {
        if (options != null) {
            startActivityForResult(intent, -1, options);
        } else {
            // Note we want to go through this call for compatibility with
            // applications that may have overridden the method.
            startActivityForResult(intent, -1);
        }
    }
@Override
    public void startActivityForResult(
            String who, Intent intent, int requestCode, @Nullable Bundle options) {
        Uri referrer = onProvideReferrer();
        if (referrer != null) {
            intent.putExtra(Intent.EXTRA_REFERRER, referrer);
        }
        Instrumentation.ActivityResult ar =
            mInstrumentation.execStartActivity(
                this, mMainThread.getApplicationThread(), mToken, who,
                intent, requestCode, options);
        if (ar != null) {
            mMainThread.sendActivityResult(
                mToken, who, requestCode,
                ar.getResultCode(), ar.getResultData());
        }
        cancelInputsAndStartExitTransition(options);
    }

最終調(diào)用了startActivityForResult()方法 在方法內(nèi)調(diào)用了mInstrumentation變量的execStartActivity()方法 所以最終startActivity的是Instrumentation類的execStartActivity()方法(nativie層面的)其實是通過遠端的ActivityManagerService啟動的 是一個跨進程通信的過程

public ActivityResult execStartActivity(
            Context who, IBinder contextThread, IBinder token, Activity target,
            Intent intent, int requestCode, Bundle options) {
       ...
        try {
            intent.migrateExtraStreamToClipData();
            intent.prepareToLeaveProcess();
            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;
    }

這里呢 我們可以去hook掉activity類內(nèi)變量 mInstrumentation 將它替換成我們自己的Instrumentation
通過手寫靜態(tài)代理類傻工,替換掉原始的方法

public class EvilInstrumentation extends Instrumentation {

    private static final String TAG = "EvilInstrumentation";

    // ActivityThread中原始的對象, 保存起來
    Instrumentation mBase;

    public EvilInstrumentation(Instrumentation base) {
        mBase = base;
    }

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

        // Hook之前, XXX到此一游!
        Log.e(TAG, "\n執(zhí)行了startActivity, 參數(shù)如下: \n" + "who = [" + who + "], " +
                "\ncontextThread = [" + contextThread + "], \ntoken = [" + token + "], " +
                "\ntarget = [" + target + "], \nintent = [" + intent +
                "], \nrequestCode = [" + requestCode + "], \noptions = [" + options + "]");
        Uri uri = Uri.parse("http://www.reibang.com/u/3d228ed8d54d");
        Intent intent1 = new Intent(Intent.ACTION_VIEW,uri);
        // 開始調(diào)用原始的方法, 調(diào)不調(diào)用隨你,但是不調(diào)用的話, 所有的startActivity都失效了.
        // 由于這個方法是隱藏的,因此需要使用反射調(diào)用;首先找到這個方法
        try {
            
            //這里通過反射找到原始Instrumentation類的execStartActivity方法
            Method execStartActivity = Instrumentation.class.getDeclaredMethod(
                    "execStartActivity",
                    Context.class, IBinder.class, IBinder.class, Activity.class,
                    Intent.class, int.class, Bundle.class);
            execStartActivity.setAccessible(true);
            
            //執(zhí)行原始Instrumentation的execStartActivity方法 再次之前 you can do whatever you want ...
            return (ActivityResult) execStartActivity.invoke(mBase, who,
                    contextThread, token, target, intent1, requestCode, options);
        } catch (Exception e) {
            // 某該死的rom修改了  需要手動適配
            throw new RuntimeException("do not support!!! pls adapt it");
        }
    }
}

在這里我們可以拿到Intent 當然你可以將這個intent對象進行替換 跳轉(zhuǎn)到另一個界面(whatever you want)

有了代理對象 下面使用反射直接進行替換

public static void replaceInstrumentation(Activity activity){
            Class<?> k = Activity.class;
        try {
            //通過Activity.class 拿到 mInstrumentation字段
            Field field = k.getDeclaredField("mInstrumentation");
            field.setAccessible(true);
            //根據(jù)activity內(nèi)mInstrumentation字段 獲取Instrumentation對象
            Instrumentation instrumentation = (Instrumentation)field.get(activity);
            //創(chuàng)建代理對象
            Instrumentation instrumentationProxy = new EvilInstrumentation(instrumentation);
            //進行替換
            field.set(activity,instrumentationProxy);
        } catch (IllegalAccessException e){
            e.printStackTrace();
        }catch (NoSuchFieldException e){
            e.printStackTrace();
        }

    }

有注釋不用過多解釋了

@Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        HookActivityHelper.replaceInstrumentation(this);
       Button tv = new Button(this);
        tv.setText("測試界面");
        setContentView(tv);
        tv.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                Intent intent = new Intent(Intent.ACTION_VIEW);
                intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
                intent.setData(Uri.parse("http://www.baidu.com"));           
                startActivity(intent);
            }
        });
    }

測試當我們點擊button后 跳轉(zhuǎn)到了自己的簡書主頁而不是百度 同時看到log

image.png

可見 我們確實hook成功了

2.下面使用 getApplicationContext().startActivity(intent)這種方式開啟另一個activity

Context開啟activity 實質(zhì)為 ContextImpl(Context的實現(xiàn)類)去真正開啟的

final ActivityThread mMainThread;
 @Override
    public void startActivity(Intent intent, Bundle options) {
        warnIfCallingFromSystemProcess();
        if ((intent.getFlags()&Intent.FLAG_ACTIVITY_NEW_TASK) == 0) {
            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);
    }

可以看到是調(diào)用了ActivityThread類的getInstrumentation()方法返回mInstrumentation變量后 再調(diào)用execStartActivity()方法

Instrumentation mInstrumentation;
public Instrumentation getInstrumentation()
    {
        return mInstrumentation;
    }

整個流程 almost like this


未命名文件.png

所以現(xiàn)在變成了我們需要替換ActivityThread類(不是主線程類 final類只不過運行在主線程而已)中的mInstrumentation,首先我們需要拿到ActivityThread對象吧 剛好在ActivityThread類中有個靜態(tài)方法currentActivityThread可以幫助我們拿到這個對象類

private static ActivityThread sCurrentActivityThread;
public static ActivityThread currentActivityThread() {
        return sCurrentActivityThread;
    }

so 我們的反射代碼如下

      // 先獲取到當前的ActivityThread對象
        Class<?> activityThreadClass = Class.forName("android.app.ActivityThread");
        Method currentActivityThreadMethod =       
        activityThreadClass.getDeclaredMethod("currentActivityThread");
        currentActivityThreadMethod.setAccessible(true);
        //currentActivityThread是一個static函數(shù)所以可以直接invoke孵滞,不需要帶實例參數(shù)
        Object currentActivityThread = currentActivityThreadMethod.invoke(null);

拿到ActivityThread對象之后 后面的就和第一種方法一樣了 將ActivityThread類中的mInstrumentaion換為EvilInstrumentation

其實比較這兩種方法的區(qū)別:

  • Activity的startActivity 是替換 Activity類內(nèi)部的mInstrumentation
  • Context的startActivity 是替換 ActivityThread類內(nèi)部的mInstrumentation

還有一點就是攔截的時機
第一種方式在Activity的onCreate()方法內(nèi)部攔截即可 而第二種方式需要在onCreate()方法之前attachBaseContext()方法中進行攔截
這里就牽扯到一些應用啟動后的生命周期:當啟動應用時中捆,會孵化一個新進程,啟動應用的啟動入口為ActivityThread.main()坊饶,后面會新建一個ContextImpl實例給application,在后續(xù)的啟動中泄伪,會進行activity的創(chuàng)建,其中ActivityThread會調(diào)用performLaunchActivity,先通過createBaseContextForActivity創(chuàng)建Context,然后再activity.attach()

在attach()方法中

final void attach(Context context, ActivityThread aThread,
            Instrumentation instr, IBinder token, int ident,
            Application application, Intent intent, ActivityInfo info,
            CharSequence title, Activity parent, String id,
            NonConfigurationInstances lastNonConfigurationInstances,
            Configuration config, String referrer, IVoiceInteractor voiceInteractor) {
        attachBaseContext(context);
        mInstrumentation = instr;
}

我們看到首先調(diào)用了attachBaseContext(context)這個方法
然后給成員mInstrumentation賦值
so 如果我們在onCreate()方法中去做hook是hook不到的 時機已晚

@Override
    protected void attachBaseContext(Context newBase) {
        super.attachBaseContext(newBase);
        try {
            Log.e("activity","attachBaseContext");
            // 在這里進行Hook
            HookHelper.attachContext();
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

測試同樣ok

3.前兩種方法都是替換Instrumentation 這種方法我們看看在Instrumentation類中的execStartActivity()方法

 int result = ActivityManagerNative.getDefault()
                .startActivity(whoThread, who.getBasePackageName(), intent,
                        intent.resolveTypeIfNeeded(who.getContentResolver()),
                        token, target != null ? target.mEmbeddedID : null,
                        requestCode, 0, null, options);

真正開啟的是這句 ActivityManagerNative.getDefault().startActivity()
首先ActivityManagerNative是個什么鬼 點開后發(fā)現(xiàn)

ublic abstract class ActivityManagerNative extends Binder implements IActivityManager

繼承了Binder 實現(xiàn)了 IActivityManager 而且是個抽象類 類圖如下

image.png

這張圖得看半天
繼續(xù)往下走

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

getDefault返回的是一個IActivityManager對象
這里gDefault借助Singleton類實現(xiàn)單利模式 gDefault為static final 形式 根據(jù)我們的hook原則 這是一個比較好hook的點

代碼如下:

public class HookUtil {
    private static final String TAG = "HookUtil";
    public void hookIActivityManager() {
        try {
            Class<?> c = Class.forName("android.app.ActivityManagerNative");

            Field field = c.getDeclaredField("gDefault");

            field.setAccessible(true);

            //返回ActivityManagerNative對象
            Object amn = field.get(null);

            Class<?> k = Class.forName("android.util.Singleton");

            Field field1 = k.getDeclaredField("mInstance");

            field1.setAccessible(true);

            //拿到ActivityManagerProxy對象 代理ActivityManagerNative對象的子類ActivityManagerService
            //為什么不是IActivityManager對象
            //因為在gDefault對象的 實現(xiàn)方法 onCreate()方法中 asInterface(b)返回的是  return new ActivityManagerProxy(obj) 具體可以看源碼

            Object iActivityManager = field1.get(amn);

            IamInvocationHandler iamInvocationHandler = new IamInvocationHandler(iActivityManager);

            Object object = Proxy.newProxyInstance(iamInvocationHandler.getClass().getClassLoader(),iActivityManager.getClass().getInterfaces(),iamInvocationHandler);

            field1.set(amn,object);

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


    class IamInvocationHandler implements InvocationHandler {

        Object iamObject;

        public IamInvocationHandler(Object iamObject) {
            this.iamObject = iamObject;
        }

        @Override
        public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
//            Log.e(TAG, method.getName());
            if ("startActivity".equals(method.getName())) {
                Log.e(TAG, "要開始啟動了 啦啦啦啦啦啦  ");
            }
            return method.invoke(iamObject, args);
        }
    }
}
image.png

測試也沒問題

so that's all thanks for your time

參考文章:
Weishu's Notes
thanks weishu在某乎上答疑

最后編輯于
?著作權歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末匿级,一起剝皮案震驚了整個濱河市臂容,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌根蟹,老刑警劉巖脓杉,帶你破解...
    沈念sama閱讀 212,884評論 6 492
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異简逮,居然都是意外死亡球散,警方通過查閱死者的電腦和手機,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 90,755評論 3 385
  • 文/潘曉璐 我一進店門散庶,熙熙樓的掌柜王于貴愁眉苦臉地迎上來蕉堰,“玉大人,你說我怎么就攤上這事悲龟∥菅龋” “怎么了?”我有些...
    開封第一講書人閱讀 158,369評論 0 348
  • 文/不壞的土叔 我叫張陵须教,是天一觀的道長皿渗。 經(jīng)常有香客問我,道長轻腺,這世上最難降的妖魔是什么乐疆? 我笑而不...
    開封第一講書人閱讀 56,799評論 1 285
  • 正文 為了忘掉前任,我火速辦了婚禮贬养,結(jié)果婚禮上挤土,老公的妹妹穿的比我還像新娘。我一直安慰自己误算,他們只是感情好仰美,可當我...
    茶點故事閱讀 65,910評論 6 386
  • 文/花漫 我一把揭開白布迷殿。 她就那樣靜靜地躺著,像睡著了一般咖杂。 火紅的嫁衣襯著肌膚如雪贪庙。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 50,096評論 1 291
  • 那天翰苫,我揣著相機與錄音,去河邊找鬼这橙。 笑死奏窑,一個胖子當著我的面吹牛,可吹牛的內(nèi)容都是我干的屈扎。 我是一名探鬼主播埃唯,決...
    沈念sama閱讀 39,159評論 3 411
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼鹰晨!你這毒婦竟也來了墨叛?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 37,917評論 0 268
  • 序言:老撾萬榮一對情侶失蹤模蜡,失蹤者是張志新(化名)和其女友劉穎漠趁,沒想到半個月后,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體忍疾,經(jīng)...
    沈念sama閱讀 44,360評論 1 303
  • 正文 獨居荒郊野嶺守林人離奇死亡闯传,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 36,673評論 2 327
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發(fā)現(xiàn)自己被綠了卤妒。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片甥绿。...
    茶點故事閱讀 38,814評論 1 341
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖则披,靈堂內(nèi)的尸體忽然破棺而出共缕,到底是詐尸還是另有隱情,我是刑警寧澤士复,帶...
    沈念sama閱讀 34,509評論 4 334
  • 正文 年R本政府宣布图谷,位于F島的核電站,受9級特大地震影響阱洪,放射性物質(zhì)發(fā)生泄漏蜓萄。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點故事閱讀 40,156評論 3 317
  • 文/蒙蒙 一澄峰、第九天 我趴在偏房一處隱蔽的房頂上張望嫉沽。 院中可真熱鬧,春花似錦俏竞、人聲如沸绸硕。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,882評論 0 21
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽玻佩。三九已至出嘹,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間咬崔,已是汗流浹背税稼。 一陣腳步聲響...
    開封第一講書人閱讀 32,123評論 1 267
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留垮斯,地道東北人郎仆。 一個月前我還...
    沈念sama閱讀 46,641評論 2 362
  • 正文 我出身青樓,卻偏偏與公主長得像兜蠕,于是被迫代替她去往敵國和親扰肌。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 43,728評論 2 351

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