Booster 系列之——爲系統 Bug 兜底

項目地址:github.com/didi/booste…java

背景

許多 Android 開發者可能常常遇到這樣的狀況:測試的時候好好的,一上線,各類系統的 crash 就報上來了,並且不少是偶現的,好比:android

  1. WindowManager$BadTokenException
  2. Resources.NotFoundException
  3. NullPointerException
  4. SecurityException
  5. IllegalArgumentException
  6. RuntimeException
  7. ......

不少狀況下,這些異常崩潰並非由 APP 致使的,並且堆棧中也沒有半點 APP 的影子,就拿 WindowManager$BadTokenException 來講,一部分是 Android 7.1 的 bug,一部分多是操做 Dialog 或者 Fragment 致使,若是是 APP 代碼邏輯的問題,很容易就能在堆棧中發現,那若是是由於系統致使的崩潰,咱們是否是就無能爲力了呢?git

修復系統 Bug

仍是拿 WindowManager$BadTokenException 來舉例子,若是是由於 Toast 致使的,不少人的第一反應就是自定義 Toast,固然,這徹底能解決問題,可是 Booster 提供了另外一種徹底不同的解決方案 —— 在構建期間將代碼中全部對 Toast.show(...) 方法的調用指令替換爲 ShadowToast.show(Toast)github

public class ShadowToast {

    /** * Fix {@code WindowManager$BadTokenException} for Android N * * @param toast * The original toast */
    public static void show(final Toast toast) {
        if (Build.VERSION.SDK_INT == 25) {
            workaround(toast).show();
        } else {
            toast.show();
        }
    }

    private static Toast workaround(final Toast toast) {
        final Object tn = getFieldValue(toast, "mTN");
        if (null == tn) {
            Log.w(TAG, "Field mTN of " + toast + " is null");
            return toast;
        }

        final Object handler = getFieldValue(tn, "mHandler");
        if (handler instanceof Handler) {
            if (setFieldValue(handler, "mCallback", new CaughtCallback((Handler) handler))) {
                return toast;
            }
        }

        final Object show = getFieldValue(tn, "mShow");
        if (show instanceof Runnable) {
            if (setFieldValue(tn, "mShow", new CaughtRunnable((Runnable) show))) {
                return toast;
            }
        }

        Log.w(TAG, "Neither field mHandler nor mShow of " + tn + " is accessible");
        return toast;
    }

}
複製代碼

這樣作的好處是,全部代碼(包括依賴的第三方 Library)都會被替換,並且徹底無不侵入,不再用擔憂 Toast 會崩潰了。算法

除了 Toast 會致使 WindowManager$BadTokenException 外,在 Activity 的生命週期回調中也常常出現,Booster 又有什麼樣的解決方案呢?—— 攔截 ActivityThreadmarkdown

public class ActivityThreadHooker {

    private volatile static boolean hooked;

    public static void hook() {
        if (hooked) {
            return;
        }

        Object thread = null;
        try {
            thread = android.app.ActivityThread.currentActivityThread();
        } catch (final Throwable t1) {
            Log.w(TAG, "ActivityThread.currentActivityThread() is inaccessible", t1);
            try {
                thread = getStaticFieldValue(android.app.ActivityThread.class, "sCurrentActivityThread");
            } catch (final Throwable t2) {
                Log.w(TAG, "ActivityThread.sCurrentActivityThread is inaccessible", t1);
            }
        }

        if (null == thread) {
            Log.w(TAG, "ActivityThread instance is inaccessible");
            return;
        }

        try {
            final Handler handler = getHandler(thread);
            if (null == handler || !(hooked = setFieldValue(handler, "mCallback", new ActivityThreadCallback(handler)))) {
                Log.i(TAG, "Hook ActivityThread.mH.mCallback failed");
            }
        } catch (final Throwable t) {
            Log.w(TAG, "Hook ActivityThread.mH.mCallback failed", t);
        }
        if(hooked) {
            Log.i(TAG, "Hook ActivityThread.mH.mCallback success!");
        }
    }

    private static Handler getHandler(final Object thread) {
        Handler handler;

        if (null != (handler = getFieldValue(thread, "mH"))) {
            return handler;
        }

        if (null != (handler = invokeMethod(thread, "getHandler"))) {
            return handler;
        }

        try {
            if (null != (handler = getFieldValue(thread, Class.forName("android.app.ActivityThread$H")))) {
                return handler;
            }
        } catch (final ClassNotFoundException e) {
            Log.w(TAG, "Main thread handler is inaccessible", e);
        }

        return null;
    }
}
複製代碼

有人可能會問,若是跟處理 Toast 的崩潰同樣,直接用 try-catch 大法這樣粗暴的處理方式的話,那 APP 自己的 bug 是否是就不能及時發現了呢?—— 確實是這樣!app

正是基於這樣的考慮,Booster 並非簡單粗暴的一塊兒兜住,雖然這樣作可讓崩潰率變得更好看,可是,APP 自己的問題也就被掩蓋了,我們但是對技術有追求的,這種掩耳盜鈴的事情我們怎麼可能會幹呢,那究竟是如何甄別異常是由 APP 引發的呢?—— 堆棧信息框架

