项目中遇到2个线程问题导致的apk崩溃,在这里总结记录下:
问题1现象:apk在接收到报警内容会弹窗,当报警数count为0时dialog消失,否则dialog显示,当apk同时收到两个报警消息时apk崩溃;
问题2现象:apk中做了呼叫等待逻辑,用list来管理对讲对象,进行呼叫拷机时发生apk崩溃;
问题1
- 原代码逻辑:
// 1. 监听未处理得报警数量
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)
}
}
}
}
- 问题分析
当apk同时接收到2条报警消息时,同时进入showAlarmDialog()中得到弹窗未显示得状态,然后同时进入到showOldAlarmDialog()方法中,当第1条报警逻辑判断dialog == null,于是进入if中创建Dialog并show,于此同时第2条报警判断dialog不为null,走到else中,判断dialog当前还未显示,走到dialog.show(),而第一条逻辑创建完Dialog也走到dialog.show(),这里得Dialog是FragmentDialog的实例,当show多次执行时会报错,apk崩溃。
java.lang.IllegalStateException: Fragment already added: AlarmingDialog{efecf6e (fb92ef90-db94-4b43-9ea9-f20faa1d75bb) dialog}
多条报警同时进入showOldAlarmDialog(),为什么会走到不同的条件中呢?
因为当接收第1条报警的线程走到DialogFragment的show()方法中,show()的最后一步提交事务commit()方法不是立即执行的,而是将事务添加到队列中,等待合适的时机执行,在这个等待的过程中正好处于dialog实例不为空且dialog未显示的情况,所以接收第2报警的线程进入到else的else中,也就是dialog.show(),在FragmentTransaction中再一次添加了相同的fragment导致apk崩溃报错。
public void show(@NonNull FragmentManager manager, @Nullable String tag) {
mDismissed = false;
mShownByMe = true;
FragmentTransaction ft = manager.beginTransaction();
ft.add(this, tag);
ft.commit();
}
- 解决办法
由于是多线程处理,同时接收到多条报警,会同时顺着逻辑执行,原代码逻辑没有确保线程安全,所以在第2步或第3步中加个锁,确保同一时间只有一个线程可以执行dialog相关的逻辑,我的处理办法是在第2步这里对代码块加了个synchronized内置锁,限制同一时间只能有一个线程进行判断,这样报警消息就有先后顺序,不会出现同时进入showOldAlarmDialog()的情况(已拷机测试)。
// 2. 存在未处理得报警,显示报警弹窗
override fun showAlarmDialog() {
synchronized(this){
// 显示报警弹窗前先判断弹窗是否正在显示
if (!Router.isOldAlarmDialogShow(childFragmentManager)) {
Router.showOldAlarmDialog(childFragmentManager)
}
}
}
DialogFragment中还提供了showNow()方法,方法最后事务会立即执行commitNow(),可以立即显示弹窗,这样接收第2报警的线程就不会进入第2个else逻辑中(未测试)。
问题2
背景:apk做了对讲功能,当有设备呼入时显示通话页面,apk支持呼叫等待,当前通话结束会优先显示当前等待列表中最先呼入的设备的呼叫请求页面。
- 原代码逻辑
<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做了对讲功能,当有设备呼入时跳转到通话页面CallActivity,CallActivity初始化数据时会打印并处理呼入等待列表dealingList中的对讲对象,在处理完成之前如果有新的设备呼入,更新dealingList的话会导致crash。
crash报错内容:
java.util.ConcurrentModificationException
java.util.ConcurrentModificationException是 Java 中的一种异常,通常在并发修改集合时发生,在源代码中,CallManager中对arrayList的操作使用了线程安全,但CallActivity中没有,所以在CallActivity中还在遍历dealingList的过程中对dealingList进行了添加操作,导致crash。
解决办法:
在所有使用dealingList的地方都改为线程安全的方法,原代码逻辑中CallManager对dealingList已经是线程安全的了,所有只需要把CallActivity中对dealingList也改为线程安全的,这样同一时刻只允许一个线程对dealingList进行操作,避免了并发使用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
}
}
}