Android 中意料之外的應用崩潰以及它們的解決方案
如果問前端、後端甚至遊戲開發人員之間存在什麼共同點,那就是我們都討厭應用產品出現 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,因為它可以確保系統會從先前關閉的位置恢復該應用程式,因此帶來更好的使用者體驗。
研究此類崩潰的根本原因可能會挺困難的,因此我希望本文能夠以任何可能的方式對你有所幫助。請告訴我你們對文中討論的解決方案有何看法。
關注我,每天分享知識乾貨,你要的,我都有~~~