Android 7.x Toast BadTokenException处理

别裁伪体亲风雅,转益多师是汝师。这篇文章主要讲述Android 7.x Toast BadTokenException处理相关的知识,希望能为你提供帮助。
7.x版本,对Toast添加了Token验证,这本是对的,但是调用show()显示Toast时,如果有耗时操作卡住了主线程超过5秒,就会抛出BadTokenException的异常,而8.x系统开始,Google则在内部进行了try-catch。而7.x系统则是永久的痛,只能靠我们自己来修复了。
 

Android 7.x Toast BadTokenException处理

文章图片
修复方案一反射代理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异常"));

【Android 7.x Toast BadTokenException处理】作者:爱写代码的何蜀黍
链接:https://www.jianshu.com/p/256103c59d45

    推荐阅读