Android 7.x Toast BadTokenException处理

Posted AndroidAlvin

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Android 7.x Toast BadTokenException处理相关的知识,希望对你有一定的参考价值。

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 BadTokenException处理的主要内容,如果未能解决你的问题,请参考以下文章

Android开发——Toast知识

Android攻城狮重新认识Toast

Android Toast小解

Android:Toast

Android学习笔记(26):Toast提示信息框

android Toast大全(五种情形)建立属于你自己的Toast