美文网首页
Android 7.x Toast BadTokenExcept

Android 7.x Toast BadTokenExcept

作者: zhx喜籽 | 来源:发表于2020-03-09 20:31 被阅读0次

    <meta charset="utf-8">

    7.x版本,对Toast添加了Token验证,这本是对的,但是调用show()显示Toast时,如果有耗时操作卡住了主线程超过5秒,就会抛出BadTokenException的异常,而8.x系统开始,Google则在内部进行了try-catch。而7.x系统则是永久的痛,只能靠我们自己来修复了。

    修复方案一

    反射代理View的Context,Context内进行try-catch,处理Toast的BadTokenException问题

    • BadTokenListener,Toast抛出BadTokenException监听器
    public interface BadTokenListener {
        /**
         * 当Toast抛出BadTokenException时回调
         *
         * @param toast 发生异常的Toast实例
         */
        void onBadTokenCaught(@NonNull Toast toast);
    }
    
    
    • SafeToastContext,包裹Toast使用的Context
    public class SafeToastContext extends ContextWrapper {
        private Toast mToast;
        private BadTokenListener mBadTokenListener;
    
        public SafeToastContext(Context base, Toast toast) {
            super(base);
            mToast = toast;
        }
    
        public void setBadTokenListener(@NonNull 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)) {
                    //获取原来的WindowManager,交给WindowManagerWrapper代理,捕获BadTokenException异常
                    Context baseContext = getBaseContext();
                    return new WindowManagerWrapper((WindowManager) baseContext.getSystemService(name));
                }
                return super.getSystemService(name);
            }
        }
    
        private class WindowManagerWrapper implements WindowManager {
            /**
             * 被包裹的WindowManager实例
             */
            private WindowManager mImpl;
    
            public WindowManagerWrapper(@NonNull WindowManager readImpl) {
                mImpl = readImpl;
            }
    
            @Override
            public Display getDefaultDisplay() {
                return mImpl.getDefaultDisplay();
            }
    
            @Override
            public void removeViewImmediate(View view) {
                mImpl.removeViewImmediate(view);
            }
    
            @Override
            public void addView(View view, ViewGroup.LayoutParams params) {
                //在addView动刀,捕获BadTokenException异常
                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);
            }
        }
    }
    
    
    • ToastCompat,替代Toast的门面
    public class ToastCompat extends Toast {
        private Toast mToast;
    
        public ToastCompat(Context context, Toast toast) {
            super(context);
            mToast = toast;
        }
    
        public static ToastCompat makeText(Context context, CharSequence text, int duration) {
            @SuppressLint("ShowToast")
            Toast toast = Toast.makeText(context, text, duration);
            setContextCompat(toast.getView(), context);
            return new ToastCompat(context, toast);
        }
    
        public static Toast makeText(Context context, @StringRes int resId, int duration)
                throws Resources.NotFoundException {
            return makeText(context, context.getResources().getText(resId), duration);
        }
    
        public @NonNull
        ToastCompat setBadTokenListener(@NonNull BadTokenListener listener) {
            final Context context = getView().getContext();
            if (context instanceof SafeToastContext) {
                ((SafeToastContext) context).setBadTokenListener(listener);
            }
            return this;
        }
    
        @Override
        public void show() {
            mToast.show();
        }
    
        @Override
        public void setDuration(int duration) {
            mToast.setDuration(duration);
        }
    
        @Override
        public void setGravity(int gravity, int xOffset, int yOffset) {
            mToast.setGravity(gravity, xOffset, yOffset);
        }
    
        @Override
        public void setMargin(float horizontalMargin, float verticalMargin) {
            mToast.setMargin(horizontalMargin, verticalMargin);
        }
    
        @Override
        public void setText(int resId) {
            mToast.setText(resId);
        }
    
        @Override
        public void setText(CharSequence s) {
            mToast.setText(s);
        }
    
        @Override
        public void setView(View view) {
            mToast.setView(view);
            setContextCompat(view, new SafeToastContext(view.getContext(), this));
        }
    
        @Override
        public float getHorizontalMargin() {
            return mToast.getHorizontalMargin();
        }
    
        @Override
        public float getVerticalMargin() {
            return mToast.getVerticalMargin();
        }
    
        @Override
        public int getDuration() {
            return mToast.getDuration();
        }
    
        @Override
        public int getGravity() {
            return mToast.getGravity();
        }
    
        @Override
        public int getXOffset() {
            return mToast.getXOffset();
        }
    
        @Override
        public int getYOffset() {
            return mToast.getYOffset();
        }
    
        @Override
        public View getView() {
            return mToast.getView();
        }
    
        @NonNull
        public Toast getBaseToast() {
            return mToast;
        }
    
        /**
         * 反射设置View中的Context,Toast会获取View的Context来获取WindowManager
         */
        private static void setContextCompat(@NonNull View view, @NonNull 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();
                }
            }
        }
    }
    
    
    • 使用ToastCompat代替Toast
    ToastCompat.makeText(context,"我是Toast的内容", Toast.LENGTH_SHORT)
        .setBadTokenListener(toast ->
                Logger.d("Toast:" + toast + " => 抛出BadTokenException异常")
        )
        .show();
    
    

    修复方案二

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

    public class SafeToast {
        private static Field sField_TN;
        private static Field sField_TN_Handler;
    
        static {
            try {
                //反射获取TN对象
                sField_TN = Toast.class.getDeclaredField("mTN");
                sField_TN.setAccessible(true);
                sField_TN_Handler = sField_TN.getType().getDeclaredField("mHandler");
                sField_TN_Handler.setAccessible(true);
            } catch (Exception e) {
                e.printStackTrace();
            }
        }
    
        private SafeToast() {
        }
    
        @SuppressLint("ShowToast")
        public static void show(Context context, CharSequence message, int duration) {
            show(context, message, duration, null);
        }
    
        @SuppressLint("ShowToast")
        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();
        }
    
        @SuppressLint("ShowToast")
        public static void show(Context context, @StringRes int resId, int duration) {
            show(context, resId, duration, null);
        }
    
        @SuppressLint("ShowToast")
        public static void show(Context context, @StringRes 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 = sField_TN.get(toast);
                Handler originHandler = (Handler) sField_TN_Handler.get(tn);
                sField_TN_Handler.set(tn, new SafeHandler(toast, originHandler, badTokenListener));
            } 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 dispatchMessage(Message msg) {
                //对分发Message的处理方法进行try-catch
                try {
                    super.dispatchMessage(msg);
                } catch (WindowManager.BadTokenException e) {
                    e.printStackTrace();
                    if (mBadTokenListener != null) {
                        mBadTokenListener.onBadTokenCaught(mToast);
                    }
                }
            }
    
            @Override
            public void handleMessage(Message msg) {
                //需要委托给原Handler执行
                mOriginImpl.handleMessage(msg);
            }
        }
    }
    
    
    • 使用SafeToast代替Toast
    SafeToast.show(context,"我是Toast的内容", Toast.LENGTH_SHORT, toast ->
                Logger.d("Toast:" + toast + " => 抛出BadTokenException异常"));
    

    作者:爱写代码的何蜀黍
    链接:https://www.jianshu.com/p/256103c59d45

    相关文章

      网友评论

          本文标题:Android 7.x Toast BadTokenExcept

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