android hook (Toast BadTokenException case)

What are Hooks?

 The hook technology is also called the hook function. Before the system calls the function, the hook program first captures the message, and the hook function first obtains the control right. At this time, the hook function can process (change) the execution behavior of the function, and can also be forced to end message delivery. To put it simply, it is to pull out the program of the system and turn it into a code fragment that we execute ourselves.

Implementation of Hook?

To implement hooks, we must know java's reflection and dynamic proxy.

​​​​​​Detailed Explanation of Java Reflection Mechanism

Case: Toast WindowManager$BadTokenException

Tips: We mainly focus on Android7.x in this small section of source code.

I believe that Android friends should have encountered token failure when they usually develop.

According to the normal process, this abnormality will not occur. But because in some cases, Androida message of a certain UI thread of the process is blocked. The method TNthat causes the 0 (display) message to come out is located after the message and has not been executed for a long time. At this time, the timeout detection of the service is over, and the record in the service is deleted . That is, as shown, delete happens before the process method. This leads to our exception above.showpostNotificationManagerWMStokentokenAndroidshow

The entire toast display principle and analysis You can take a complete look at the analysis of the QQ music technology team, (the source of the above picture is also the same) [ Android] Toast problem analysis (1) - Tencent Cloud Developer Community - Tencent Cloud

The token is valid is not reproduced in the sleep method, but after reading the Toast source code, the BadTokenException can be reproduced in another way:

val mw = getSystemService(WINDOW_SERVICE) as WindowManager
        val tv = TextView(this)
        tv.layoutParams = WindowManager.LayoutParams(1, 1)
        tv.text = "模拟toast悬浮窗"
        val params = WindowManager.LayoutParams()
        params.type = WindowManager.LayoutParams.TYPE_TOAST
        mw.addView(tv, params)
        Toast.makeText(this, "xxxx", Toast.LENGTH_SHORT).show()

Because toasts of type==TYPE_TOAST cannot be added repeatedly, a BadTokenException will also be reported. Next, we will pass this demo and use the hook solution to solve this exception.

Reading the source code, we found that the handleShow method in Android 7.0 Toast.java:

and on Android 8.0 Toast.java:

The comparison found that in Android 8.0, 8.0 has a layer of try catch protection when WindowManager performs addView, but it does not on 7.0. Then we can refer to the method of 8.0 and catch this exception directly.

Find the hook point

Try to hook static variables and singleton objects

Try to hook public objects and methods

Looking at the call chain, a message is sent in mHandler, and handlerShow is called after processing the message displayed by the toast in handlerMessage. mHandler is a variable in the TN class, and there is a variable mTN (TN class) in Toast. So it is very simple, we hook the point to locate this mTN, then reflect and replace the internal member variable mHandler of TN, and add a try-catch to the handleMessage method to protect it.

public class HookToastUtil {
    private static Field sField_N;
    private static Field sField_TN_Handler;
    private static Toast mToast;


    private HookToastUtil() {

    }

    public static void show(Context context, CharSequence message, int duration) {
        if (mToast == null) {
            mToast = Toast.makeText(context.getApplicationContext(), message, duration);
            hook(mToast);
        } else {
            mToast.setDuration(duration);
            mToast.setText(message);

            mToast.show();
        }
    }

    public static void show(Context context, @StringRes int resId, int duration) {
        if (mToast == null) {
            mToast = Toast.makeText(context.getApplicationContext(), resId, duration);
            hook(mToast);
        } else {
            mToast.setDuration(duration);
            mToast.setText(context.getString(resId));
        }
        mToast.show();
    }

    private static void hook(Toast toast) {
        if (Build.VERSION.SDK_INT != Build.VERSION_CODES.N_MR1) {
            return;
        }

        try {
            Class<?> cls = Class.forName("android.widget.Toast");
            sField_N = cls.getDeclaredField("mTN");
            sField_N.setAccessible(true);
            sField_TN_Handler = sField_N.getType().getDeclaredField("mHandler");
            sField_TN_Handler.setAccessible(true);
            Object tn = sField_N.get(toast);
            Handler handler = (Handler) sField_TN_Handler.get(tn);

            sField_TN_Handler.set(tn, new ReplaceHandler(handler));
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

    private static class ReplaceHandler extends Handler {
        private Handler tnHandler;

        public ReplaceHandler(Handler handler) {
            this.tnHandler = handler;
        }

        @Override
        public void handleMessage(Message msg) {
            try{
                tnHandler.handleMessage(msg);
            }catch (Exception e){
                e.printStackTrace();
            }
        }
    }
}

 Call HookToastUtil.show(this,"111",1000) to replace the previous Toast.makeText and no error will be reported

Guess you like

Origin blog.csdn.net/u013773608/article/details/130048177