Toast源碼分析及消息內(nèi)容hook

  • 最近發(fā)現(xiàn)在小米高系統(tǒng)版本的手機(jī)上,Toast的內(nèi)容會(huì)自帶應(yīng)用名稱(chēng)的前綴;百度一下,發(fā)現(xiàn)的確不少這些反饋(萬(wàn)惡的小米系統(tǒng)開(kāi)發(fā)...),看了幾篇解決這個(gè)問(wèn)題的文章,基本如下:
Toast toast = Toast.makeText(context, “”, Toast.LENGTH_LONG);
toast.setText(message);
toast.show();
  • 但是如果我們的項(xiàng)目中,有幾十個(gè)地方用到了Toast,那就要在幾十個(gè)地方都去修改,這樣太麻煩了,能不能在一個(gè)地方做處理,其他地方都不用修改呢。

先查看Toast類(lèi)的源碼:

1、makeText()

public static Toast makeText(Context context, CharSequence text, @Duration int duration) {
    return makeText(context, null, text, duration);//調(diào)用makeText的重載方法,Looper傳入為null
}


public static Toast makeText(@NonNull Context context, @Nullable Looper looper,
            @NonNull CharSequence text, @Duration int duration) {
    //調(diào)用Toast的構(gòu)造方法,先創(chuàng)建一個(gè)Toast的實(shí)例
   Toast result = new Toast(context, looper);

   //填充id為transient_notification的layout頁(yè)面,獲取id為message的TextView,設(shè)置內(nèi)容為text
   LayoutInflater inflate = (LayoutInflater)
       context.getSystemService(Context.LAYOUT_INFLATER_SERVICE);
   View v = inflate.inflate(com.android.internal.R.layout.transient_notification, null);
   TextView tv = (TextView)v.findViewById(com.android.internal.R.id.message);
   tv.setText(text);

   //把Toast中的mNextView變量賦值為v,這句很重要,后面hook的時(shí)候會(huì)用到
   result.mNextView = v;
   result.mDuration = duration;

   return result;
}

makeText()第一步就是創(chuàng)建一個(gè)Toast實(shí)例。

1.1 Toast的構(gòu)造方法

    public Toast(Context context) {
        this(context, null);
    }

    //Toast的構(gòu)造方法就是初始化mTN變量(mTN在show()方法中會(huì)用到),配置Toast的layout參數(shù)
    public Toast(@NonNull Context context, @Nullable Looper looper) {
        mContext = context;
        mTN = new TN(context.getPackageName(), looper);
        mTN.mY = context.getResources().getDimensionPixelSize(
                com.android.internal.R.dimen.toast_y_offset);
        mTN.mGravity = context.getResources().getInteger(
                com.android.internal.R.integer.config_toastDefaultGravity);
    }

private static class TN extends ITransientNotification.Stub {

    //TN的構(gòu)造方法就是配置Toast的layout參數(shù)
    TN(String packageName, @Nullable Looper looper) {
            // XXX This should be changed to use a Dialog, with a Theme.Toast
            // defined that sets up the layout params appropriately.
            final WindowManager.LayoutParams params = mParams;
            params.height = WindowManager.LayoutParams.WRAP_CONTENT;
            params.width = WindowManager.LayoutParams.WRAP_CONTENT;
            params.format = PixelFormat.TRANSLUCENT;
            params.windowAnimations = com.android.internal.R.style.Animation_Toast;
            params.type = WindowManager.LayoutParams.TYPE_TOAST;
            params.setTitle("Toast");
            params.flags = WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON
                    | WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE
                    | WindowManager.LayoutParams.FLAG_NOT_TOUCHABLE;

            mPackageName = packageName;

            if (looper == null) {
                // Use Looper.myLooper() if looper is not specified.
                looper = Looper.myLooper();
                if (looper == null) {
                    throw new RuntimeException(
                            "Can't toast on a thread that has not called Looper.prepare()");
                }
            }
            mHandler = new Handler(looper, null) {

                ...
            };
     }
}

分析:

  • makeText()方法首先創(chuàng)建一個(gè)Toast的實(shí)例。
  • Toast的構(gòu)造方法中會(huì)配置好Toast展示所需要的layout參數(shù)
  • 創(chuàng)建好Toast后,會(huì)填充id為transient_notification的layout布局,實(shí)例成View實(shí)例,這個(gè)View也就是我們能看到的Toast,layout中包含了一個(gè)id為message的TextView,給TextView設(shè)置內(nèi)容為傳遞進(jìn)來(lái)的text。
  • 最后再把mNextView變量賦值為上一步填充形成的View;這個(gè)mNextView最后調(diào)用show()方法時(shí)會(huì)用到。

