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

  • 最近發(fā)現(xiàn)在小米高系統(tǒng)版本的手機(jī)上,Toast的內(nèi)容會(huì)自帶應(yī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類的源碼:

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)部類TN的實(shí)例眠屎,配置Toast展示時(shí)需要的layout參數(shù)剔交。
2、填充Toast展示所需要的layout布局改衩,實(shí)例化為View類型岖常,然后把view中的TextView控件設(shè)置我們傳入的text內(nèi)容文字。
3葫督、把上一步實(shí)例出來(lái)的view竭鞍,設(shè)置為給TN類的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類中指向這個(gè)被hook的對(duì)象的全局變量)。
  • 創(chuàng)建hook對(duì)象的代理類丐怯,并新建這個(gè)代理類的實(shí)例喷好。
  • 用代理類的實(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ì)象的代理類
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類中的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ì)象的代理類實(shí)例
            Object serviceProxy = Proxy.newProxyInstance(toast.getClass().getClassLoader(), sService.getClass().getInterfaces(), toastProxy);
            Field sServiceField = toast.getClass().getDeclaredField("sService");
            sServiceField.setAccessible(true);
            //替換Toast類中已經(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的代理類實(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)系作者
  • 序言:七十年代末蝶柿,一起剝皮案震驚了整個(gè)濱河市丈钙,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌交汤,老刑警劉巖雏赦,帶你破解...
    沈念sama閱讀 217,406評(píng)論 6 503
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場(chǎng)離奇詭異芙扎,居然都是意外死亡星岗,警方通過(guò)查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,732評(píng)論 3 393
  • 文/潘曉璐 我一進(jìn)店門戒洼,熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái)俏橘,“玉大人,你說(shuō)我怎么就攤上這事圈浇×绕” “怎么了?”我有些...
    開(kāi)封第一講書(shū)人閱讀 163,711評(píng)論 0 353
  • 文/不壞的土叔 我叫張陵,是天一觀的道長(zhǎng)。 經(jīng)常有香客問(wèn)我古今,道長(zhǎng),這世上最難降的妖魔是什么怎茫? 我笑而不...
    開(kāi)封第一講書(shū)人閱讀 58,380評(píng)論 1 293
  • 正文 為了忘掉前任,我火速辦了婚禮,結(jié)果婚禮上轨蛤,老公的妹妹穿的比我還像新娘蜜宪。我一直安慰自己,他們只是感情好祥山,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,432評(píng)論 6 392
  • 文/花漫 我一把揭開(kāi)白布圃验。 她就那樣靜靜地躺著,像睡著了一般缝呕。 火紅的嫁衣襯著肌膚如雪澳窑。 梳的紋絲不亂的頭發(fā)上,一...
    開(kāi)封第一講書(shū)人閱讀 51,301評(píng)論 1 301
  • 那天供常,我揣著相機(jī)與錄音摊聋,去河邊找鬼。 笑死栈暇,一個(gè)胖子當(dāng)著我的面吹牛麻裁,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播源祈,決...
    沈念sama閱讀 40,145評(píng)論 3 418
  • 文/蒼蘭香墨 我猛地睜開(kāi)眼煎源,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼!你這毒婦竟也來(lái)了香缺?” 一聲冷哼從身側(cè)響起手销,我...
    開(kāi)封第一講書(shū)人閱讀 39,008評(píng)論 0 276
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤,失蹤者是張志新(化名)和其女友劉穎图张,沒(méi)想到半個(gè)月后锋拖,有當(dāng)?shù)厝嗽跇?shù)林里發(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 45,443評(píng)論 1 314
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡埂淮,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,649評(píng)論 3 334
  • 正文 我和宋清朗相戀三年姑隅,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片倔撞。...
    茶點(diǎn)故事閱讀 39,795評(píng)論 1 347
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡,死狀恐怖慕趴,靈堂內(nèi)的尸體忽然破棺而出痪蝇,到底是詐尸還是另有隱情,我是刑警寧澤冕房,帶...
    沈念sama閱讀 35,501評(píng)論 5 345
  • 正文 年R本政府宣布躏啰,位于F島的核電站,受9級(jí)特大地震影響耙册,放射性物質(zhì)發(fā)生泄漏给僵。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,119評(píng)論 3 328
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望帝际。 院中可真熱鬧蔓同,春花似錦、人聲如沸蹲诀。這莊子的主人今日做“春日...
    開(kāi)封第一講書(shū)人閱讀 31,731評(píng)論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)脯爪。三九已至则北,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間痕慢,已是汗流浹背尚揣。 一陣腳步聲響...
    開(kāi)封第一講書(shū)人閱讀 32,865評(píng)論 1 269
  • 我被黑心中介騙來(lái)泰國(guó)打工, 沒(méi)想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留掖举,地道東北人快骗。 一個(gè)月前我還...
    沈念sama閱讀 47,899評(píng)論 2 370
  • 正文 我出身青樓,卻偏偏與公主長(zhǎng)得像拇泛,于是被迫代替她去往敵國(guó)和親滨巴。 傳聞我的和親對(duì)象是個(gè)殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 44,724評(píng)論 2 354

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