Android7.1.1Toast崩潰解決方案

ICH發表於2018-09-10

概述

Toast作為Android應用中最常見的一種提示方式,由於簡單的api設計和簡潔的互動體驗被我們廣泛使用,但是這並代表他很完美,本文將記錄我在開發中遇到的問題。

背景

最近專案好多使用者反應有bug,然後看log出現了一個奇怪的問題,而且次數不很多,如下:

#1664 android.view.WindowManager$BadTokenException
Unable to add window -- window android.view.ViewRootImpl$W@4a51004 has already been added
android.view.ViewRootImpl.setView(ViewRootImpl.java:695)
android.view.ViewRootImpl.setView(ViewRootImpl.java:691)
android.view.WindowManagerGlobal.addView(WindowManagerGlobal.java:342)
android.view.WindowManagerImpl.addView(WindowManagerImpl.java:94)
android.widget.Toast$TN.handleShow(Toast.java:506)
android.widget.Toast$TN$2.handleMessage(Toast.java:389)
android.os.Handler.dispatchMessage(Handler.java:102)
android.os.Looper.loop(Looper.java:154) android.app.ActivityThread.main(ActivityThread.java:6292) java.lang.reflect.Method.invoke(Native Method)
com.android.internal.os.ZygoteInit$MethodAndArgsCaller.run(ZygoteInit.java:906)
com.android.internal.os.ZygoteInit.main(ZygoteInit.java:796)

複製程式碼

然後發現居然都是7.1.1裝置才出現的,見下圖:

Android7.1.1Toast崩潰解決方案

Toast顯示與隱藏

首先Toast顯示依賴於一個視窗,這個視窗被WMS管理(WindowManagerService),當需要show的時候這個請求會放在WMS請求佇列中,並且會傳遞一個TN型別的Bider物件給WMS,WMS並生成一個token傳遞給Android進行顯示與隱藏,但是如果UI執行緒的某個執行緒發生了阻塞,並且已經NotificationManager檢測已經超時就不刪除token記錄,此時token已經過期,阻塞結束的時候再顯示的時候就發生了異常。

在android7.1.1的Toast原始碼handleShow是這樣寫的:

mWM.addView(mView, mParams);
複製程式碼

而在8.0則是這樣的:

 try {
                    mWM.addView(mView, mParams);
                    trySendAccessibilityEvent();
                } catch (WindowManager.BadTokenException e) {
                    /* ignore */
                }
複製程式碼

到這裡能看到在發生異常的時候使用了try catch捕獲,程式不會掛掉

解決方案

/**
 * @author CH
 * @date 2018/6/26
 * 部分7.1.1手機崩潰Toast解決方案
 */
public class ToastCompat {
    private static Field sField_TN;
    private static Field sField_TN_Handler;
    private Toast mToast;

    static {
        try {
            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) {
        }
    }

    private static void hook(Toast toast) {
        try {
            Object tn = sField_TN.get(toast);
            Handler preHandler = (Handler) sField_TN_Handler.get(tn);
            sField_TN_Handler.set(tn, new SafelyHandlerWarpper(preHandler));
        } catch (Exception e) {
        }
    }

    public void showToast(Context context, CharSequence cs, int length) {
        if (mToast == null) {
            mToast = Toast.makeText(context, cs, length);
        } else {
            mToast.setText(cs);
        }
        hook(mToast);
        mToast.show();
    }

    public static class SafelyHandlerWarpper extends Handler {
        private Handler impl;

        public SafelyHandlerWarpper(Handler impl) {
            this.impl = impl;
        }

        @Override
        public void dispatchMessage(Message msg) {
            try {
                super.dispatchMessage(msg);
            } catch (Exception e) {
            }
        }

        @Override
        public void handleMessage(Message msg) {
            impl.handleMessage(msg);//需要委託給原Handler執行
        }
    }
}
複製程式碼

簡單來說就是通過反射注入在發生異常的地方進行try catch

宣告:此解決方案參考QQ音樂團隊

www.itboth.com/d/jiY73qyeA…

相關文章