Android--項目中遇到的線程問題

項目中遇到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
            }
        }
    }
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容