項目中遇到2個線程問題導(dǎo)致的apk崩潰,在這里總結(jié)記錄下:
問題1現(xiàn)象:apk在接收到報警內(nèi)容會彈窗,當(dāng)報警數(shù)count為0時dialog消失,否則dialog顯示,當(dāng)apk同時收到兩個報警消息時apk崩潰;
問題2現(xiàn)象:apk中做了呼叫等待邏輯,用list來管理對講對象,進(jìn)行呼叫拷機時發(fā)生apk崩潰;
問題1
- 原代碼邏輯:
// 1. 監(jiān)聽未處理得報警數(shù)量
viewModel.unHandledCount.observe(viewLifecycleOwner, Observer {
it?.let { count ->
if (count != 0) {
showAlarmDialog()
} else {
hideAlarmDialog()
}
updataAlarmRecord(count)
}
})
// 2. 存在未處理得報警,顯示報警彈窗
override fun showAlarmDialog() {
// 顯示報警彈窗前先判斷彈窗是否正在顯示
if (!Router.isOldAlarmDialogShow(childFragmentManager)) {
Router.showOldAlarmDialog(childFragmentManager)
}
}
// 3. 彈窗顯示邏輯處理
override fun showOldAlarmDialog(fragmentManager: FragmentManager) {
runBlocking {
var dialog: AlarmingDialog? =
fragmentManager.findFragmentByTag(RouterPath.OLD_ALARM_DIALOG) as? AlarmingDialog
if (dialog == null) {
dialog = AlarmingDialog()
dialog.show(fragmentManager, RouterPath.OLD_ALARM_DIALOG)
} else {
if (dialog.dialog?.isShowing == true) {
dialog.dismiss()
} else {
dialog.show(fragmentManager, RouterPath.OLD_ALARM_DIALOG)
}
}
}
}
- 問題分析
當(dāng)apk同時接收到2條報警消息時,同時進(jìn)入showAlarmDialog()中得到彈窗未顯示得狀態(tài),然后同時進(jìn)入到showOldAlarmDialog()方法中,當(dāng)?shù)?條報警邏輯判斷dialog == null,于是進(jìn)入if中創(chuàng)建Dialog并show,于此同時第2條報警判斷dialog不為null,走到else中,判斷dialog當(dāng)前還未顯示,走到dialog.show(),而第一條邏輯創(chuàng)建完Dialog也走到dialog.show(),這里得Dialog是FragmentDialog的實例,當(dāng)show多次執(zhí)行時會報錯,apk崩潰。
java.lang.IllegalStateException: Fragment already added: AlarmingDialog{efecf6e (fb92ef90-db94-4b43-9ea9-f20faa1d75bb) dialog}
多條報警同時進(jìn)入showOldAlarmDialog(),為什么會走到不同的條件中呢?
因為當(dāng)接收第1條報警的線程走到DialogFragment的show()方法中,show()的最后一步提交事務(wù)commit()方法不是立即執(zhí)行的,而是將事務(wù)添加到隊列中,等待合適的時機執(zhí)行,在這個等待的過程中正好處于dialog實例不為空且dialog未顯示的情況,所以接收第2報警的線程進(jìn)入到else的else中,也就是dialog.show(),在FragmentTransaction中再一次添加了相同的fragment導(dǎo)致apk崩潰報錯。
public void show(@NonNull FragmentManager manager, @Nullable String tag) {
mDismissed = false;
mShownByMe = true;
FragmentTransaction ft = manager.beginTransaction();
ft.add(this, tag);
ft.commit();
}
- 解決辦法
由于是多線程處理,同時接收到多條報警,會同時順著邏輯執(zhí)行,原代碼邏輯沒有確保線程安全,所以在第2步或第3步中加個鎖,確保同一時間只有一個線程可以執(zhí)行dialog相關(guān)的邏輯,我的處理辦法是在第2步這里對代碼塊加了個synchronized內(nèi)置鎖,限制同一時間只能有一個線程進(jìn)行判斷,這樣報警消息就有先后順序,不會出現(xiàn)同時進(jìn)入showOldAlarmDialog()的情況(已拷機測試)。
// 2. 存在未處理得報警,顯示報警彈窗
override fun showAlarmDialog() {
synchronized(this){
// 顯示報警彈窗前先判斷彈窗是否正在顯示
if (!Router.isOldAlarmDialogShow(childFragmentManager)) {
Router.showOldAlarmDialog(childFragmentManager)
}
}
}
DialogFragment中還提供了showNow()方法,方法最后事務(wù)會立即執(zhí)行commitNow(),可以立即顯示彈窗,這樣接收第2報警的線程就不會進(jìn)入第2個else邏輯中(未測試)。
問題2
背景:apk做了對講功能,當(dāng)有設(shè)備呼入時顯示通話頁面,apk支持呼叫等待,當(dāng)前通話結(jié)束會優(yōu)先顯示當(dāng)前等待列表中最先呼入的設(shè)備的呼叫請求頁面。
- 原代碼邏輯
<CallActivity.kt>
override fun onResume() {
...
initData()
}
private fun initData() {
...
...
Log.d(TAG, "dealingList >>> ${CallManager.instance.dealingList}")
if (CallManager.instance.dealingList.find {
...
}
...
...
}
<CallManager.kt>
private fun addDealingVoiceTalkEventBean(talkBean: TalkBean) {
synchronized(dealingList) {
if (!dealingList.contains(talkBean)) {
dealingList.add(talkBean)
}
}
}
fun removeDealingVoiceTalkEventBean(talkBean: TalkBean?) {
if (talkBean== null) return
if (dealingList.size == 0) return
synchronized(dealingList) {
for (i in dealingList.indices) {
...
dealingList.removeAt(i)
break
}
}
}
- 問題分析
背景:apk做了對講功能,當(dāng)有設(shè)備呼入時跳轉(zhuǎn)到通話頁面CallActivity,CallActivity初始化數(shù)據(jù)時會打印并處理呼入等待列表dealingList中的對講對象,在處理完成之前如果有新的設(shè)備呼入,更新dealingList的話會導(dǎo)致crash。
crash報錯內(nèi)容:
java.util.ConcurrentModificationException
java.util.ConcurrentModificationException是 Java 中的一種異常,通常在并發(fā)修改集合時發(fā)生,在源代碼中,CallManager中對arrayList的操作使用了線程安全,但CallActivity中沒有,所以在CallActivity中還在遍歷dealingList的過程中對dealingList進(jìn)行了添加操作,導(dǎo)致crash。
解決辦法:
在所有使用dealingList的地方都改為線程安全的方法,原代碼邏輯中CallManager對dealingList已經(jīng)是線程安全的了,所有只需要把CallActivity中對dealingList也改為線程安全的,這樣同一時刻只允許一個線程對dealingList進(jìn)行操作,避免了并發(fā)使用dealingList的情況。
<CallActivity.kt>
override fun onResume() {
...
initData()
}
private fun initData() {
...
...
synchronized(CallManager.instance.dealingList) {
Log.d(TAG, "dealingList >>> ${CallManager.instance.dealingList}")
if (CallManager.instance.dealingList.find {
...
}
}
...
...
}
<CallManager.kt>
private fun addDealingVoiceTalkEventBean(talkBean: TalkBean) {
synchronized(dealingList) {
if (!dealingList.contains(talkBean)) {
dealingList.add(talkBean)
}
}
}
fun removeDealingVoiceTalkEventBean(talkBean: TalkBean?) {
if (talkBean== null) return
if (dealingList.size == 0) return
synchronized(dealingList) {
for (i in dealingList.indices) {
...
dealingList.removeAt(i)
break
}
}
}