Android Hook 解決詭異Toast

開(kāi)篇廢話

線上用戶遇到一個(gè)問(wèn)題,就是會(huì)經(jīng)常彈出一個(gè)Toast,但是這個(gè)Toast的文案在端上和后臺(tái)都沒(méi)有找到,只能懷疑是第三方SDK彈出的,但是又不能一個(gè)一個(gè)問(wèn),問(wèn)了也不一定幫你好好查,所以只能自食其力。

遇到的問(wèn)題

如何Hook Toast,線上用戶是Android 13。

開(kāi)始解決

所以有兩條路,一條是Hook所有調(diào)用Toast的地方,一條是通過(guò)Xposed框架解決,Xposed成本太高,所以采用Hook調(diào)用Toast的地方,方案采用站在巨人的肩膀上的第三方庫(kù)me.ele:lancet-plugin。這個(gè)第三方庫(kù),屬于在編譯期,動(dòng)態(tài)生成代碼,所以我們可以在所有調(diào)用Toast的方法前后添加我們的代碼,輸出堆棧信息。

添加第三方庫(kù)

在Porject的build.gradle中添加hook插件。

buildscript {
    dependencies {
        classpath 'com.bytedance.tools.lancet:lancet-plugin-asm6:1.0.0' //看情況添加,是為了解決asm6問(wèn)題
        classpath 'me.ele:lancet-plugin:1.0.6' //hook框架,必須添加
    }
}

在Module的build.gradle頂部中添加引用插件。

apply plugin: 'com.glazero.android.spi'

在Module的build.gradle中導(dǎo)包hook工程。

dependencies {
    implementation 'me.ele:lancet-base:1.0.6'
}

找到Hook點(diǎn)

我們先來(lái)看一下Toast的源碼,Toast源碼還是比較簡(jiǎn)單的,我這里列舉一些關(guān)鍵代碼,方便我們進(jìn)行觀察。

public class Toast {

    @Nullable
    private View mNextView;
    @Nullable
    private CharSequence mText;

    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) {
        if (Compatibility.isChangeEnabled(CHANGE_TEXT_TOASTS_IN_THE_SYSTEM)) {
            Toast result = new Toast(context, looper);
            result.mText = text;
            result.mDuration = duration;
            return result;
        } else {
            Toast result = new Toast(context, looper);
            View v = ToastPresenter.getTextToastView(context, text);
            result.mNextView = v;
            result.mDuration = duration;
            return result;
        }
    }

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

    public Toast(@NonNull Context context, @Nullable Looper looper) {

    }

    public void show() {

    }

    public void setText(@StringRes int resId) {
        setText(mContext.getText(resId));
    }

    public void setText(CharSequence s) {
        if (Compatibility.isChangeEnabled(CHANGE_TEXT_TOASTS_IN_THE_SYSTEM)) {
            if (mNextView != null) {
                throw new IllegalStateException(
                        "Text provided for custom toast, remove previous setView() calls if you "
                                + "want a text toast instead.");
            }
            mText = s;
        } else {
            if (mNextView == null) {
                throw new RuntimeException("This Toast was not created with Toast.makeText()");
            }
            TextView tv = mNextView.findViewById(com.android.internal.R.id.message);
            if (tv == null) {
                throw new RuntimeException("This Toast was not created with Toast.makeText()");
            }
            tv.setText(s);
        }
    }
    
    @Deprecated
    public void setView(View view) {
        mNextView = view;
    }

}

可以看出,我們需要注意的關(guān)鍵點(diǎn)是,用戶在makeText()、setText()、setView()方法、這些方法設(shè)置了彈出的Toast的內(nèi)容。重點(diǎn)來(lái)了,我們要對(duì)這些方法,用第三方庫(kù),進(jìn)行Hook。

Hook具體方法

先放出實(shí)現(xiàn)類,大家觀察一下。

public class ToastsLancet {

    private static final String TAG = "HookToast";

    @TargetClass("android.widget.Toast")
    @Proxy("show")
    public void show() {
        Log.i(TAG, "Toast方法被調(diào)用----------> showToast()");
        showStackTraceLog();
        Origin.callVoid();
    }

    @TargetClass("android.widget.Toast")
    @Proxy("makeText")
    public static Toast makeText(Context context, CharSequence text, int duration) {
        Log.i(TAG, "Toast方法被調(diào)用----------> makeText(text)");
        Log.i(TAG, "text = " + text);
        showStackTraceLog();
        return (Toast) Origin.call();
    }

