Android Hook 解决诡异Toast

作者: 一个有故事的程序员 | 来源:发表于2023-04-27 10:58 被阅读0次

    开篇废话

    线上用户遇到一个问题,就是会经常弹出一个Toast,但是这个Toast的文案在端上和后台都没有找到,只能怀疑是第三方SDK弹出的,但是又不能一个一个问,问了也不一定帮你好好查,所以只能自食其力。

    遇到的问题

    如何Hook Toast,线上用户是Android 13。

    开始解决

    所以有两条路,一条是Hook所有调用Toast的地方,一条是通过Xposed框架解决,Xposed成本太高,所以采用Hook调用Toast的地方,方案采用站在巨人的肩膀上的第三方库me.ele:lancet-plugin。这个第三方库,属于在编译期,动态生成代码,所以我们可以在所有调用Toast的方法前后添加我们的代码,输出堆栈信息。

    添加第三方库

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

    buildscript {
        dependencies {
            classpath 'com.bytedance.tools.lancet:lancet-plugin-asm6:1.0.0' //看情况添加,是为了解决asm6问题
            classpath 'me.ele:lancet-plugin:1.0.6' //hook框架,必须添加
        }
    }
    

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

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

    在Module的build.gradle中导包hook工程。

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

    找到Hook点

    我们先来看一下Toast的源码,Toast源码还是比较简单的,我这里列举一些关键代码,方便我们进行观察。

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

    可以看出,我们需要注意的关键点是,用户在makeText()、setText()、setView()方法、这些方法设置了弹出的Toast的内容。重点来了,我们要对这些方法,用第三方库,进行Hook。

    Hook具体方法

    先放出实现类,大家观察一下。

    public class ToastsLancet {
    
        private static final String TAG = "HookToast";
    
        @TargetClass("android.widget.Toast")
        @Proxy("show")
        public void show() {
            Log.i(TAG, "Toast方法被调用----------> showToast()");
            showStackTraceLog();
            Origin.callVoid();
        }
    
        @TargetClass("android.widget.Toast")
        @Proxy("makeText")
        public static Toast makeText(Context context, CharSequence text, int duration) {
            Log.i(TAG, "Toast方法被调用----------> 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方法被调用----------> 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方法被调用----------> 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方法被调用----------> 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方法被调用----------> 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内容,然后进行打印内容和堆栈信息。
    @TargetClass代表Hook的类名,@Proxy代表是的Hook的方法名,Origin.callVoid();代表调用原来的代码,并且无返回值,如果不调用则不会显示Toast的了,Origin.call()是有返回值的调用方法。
    在setView()方法之后,我们需要通过遍历View的方式,找到TextView,再拿到Toast的内容,但是如果调用的地方是先进行setView(),再进行TextView.setText(),那么现在是拿不到的,只能通过相同方法去Hook TextView.setText()方法了,就不展开了。

    找到所有TextView的方法我也列出来。

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

    写在最后

    这里只提供一种解决问题的思路,除了这种方案,还可以通过上面提到的Xposed框架解决,这种方案是可以直接Hook到系统源码的,只不过我现在了解的在自己工程中使用,最高支持到Android 11,具体可以参考github-epic,不过作者主要精力在Xposed框架太极上,所以这里的文档都没有更新,类名方法名有小调整。

    更多内容戳这里(整理好的各种文集)

    相关文章

      网友评论

        本文标题:Android Hook 解决诡异Toast

        本文链接:https://www.haomeiwen.com/subject/azvbjdtx.html