Toast BadTokenException

2021-02-22 14:43:11 浏览数 (2)

公司的产品在线上一直会报错BadTokenException

Toast报错信息

从报错信息看应该是Toast的。而且都在7.1.2以下,也就是API25以下。查看Toast的handleShow方法(api27和api25),发现是因为 mWM.addView(mView, mParams); 这一段代码在 25 没有 try catch,而 27 是加了 try catch 的。 鉴于 handleShow 是 Toast 的内部类 TN 里面的方法。因此一共有两种思路

1.handleShow添加try-catch

对Toast进行Hook,替换Toast中TN对象的Handler,对分发消息handleMessage()方法进行try-catch

代码语言:javascript复制
public class SafeToast {
    private static Field mTN;
    private static Field mTNHandler;

    static {
        try {
            //反射获取TN对象
            mTN = Toast.class.getDeclaredField("mTN");
            mTN.setAccessible(true);
            mTNHandler = mTN.getType().getDeclaredField("mHandler");
            mTNHandler.setAccessible(true);
        } catch (Exception e) {
            Log.e("SafeToast", e.getMessage());
        }
    }

    public static void show(Context context, CharSequence message, int duration) {
        show(context, message, duration, null);
    }

    public static void show(Context context, CharSequence message, int duration, BadTokenListener badTokenListener) {
        Toast toast = Toast.makeText(context.getApplicationContext(), message, duration);
        hook(toast, badTokenListener);
        toast.setDuration(duration);
        toast.setText(message);
        toast.show();
    }

    public static void show(Context context, int resId, int duration) {
        show(context, resId, duration, null);
    }

    public static void show(Context context, int resId, int duration, BadTokenListener badTokenListener) {
        Toast toast = Toast.makeText(context.getApplicationContext(), resId, duration);
        hook(toast, badTokenListener);
        toast.setDuration(duration);
        toast.setText(context.getString(resId));
        toast.show();
    }

    private static void hook(Toast toast, BadTokenListener badTokenListener) {
        try {
            Object tn = mTN.get(toast);
            Handler originHandler = (Handler) mTNHandler.get(tn);
            mTNHandler.set(tn, new SafeHandler(toast, originHandler, badTokenListener));//用 SafeHandler 替换原来的 handler
        } catch (Throwable e) {
            e.printStackTrace();
        }
    }

    /**
     * 替换Toast原有的Handler
     */
    private static class SafeHandler extends Handler {
        private Toast mToast;
        private Handler mOriginImpl;
        private BadTokenListener mBadTokenListener;

        SafeHandler(Toast toast, Handler originHandler, BadTokenListener badTokenListener) {
            mToast = toast;
            mOriginImpl = originHandler;
            mBadTokenListener = badTokenListener;
        }

        @Override
        public void handleMessage(Message msg) {
            try {
                //需要委托给原Handler执行.原Handler会在这里面handleShow
                mOriginImpl.handleMessage(msg);
            } catch (WindowManager.BadTokenException e) {
                e.printStackTrace();
                if (mBadTokenListener != null) {
                    mBadTokenListener.onBadTokenCaught(mToast);
                }
            }
        }
    }
}

顺便贴上 BadTokenListener 代码

代码语言:javascript复制
public interface BadTokenListener {
    /**
     * 当Toast抛出BadTokenException时回调
     *
     * @param toast 发生异常的Toast实例
     */
    void onBadTokenCaught(@NonNull Toast toast);
}

2.mWM.addView添加try-catch

注意这三句代码

代码语言:javascript复制
Context context = mView.getContext().getApplicationContext();
mWM = (WindowManager)context.getSystemService(Context.WINDOW_SERVICE);
mWM.addView(mView, mParams);

从下往上,我们需要 (1)WindowManager的代理类WindowManagerWrapper (2)重写getSystemService,在里面获取WindowManagerWrapper (3)重写 getApplicationContext

代码语言:javascript复制
/**
 * 只需要把 SafeToastContext 替换原来的 Toast 里面 mView.getContext()
 */
public class SafeToastContext extends ContextWrapper {

    private Toast mToast;
    private BadTokenListener mBadTokenListener;

    public SafeToastContext(Context base, Toast toast) {
        super(base);
        mToast = toast;
    }

    public void setBadTokenListener(BadTokenListener badTokenListener) {
        mBadTokenListener = badTokenListener;
    }