2、show()

    public void show() {
        if (mNextView == null) {
            throw new RuntimeException("setView must have been called");
        }

        //通過(guò)getService()方法獲取INotificationManager 的實(shí)例
        INotificationManager service = getService();
        String pkg = mContext.getOpPackageName();
        TN tn = mTN;
        //把makeText方法中實(shí)例的view賦值給tn的mNextView變量
        tn.mNextView = mNextView;

        try {
            //調(diào)用INotificationManager的enqueueToast的方法
            //參數(shù)tn的mNextView為View布局,包含了TextView的子控件
            service.enqueueToast(pkg, tn, mDuration);
        } catch (RemoteException e) {
            // Empty
        }
    }

    //獲取INotificationManager 的實(shí)例,非空判斷確保了sService為單例
    static private INotificationManager getService() {
        if (sService != null) {
            return sService;
        }
        sService = INotificationManager.Stub.asInterface(ServiceManager.getService("notification"));
        return sService;
    }
  • 首先先通過(guò)調(diào)用 getService()獲取INotificationManager的實(shí)例;
  • getService()方法返回sService的單例實(shí)例;
  • 通過(guò)調(diào)用INotificationManager的實(shí)例service的enqueueToast方法來(lái)展示toast。

小結(jié):展示一個(gè)Toast,主要經(jīng)過(guò)4個(gè)步驟:
1、創(chuàng)建一個(gè)Toast實(shí)例,同時(shí)創(chuàng)建Toast的內(nèi)部類(lèi)TN的實(shí)例,配置Toast展示時(shí)需要的layout參數(shù)。
2、填充Toast展示所需要的layout布局,實(shí)例化為View類(lèi)型,然后把view中的TextView控件設(shè)置我們傳入的text內(nèi)容文字。
3、把上一步實(shí)例出來(lái)的view,設(shè)置為給TN類(lèi)的mNextView 變量。
4、通過(guò)getService()方法獲取INotificationManager實(shí)例,調(diào)用INotificationManager的enqueueToast方法。

3、hook消息內(nèi)容

  • 先找到需要hook的對(duì)象(最好是個(gè)單例對(duì)象,這樣可以實(shí)現(xiàn)無(wú)侵入修改)。
  • 然后找到hook對(duì)象的持有者(在這里也就是找到Toast類(lèi)中指向這個(gè)被hook的對(duì)象的全局變量)。
  • 創(chuàng)建hook對(duì)象的代理類(lèi),并新建這個(gè)代理類(lèi)的實(shí)例。
  • 用代理類(lèi)的實(shí)例替換原先需要hook的對(duì)象。
3.1、先確定要hook的對(duì)象
    final TN mTN;

    public static Toast makeText(Context context, CharSequence text, @Duration int duration) {
        return makeText(context, null, text, duration);
    }

    public static Toast makeText(@NonNull Context context, @Nullable Looper looper,
            @NonNull CharSequence text, @Duration int duration) {
        Toast result = new Toast(context, looper);

        LayoutInflater inflate = (LayoutInflater)
                context.getSystemService(Context.LAYOUT_INFLATER_SERVICE);
        View v = inflate.inflate(com.android.internal.R.layout.transient_notification, null);
        TextView tv = (TextView)v.findViewById(com.android.internal.R.id.message);
        tv.setText(text);

        result.mNextView = v;
        result.mDuration = duration;

        return result;
    }

    public void show() {
        if (mNextView == null) {
            throw new RuntimeException("setView must have been called");
        }

        INotificationManager service = getService();
        String pkg = mContext.getOpPackageName();
        TN tn = mTN;
        tn.mNextView = mNextView;

        try {
            service.enqueueToast(pkg, tn, mDuration);
        } catch (RemoteException e) {
            // Empty
        }
    }

    private static INotificationManager sService;

    static private INotificationManager getService() {
        if (sService != null) {
            return sService;
        }
        sService = INotificationManager.Stub.asInterface(ServiceManager.getService("notification"));
        return sService;
    }

我們的目的是修改text的內(nèi)容,而text是設(shè)置給TextView控件,也就是我們最終需要hook TextView對(duì)象,或者h(yuǎn)ook TextView對(duì)象的持有者或間接持有者(通過(guò)TextView對(duì)象的持有者獲取到TextView,來(lái)實(shí)現(xiàn)對(duì)TextView內(nèi)容的修改)。

  1. 首先TextView是局部變量,沒(méi)辦法hook。
  2. 而TextView的持有者View被全局變量mNextView所持有,這是一個(gè)可以hook的切入點(diǎn)。
  3. 從show()方法中可以看到,mTN的mNextView變量也指向了View,所以mTN也是TextView的持有者,這也是一個(gè)可以hook的切入點(diǎn)。
  4. show()方法中,tn對(duì)象作為參數(shù)傳入了enqueueToast方法中,也就是service對(duì)象間接持有了tn對(duì)象,service間接持有TextView對(duì)象,service也是一個(gè)可以hook的切入點(diǎn)。