    @TargetClass("android.widget.Toast")
    @Proxy("makeText")
    public static Toast makeText(Context context, int res, int duration) {
        Log.i(TAG, "Toast方法被調(diào)用----------> makeText(res)");
        Log.i(TAG, "text = " + Application.getString(res));
        showStackTraceLog();
        return (Toast) Origin.call();
    }

    @TargetClass("android.widget.Toast")
    @Proxy("setText")
    public void setText(CharSequence text) {
        Log.i(TAG, "Toast方法被調(diào)用----------> setText(text)");
        Log.i(TAG, "text = " + text);
        showStackTraceLog();
        Origin.callVoid();
    }

    @TargetClass("android.widget.Toast")
    @Proxy("setText")
    public void setText(int res) {
        Log.i(TAG, "Toast方法被調(diào)用----------> setText(res)");
        Log.i(TAG, "text = " + Application.getString(res));
        showStackTraceLog();
        Origin.callVoid();
    }

    @TargetClass("android.widget.Toast")
    @Proxy("setView")
    public void setView(View view) {
        List<TextView> textViewList = FindViewHelper.findTextView(view);
        for (TextView textView : textViewList) {
            Log.i(TAG, "Toast方法被調(diào)用----------> setView(view)");
            Log.i(TAG, "text = " + textView.getText().toString());
            showStackTraceLog();
        }
        Origin.callVoid();
    }

    private static void showStackTraceLog() {
        //打印堆棧信息
        Log.i(TAG, Log.getStackTraceString(new Throwable()));
    }

}

Application.getString()方法,大家可以替換成自己的方法,去獲取到具體的Toast內(nèi)容,然后進(jìn)行打印內(nèi)容和堆棧信息。
@TargetClass代表Hook的類名,@Proxy代表是的Hook的方法名,Origin.callVoid();代表調(diào)用原來(lái)的代碼,并且無(wú)返回值,如果不調(diào)用則不會(huì)顯示Toast的了,Origin.call()是有返回值的調(diào)用方法。
在setView()方法之后,我們需要通過(guò)遍歷View的方式,找到TextView,再拿到Toast的內(nèi)容,但是如果調(diào)用的地方是先進(jìn)行setView(),再進(jìn)行TextView.setText(),那么現(xiàn)在是拿不到的,只能通過(guò)相同方法去Hook TextView.setText()方法了,就不展開(kāi)了。

找到所有TextView的方法我也列出來(lái)。

public class FindViewHelper {

    public static List<TextView> findTextView(View view) {
        if (view == null) {
            return new ArrayList<>();
        }
        List<TextView> visited = new ArrayList<>();
        List<View> unvisited = new ArrayList<>();
        unvisited.add(view);
        while (!unvisited.isEmpty()) {
            View child = unvisited.remove(0);
            if (child instanceof TextView) {
                visited.add((TextView) child);
            }
            if (!(child instanceof ViewGroup)){
                continue;
            }
            ViewGroup group = (ViewGroup) child;
            final int childCount = group.getChildCount();
            for (int i=0; i < childCount; i++) {
                unvisited.add(group.getChildAt(i));
            }
        }
        return visited;
    }

}

寫在最后

這里只提供一種解決問(wèn)題的思路,除了這種方案,還可以通過(guò)上面提到的Xposed框架解決,這種方案是可以直接Hook到系統(tǒng)源碼的,只不過(guò)我現(xiàn)在了解的在自己工程中使用,最高支持到Android 11,具體可以參考github-epic,不過(guò)作者主要精力在Xposed框架太極上,所以這里的文檔都沒(méi)有更新,類名方法名有小調(diào)整。

更多內(nèi)容戳這里(整理好的各種文集)

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

  • afinalAfinal是一個(gè)android的ioc,orm框架 https://github.com/yangf...
    passiontim閱讀 15,898評(píng)論 2 45
  • afinalAfinal是一個(gè)android的ioc,orm框架 https://github.com/yangf...
    wgl0419閱讀 6,602評(píng)論 1 9
  • 之前在學(xué)習(xí)Fragment和總結(jié)Android異步操作的時(shí)候會(huì)在很多blog中看到對(duì)Configuration C...
    IOXusu閱讀 4,849評(píng)論 0 2
  • Toast是一個(gè)View視圖,快速的為用戶顯示少量的信息。Toast在應(yīng)用程序上浮動(dòng)顯示信息給用戶,它永遠(yuǎn)不會(huì)獲得...
    testshao閱讀 540評(píng)論 0 0
  • Toast是一個(gè)View視圖,快速的為用戶顯示少量的信息。Toast在應(yīng)用程序上浮動(dòng)顯示信息給用戶,它永遠(yuǎn)不會(huì)獲得...
    cxm11閱讀 583評(píng)論 0 4

友情鏈接更多精彩內(nèi)容