公司的产品在线上一直会报错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,本人已经在评论区中提醒作者了(评论区第一条就是本人的知乎账号😊)
网友评论