從上面四點(diǎn),我們可以找到三個(gè)可以hook的切入點(diǎn),而最佳的hook的對(duì)象service,因?yàn)閟ervice對(duì)象持有者是sService變量,sService是個(gè)單例,多個(gè)的Toast對(duì)象中我們都只需要替換一次。

3.2創(chuàng)建hook對(duì)象的代理類(lèi)
public class ToastProxy implements InvocationHandler {

    private static final String TAG = "ToastProxy";
    private Object mProxyObject;
    private Context mContext;

    public ToastProxy( Context mContext, Object mProxyObject) {
        this.mContext = mContext;
        this.mProxyObject = mProxyObject;
    }

    @Override
    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
        Log.i(TAG, "invoke: method == " + method.getName());

        //對(duì)Toast類(lèi)中的INotificationManager實(shí)例sService執(zhí)行enqueueToast方法時(shí),進(jìn)行攔截操作
        if (method.getName().equals("enqueueToast")){
            if (args != null && args.length > 0) {
                try{
                    //獲取tn對(duì)象
                    Object tn = args[1];
                    //獲取mNextView變量,也就是View, 對(duì)應(yīng)的是LinearLayout對(duì)象
                    Field mNextView = tn.getClass().getDeclaredField("mNextView");
                    mNextView.setAccessible(true);
                    LinearLayout linearLayout = (LinearLayout) mNextView.get(args[1]);

                    //從對(duì)應(yīng)的是LinearLayout對(duì)象中獲取TextView對(duì)象
                    if (linearLayout.getChildAt(0) instanceof TextView){
                        TextView textView = (TextView) linearLayout.getChildAt(0);
                        String msgOfToast = textView.getText().toString();//這個(gè)就是Toast的內(nèi)容
                        String appName = mContext.getString(R.string.app_name);
                        if (msgOfToast.contains(appName)){
                            String content = msgOfToast.substring(appName.length() + 1);
                            textView.setText(content);
                        }
                    }
                }catch (NoSuchFieldException e){
                    e.printStackTrace();
                }
            }
        }
        return method.invoke(mProxyObject, args);
    }

}

判斷方法名,攔截enqueueToast方法的邏輯,獲取到TextView對(duì)象,修改文字內(nèi)容。

3.3、替換需要hook的對(duì)象
public class ToastUtil {

    public static void hookToast(Context ctx){
        Looper.prepare();
        Toast toast = new Toast(ctx);
        try {
            Method getService = toast.getClass().getDeclaredMethod("getService");
            getService.setAccessible(true);
            //實(shí)例化INotificationManager
            Object sService = getService.invoke(toast);
            ToastProxy toastProxy = new ToastProxy(ctx, sService);
            //創(chuàng)建hook對(duì)象的代理類(lèi)實(shí)例
            Object serviceProxy = Proxy.newProxyInstance(toast.getClass().getClassLoader(), sService.getClass().getInterfaces(), toastProxy);
            Field sServiceField = toast.getClass().getDeclaredField("sService");
            sServiceField.setAccessible(true);
            //替換Toast類(lèi)中已經(jīng)初始化的單例對(duì)象sService
            sServiceField.set(toast, serviceProxy);
        } catch (NoSuchMethodException e) {
            e.printStackTrace();
        } catch (IllegalAccessException e) {
            e.printStackTrace();
        } catch (InvocationTargetException e) {
            e.printStackTrace();
        } catch (NoSuchFieldException e) {
            e.printStackTrace();
        }
    }

}

創(chuàng)建Toast實(shí)例,同時(shí)實(shí)例化INotificationManager的單例sService;再創(chuàng)建hook的對(duì)象service的代理類(lèi)實(shí)例,替換sService所指向的對(duì)象。然后在我們業(yè)務(wù)代碼使用Toast前,執(zhí)行hookToast方法即可。

總結(jié):hook可以幫我們?cè)谀承┨囟ǖ那腥朦c(diǎn)中無(wú)侵入式的完成一些代碼邏輯的修改;可以在不改變?cè)械拇a業(yè)務(wù),插入一些特定的代碼業(yè)務(wù)。而實(shí)現(xiàn)hook,只需要我們根據(jù)需求,從源代碼中找到最佳可以hook的對(duì)象,通過(guò)反射等代碼即可實(shí)現(xiàn)。

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

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