class ActivityThreadCallback implements Handler.Callback {

    private static final String LOADED_APK_GET_ASSETS = "android.app.LoadedApk.getAssets";

    private static final String ASSET_MANAGER_GET_RESOURCE_VALUE = "android.content.res.AssetManager.getResourceValue";

    private static final String[] SYSTEM_PACKAGE_PREFIXES = {
            "java.",
            "android.",
            "androidx.",
            "dalvik.",
            "com.android.",
            ActivityThreadCallback.class.getPackage().getName() + "."
    };

    private final Handler mHandler;

    public ActivityThreadCallback(final Handler handler) {
        this.mHandler = handler;
    }

    @Override
    public final boolean handleMessage(final Message msg) {
        try {
            this.mHandler.handleMessage(msg);
        } catch (final NullPointerException e) {
            if (hasStackTraceElement(e, ASSET_MANAGER_GET_RESOURCE_VALUE, LOADED_APK_GET_ASSETS)) {
                abort(e);
            }
            rethrowIfNotCausedBySystem(e);
        } catch (final SecurityException
                | IllegalArgumentException
                | AndroidRuntimeException
                | WindowManager.BadTokenException e) {
            rethrowIfNotCausedBySystem(e);
        } catch (final Resources.NotFoundException e) {
            rethrowIfNotCausedBySystem(e);
            abort(e);
        } catch (final RuntimeException e) {
            final Throwable cause = e.getCause();
            if (((Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) && isCausedBy(cause, DeadSystemException.class))
                    || (isCausedBy(cause, NullPointerException.class) && hasStackTraceElement(e, LOADED_APK_GET_ASSETS))) {
                abort(e);
            }
            rethrowIfNotCausedBySystem(e);
        } catch (final Error e) {
            rethrowIfNotCausedBySystem(e);
            abort(e);
        }

        return true;
    }

    private static void rethrowIfNotCausedBySystem(final RuntimeException e) {
        if (!isCausedBySystem(e)) {
            throw e;
        }
    }

    private static void rethrowIfNotCausedBySystem(final Error e) {
        if (!isCausedBySystem(e)) {
            throw e;
        }
    }

    private static boolean isCausedBySystem(final Throwable t) {
        if (null == t) {
            return false;
        }

        for (Throwable cause = t; null != cause; cause = cause.getCause()) {
            for (final StackTraceElement element : cause.getStackTrace()) {
                if (!isSystemStackTrace(element)) {
                    return false;
                }
            }
        }

        return true;
    }

    private static boolean isSystemStackTrace(final StackTraceElement element) {
        final String name = element.getClassName();
        for (final String prefix : SYSTEM_PACKAGE_PREFIXES) {
            if (name.startsWith(prefix)) {
                return true;
            }
        }
        return false;
    }

    private static boolean hasStackTraceElement(final Throwable t, final String... traces) {
        return hasStackTraceElement(t, new HashSet<>(Arrays.asList(traces)));
    }

    private static boolean hasStackTraceElement(final Throwable t, final Set<String> traces) {
        if (null == t || null == traces || traces.isEmpty()) {
            return false;
        }

        for (final StackTraceElement element : t.getStackTrace()) {
            if (traces.contains(element.getClassName() + "." + element.getMethodName())) {
                return true;
            }
        }

        return hasStackTraceElement(t.getCause(), traces);
    }

    @SafeVarargs
    private static boolean isCausedBy(final Throwable t, final Class<? extends Throwable>... causes) {
        return isCausedBy(t, new HashSet<>(Arrays.asList(causes)));
    }

    private static boolean isCausedBy(final Throwable t, final Set<Class<? extends Throwable>> causes) {
        if (null == t) {
            return false;
        }

        if (causes.contains(t.getClass())) {
            return true;
        }

        return isCausedBy(t.getCause(), causes);
    }

    private static void abort(final Throwable t) {
        final int pid = Process.myPid();
        final String msg = "Process " + pid + " is going to be killed";

        if (null != t) {
            Log.w(TAG, msg, t);
        } else {
            Log.w(TAG, msg);
        }

        Process.killProcess(pid);
        System.exit(10);
    }

}
複製代碼

以上的異常處理中,包含了有不少細節的問題,好比:Android N 以上的版本在 APP 升級後首次啓動找不到 AssetManager 等等。因此針對這些異常的處理辦法就是 —— 不是系統致使的,統統拋出去,這樣,APP 自身的 bug 就能在第一時間被發現了。ide

反作用

在攔截 ActivityThread 後,將非系統異常拋出去雖然對於崩潰率來講收益明顯,可是給 APM 系統作異常聚合帶來了一些麻煩,由於不少 APM 系統的聚合算法也是根據堆棧來聚合的,不巧的是,這些被拋出來的異常最終都會被聚合到 ActivityThreadCallbackoop

總結

以上的這些解決方案,在 Booster 框架中都提供了現成的模塊:

關於如何集成,請參見:github.com/didi/booste…

相關文章
相關標籤/搜索