
如果問前端、后端甚至游戲開發人員之間存在什么共同點,那就是我們都討厭應用產品出現 Bug,尤其是當這些錯誤導致應用崩潰時。而在應用發布后,監視應用程序中這些不斷增加的崩潰是一種極其不愉快的體驗。
不管應用程序的業務邏輯如何,都可能會因為運行的系統或平台問題而導致出現某些奇怪的崩潰現象。在 Android 中,從后台狀態恢復應用程序時可能會產生崩潰 —— 此類崩潰是意外發生的,而且僅通過查看崩潰日志,我們很難理解崩潰的具體原因以及解決問題,而本文討論了此類問題及其解決方法。
問題
在監視產品的崩潰日志時,我注意到一些問題與日俱增。該應用在正常測試條件下似乎運行良好,並且崩潰不可復現,直到應用程序從后台任務中進入前台。
每個 Android 應用程序都在其自己的進程中運行,並且操作系統已為該進程分配了一些內存。當用戶與其他應用程序交互時將應用程序置於后台時,如果應用程序沒有足夠的可用內存,則操作系統會終止你的應用程序進程。而這一情況通常發生在前台運行另一個需要更大手機內存 (RAM) 的應用程序時。
當應用程序進程被終止的時候,所有的單例對象和臨時數據都同時丟失了,而現在如果你返回你的應用程序,系統會創建一個新的進程,而你的應用程序會從你退出時候的 Activity 棧頂執行 Resume 函數恢復該 Activity。
由於此時你的所有的單例對象都丟失了,因此當這個 Activity 嘗試訪問相同的對象時,就會遇到空指針異常而崩潰退出。
這是個問題。在我們繼續討論解決方案之前,讓我們復現一下這種情況。
復現崩潰
- 在模擬器或通過 USB 電纜(譯者注:Android 11 也可使用 Wi-Fi 連接設備調試)連接的實際設備上使用 ADB 運行指令(如 Android Studio)運行的任何應用程序。
- 導航到任意一個頁面,然后按下“主頁”按鈕。
- 打開終端,鍵入以下命令,我們就可以獲取應用程序的進程 ID(PID)。
adb shell pidof com.darktheme.example
該命令的語法為 adb shell pidof APP_BUNDLE_ID
請記下你在終端窗口上看到的 PID(這可用於驗證現有的應用程序進程是否已被終止,並在我們恢復應用程序時啟動了新的進程)。
- 鍵入以下終端命令以終止你的應用程序進程
adb shell am kill com.darktheme.example
現在,從后台任務中打開你的應用程序,並檢查該應用程序是否崩潰。如果是,請不要擔心,我們將在下一部分中討論如何處理此問題。如果沒有,你可以松一口氣了,因為這是你應得的。
需要注意的是,從后台打開應用后,請重新獲取應用所屬進程的 PID。如果你在第 3 步中記下的 PID 與新的 PID 相等,則該過程並沒有被終止。
建議的解決方案
有兩種方法可以解決此問題。根據你所處的情況,你可以決定用哪一個方法來推進問題的解決:
解決方案 1:
一種簡便的解決方案是,當用戶從后台恢復應用程序時,讓應用程序檢查我們現有的應用程序進程是否被結束並重新創建。如果是,則可以導航回啟動界面,使其看起來像是一個應用程序的初始化界面。
你可以將以下代碼放在 BaseActivity 中:
override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) if (savedInstanceState != null) { // 獲取當前 PID val currentPID = android.os.Process.myPid().toString() // 比較當前 PID 與 保存的 PID 是否一致 if (currentPID != savedInstanceState.getString(PID)) { // 如果當前 PID 與 保存的 PID 不相同,意味着新的進程被創建,從 SplashActivity 重啟應用 val intent = Intent(applicationContext, SplashActivity::class.java) intent.flags = Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TASK startActivity(intent) finish() } } } override fun onSaveInstanceState(bundle: Bundle) { super.onSaveInstanceState(bundle) bundle.putString(PID, android.os.Process.myPid().toString()) }
- 通過覆寫
onSaveInstanceState()功能,你可以將你的 PID 打包保存下來。 - 在
onCreate()方法中,你需要比較當前 PID 和打包保存的 PID。 - 如果當前進程是是重新創建的流程,則重定向導航到 Splash Activity。
當用戶從后台導航回被結束了的應用程序時候,該應用程序將從 SplashActivity 重新啟動,就像是一次新的啟動。
這將防止應用程序訪問在進程重建過程中可能已丟失的數據,從而防止應用程序崩潰。
雖然此解決方案可以防止崩潰,但是這種方法其實就是重新啟動應用程序,而不是從中斷的位置恢復應用程序。如果你在發布應用后遇到此問題,並且急切地希望快速解決這個問題,則此解決方案應該能幫你大忙。
但是,如果你剛從頭開始開發,則解決方案 2 將是你的理想選擇,因為它可以做到從中斷的位置恢復應用程序。
解決方案 2:
現在,你肯定已經注意到可以利用“包”對象保存和訪問數據。與前面的示例中的操作類似,將每個 Activity / Fragment 中所有必要的信息保存下來。
由於我們訪問是被保存在“包”中的數據,這會避免應用程序崩潰,並且應用程序能從中斷處恢復。所有其他 Activity / Fragment 也會被重新創建。
對於 Fragment 中的 RecyclerView,做法應該是:
override fun onViewCreated(view: View, savedInstanceState: Bundle?) { super.onViewCreated(view, savedInstanceState) val recyclerView = view.findViewById(R.id.recyclerView) recyclerView.layoutManager = LinearLayoutManager(context) users = savedInstanceState?.getParcelableArrayList("userList") ?: viewModel.getUsers() rv.adapter = DataAdapter(users, this) } override fun onSaveInstanceState(outState: Bundle) { super.onSaveInstanceState(outState) outState.putParcelableArrayList("userList", users as ArrayList) }
- 通過覆寫
onSaveInstanceState()功能,我們可以將所需信息保存在 Bundle 對象中。 - 我們會讓應用程序檢查
onViewCreated()函數中捆綁包中的數據是否可用,如果不可用,則會通過訪問 ViewModel 的方法獲取數據。
結論
在 Android 平台上,由於進程被終止而導致的應用崩潰是很常見的。而如果我們使用較新的 Android 版本,我們可以注意到,出於節省電源的目的,大量的后台應用程序被強制結束運行了。
解決方案 1 可以快速解決你現有的應用崩潰問題。
但是,如果你正在從頭開始開發應用程序,我建議使用解決方案 2,因為它可以確保系統會從先前關閉的位置恢復該應用程序,因此帶來更好的用戶體驗。
研究此類崩潰的根本原因可能會挺困難的,因此我希望本文能夠以任何可能的方式對你有所幫助。請告訴我你們對文中討論的解決方案有何看法。
關注我,每天分享知識干貨,你要的,我都有~~~
