Android 疑難雜症修復之路(一)- RemoteServiceException

android-vitals

 cover source: https://developer.android.com/topic/performance/vitals

Android 版本碎片化衍伸了許多問題,除了開發過程適配繁複外,對於一般 Android Developer 比較痛的點是常會遇到很多無法理解的 Crash 或是 ANR。 Google 在 2017 宣布在 Google Play Console 中引進 Android Vitals Dashboard,而影響 App ranking 的其中一項數據來源就是 Android Vitals,因次 Android Developer 常常被逼著需要思考如何解決各式無法理解無法重現的問題。個人過去曾經開發過 5千萬 DAU 的 Utility Apps,曾經解決的各式疑難雜症也算多了,因此打算開一個系列文章來紀錄過往或是現在曾經解決的 Android 技術問題。

這裡主要想解決的是 RemoteServiceException,如果 App 中有 startForegroundService 或是大量使用 Notifications,可能難免都會遇到在 Google Play Console 中列出的這個 Exception。 比較常見的說明是 startForegroundService 後沒有在啟動的 service 裡即時 (5秒內) call startForeground,但通常都是有 call,所以通常百思不得其解,我後來的理解是在這個跨 process 的過程中,其中一段經過 binder thread 也經過 main thread,其實中間可能有種種系統因素(也有可能自己卡)而延遲了 service creation(總感覺這是 Android system design problem)。 因此這類問題通常無法正面解決,只能通過一些非正規的方式來規避。

以下是通常 RemoteServiceException 的 stack trace:

android.app.RemoteServiceException:
at android.app.ActivityThread$H.handleMessage (ActivityThread.java:2101)
at android.os.Handler.dispatchMessage (Handler.java:108)
at android.os.Looper.loop (Looper.java:166)
at android.app.ActivityThread.main (ActivityThread.java:7425)
at java.lang.reflect.Method.invoke (Native Method)
at com.android.internal.os.Zygote$MethodAndArgsCaller.run (Zygote.java:245)
at com.android.internal.os.ZygoteInit.main (ZygoteInit.java:921)

RemoteServiceException 從名字上就已經說明是來源自 remote service 的異常,再異步通知 App 本身(雖然這通知是直接 crash 就是),也就是說一般 try / catch 的方式是無法避免這個 crash。

從 stack trace 找線索來規避問題的話可以看到最後觸發 RemoteServiceException 的點是在 ActivityThread 裡 inner class H 中的 handleMessage,從 H 的實作 source code 中可以看到他是繼承自 Handler:

    class H extends Handler {
        public static final int BIND_APPLICATION        = 110;
        @UnsupportedAppUsage
        public static final int EXIT_APPLICATION        = 111;
        @UnsupportedAppUsage
        public static final int RECEIVER                = 113;
        @UnsupportedAppUsage
        public static final int CREATE_SERVICE          = 114;
        @UnsupportedAppUsage
        public static final int SERVICE_ARGS            = 115;
        @UnsupportedAppUsage
        public static final int STOP_SERVICE            = 116;
...

所以應該繼續往 Handler 的實作看:

    /**
     * Handle system messages here.
     */
    public void dispatchMessage(@NonNull Message msg) {
        if (msg.callback != null) {
            handleCallback(msg);
        } else {
            if (mCallback != null) {
                if (mCallback.handleMessage(msg)) {
                    return;
                }
            }
            handleMessage(msg);
        }
    }

這裡有一個關鍵點是 mCallback,如果 mCallback 處理了該 message 的話那麼就不會 call 到 ActivityThread 中 inner class H 的 handleMessage 也就不會 crash 了。

因此規避此 Exception 的思路就是先取得 ActivityThread instance 然後設置其中 mH 的 callback 並針對 SCHEDULE_CRASH 回傳 true。

剛好 ActivityThread 本身就 keep 了對自己的 static reference:

private static volatile ActivityThread sCurrentActivityThread;

拿到 sCurrentActivityThread 再對其中的 mH 設定 mCallback 就搞定了。

    // 在 App 中實作此 Handler.Callback 並置換 mH.mCallback 來規避 Exception 
    private val callbackHandler = object: Handler.Callback {
        override fun handleMessage(message: Message): Boolean {
            if (msgHandlers.containsKey(message.what)) {
                return msgHandlers[message.what]?.invoke() == true
            }

            return false
        }
    }

完整的規避此 Exception 的實作可參考:https://github.com/kswlee/android-vital-fix。為了寫這系列文章,打算把這類疑難雜症修復寫成 library,所以如果不想寫 code 也不想思考,就直接引用此 module (已發佈至 JitPack) 簡單 call 一行就可以自動 fix 了。



Leave a Reply

發佈留言必須填寫的電子郵件地址不會公開。 必填欄位標示為 *