    @Override
    public Context getApplicationContext() {
        //代理原本的Context
        return new ApplicationContextWrapper(super.getApplicationContext());
    }

    private class ApplicationContextWrapper extends ContextWrapper {

        public ApplicationContextWrapper(Context base) {
            super(base);
        }

        @Override
        public Object getSystemService(String name) {
            if (Context.WINDOW_SERVICE.equals(name)) {
                Context baseContext = getBaseContext();
                // 获取自定义 WindowManagerWrapper
                return new WindowManagerWrapper((WindowManager) baseContext.getSystemService(name));
            }
            return super.getSystemService(name);
        }
    }

    private class WindowManagerWrapper implements WindowManager {

        private WindowManager mImpl;

        public WindowManagerWrapper(WindowManager readImpl) {
            mImpl = readImpl;
        }

        /**
         * mWM.addView(mView, mParams); 在这里 try catch
         */
        @Override
        public void addView(View view, ViewGroup.LayoutParams params) {
            try {
                mImpl.addView(view, params);
            } catch (BadTokenException e) {
                e.printStackTrace();
                if (mBadTokenListener != null) {
                    mBadTokenListener.onBadTokenCaught(mToast);
                }
            }
        }

        @Override
        public void updateViewLayout(View view, ViewGroup.LayoutParams params) {
            mImpl.updateViewLayout(view, params);
        }

        @Override
        public void removeView(View view) {
            mImpl.removeView(view);
        }

        @Override
        public Display getDefaultDisplay() {
            return mImpl.getDefaultDisplay();
        }

        @Override
        public void removeViewImmediate(View view) {
            mImpl.removeViewImmediate(view);
        }
    }
}

然后用ToastCompat替换Toast

代码语言:javascript复制
public class ToastCompat extends Toast {

    private Toast mToast;

    public ToastCompat setBadTokenListener(BadTokenListener listener) {
        final Context context = getView().getContext();
        if (context instanceof SafeToastContext) {
            ((SafeToastContext) context).setBadTokenListener(listener);
        }
        return this;
    }

    public ToastCompat(Context context, Toast toast) {
        super(context);
        mToast = toast;
    }

    public static ToastCompat makeText(Context context, CharSequence text, int duration) {
        Toast toast = Toast.makeText(context, text, duration);
        setContextCompat(toast.getView(), new SafeToastContext(context, toast));
        return new ToastCompat(context, toast);
    }

    @Override
    public void setView(View view) {
        mToast.setView(view);
        setContextCompat(view, new SafeToastContext(view.getContext(), this));
    }

    /**
     * 反射设置View中的Context,Toast会获取View的Context来获取WindowManager
     */
    private static void setContextCompat(View view, Context context) {
        //7.1.1版本才进行处理
        if (Build.VERSION.SDK_INT == 25) {
            try {
                Field field = View.class.getDeclaredField("mContext");
                field.setAccessible(true);
                field.set(view, context);
            } catch (Throwable e) {
                e.printStackTrace();
            }
        }
    }

    @Override
    public void show() {
        mToast.show();
    }

    @Override
    public void cancel() {
        mToast.cancel();
    }

    @Override
    public View getView() {
        return mToast.getView();
    }

    @Override
    public void setDuration(int duration) {
        mToast.setDuration(duration);
    }

    @Override
    public int getDuration() {
        return mToast.getDuration();
    }

    @Override
    public void setMargin(float horizontalMargin, float verticalMargin) {
        mToast.setMargin(horizontalMargin, verticalMargin);
    }

    @Override
    public float getHorizontalMargin() {
        return mToast.getHorizontalMargin();
    }

    @Override
    public float getVerticalMargin() {
        return mToast.getVerticalMargin();
    }

    @Override
    public void setGravity(int gravity, int xOffset, int yOffset) {
        mToast.setGravity(gravity, xOffset, yOffset);
    }

    @Override
    public int getGravity() {
        return mToast.getGravity();
    }

    @Override
    public int getXOffset() {
        return mToast.getXOffset();
    }

    @Override
    public int getYOffset() {
        return mToast.getYOffset();
    }

    @Override
    public void setText(int resId) {
        mToast.setText(resId);
    }

    @Override
    public void setText(CharSequence s) {
        mToast.setText(s);
    }
}

参考文献

Android 7.x Toast BadTokenException处理 # Toast与Snackbar的那点事

0 人点赞