美文网首页
Toast BadTokenException

Toast BadTokenException

作者: 勇敢地追 | 来源:发表于2021-02-04 16:47 被阅读0次

    公司的产品在线上一直会报错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

    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 代码

    public interface BadTokenListener {
        /**
         * 当Toast抛出BadTokenException时回调
         *
         * @param toast 发生异常的Toast实例
         */
        void onBadTokenCaught(@NonNull Toast toast);
    }
    

    2.mWM.addView添加try-catch

    注意这三句代码

    Context context = mView.getContext().getApplicationContext();
    mWM = (WindowManager)context.getSystemService(Context.WINDOW_SERVICE);
    mWM.addView(mView, mParams);
    

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

    /**
     * 只需要把 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

    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的那点事

    注:第一篇文章《Android 7.x Toast BadTokenException处理》里面有些小bug,本人已经在评论区中提醒作者了(评论区第一条就是本人的知乎账号😊)

    相关文章

      网友评论

          本文标题:Toast BadTokenException

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