协程作用域中处理Google Maps任务标记列表引发异常的问题排查与解决
问题分析与解决方案
咱们一步步拆解这两个异常的根源,再给出针对性的修复方案:
为什么会触发这两个异常?
1. ConcurrentModificationException(并发修改异常)
你用的alreadyDisplayedMissions是普通的ArrayList,它不是线程安全的。你的代码存在多个线程/协程同时读写这个列表的场景:
- 在
Dispatchers.Default协程里执行filter、removeAll等修改操作 - 又在
Dispatchers.Main协程里调用add添加元素 - 最关键的是:用户多次滚动地图时,多个
displayMissions调用会同时启动,多个协程抢着修改同一个列表,直接触发并发修改异常。
2. IndexOutOfBoundsException(索引越界)
这个异常本质还是**多协程并发修改同一个ArrayList**导致的:当一个协程正在执行removeAll内部的遍历操作时,另一个协程可能已经修改了列表的长度,导致遍历到不存在的索引位置。
另外还有个隐藏坑:你没有取消之前的协程任务,用户滚动多次时,旧的displayMissions协程还在后台运行,会和新的协程冲突,重复修改标记列表。
具体修复步骤
1. 替换为线程安全的集合
把ArrayList换成CopyOnWriteArrayList,它专门针对并发场景设计,虽然写操作成本稍高,但地图标记的数量一般不会特别大,完全够用:
private val alreadyDisplayedMissions = CopyOnWriteArrayList<Pair<MissionHeaderEntity, Marker>>()
2. 取消旧的协程任务
添加一个Job对象跟踪当前运行的displayMissions协程,每次调用这个函数时先取消之前的任务,避免多个协程同时操作:
private var displayMissionsJob: Job? = null private fun displayMissions(newMissions : List<MissionHeaderEntity>) { // 先终止之前的任务,防止并发冲突 displayMissionsJob?.cancel() displayMissionsJob = lifecycleScope.launch(Dispatchers.Default) { // 原有的逻辑... } }
3. 优化集合操作的效率与安全性
原来的removeAll里用missionsToRemove.any判断,每次都要遍历整个missionsToRemove列表,效率很低,改成用ID集合快速判断:
// 先把要移除的任务ID存入Set val missionsToRemoveIds = missionsToRemove.map { it.first.id }.toSet() alreadyDisplayedMissions.removeAll { mission -> missionsToRemoveIds.contains(mission.first.id) }
4. 补全空安全检查
在主线程添加标记时,先判断googleMap和marker是否非空,避免潜在的空指针:
missionsToShow.forEach { (mission, options) -> googleMap?.let { map -> val marker = map.addMarker(options) marker?.tag = mission marker?.let { m -> alreadyDisplayedMissions.add(Pair(mission, m)) } } }
5. 过滤无效任务数据
在生成missionsToShow时,先判断经纬度是否非空,避免后续出现空指针:
val missionsToShow = newMissions.filter { newMission -> // 原有的过滤条件... }.mapNotNull { newMission -> // 确保经纬度不为空,否则跳过该任务 val lat = newMission.place?.address?.location?.latitude ?: return@mapNotNull null val lng = newMission.place?.address?.location?.longitude ?: return@mapNotNull null // 生成MarkerOptions的逻辑... }
完整修复后的代码
private val alreadyDisplayedMissions = CopyOnWriteArrayList<Pair<MissionHeaderEntity, Marker>>() private var displayMissionsJob: Job? = null private fun displayMissions(newMissions : List<MissionHeaderEntity>) { // 取消之前的任务,防止并发执行冲突 displayMissionsJob?.cancel() displayMissionsJob = lifecycleScope.launch(Dispatchers.Default) { // 找出需要移除的任务:不在新列表中,或者版本已更新 val missionsToRemove = alreadyDisplayedMissions.filter { oldPair -> val newMission = newMissions.find { it.id == oldPair.first.id } newMission == null || newMission != oldPair.first } // 用ID集合优化移除操作,提升效率 val removeIds = missionsToRemove.map { it.first.id }.toSet() alreadyDisplayedMissions.removeAll { removeIds.contains(it.first.id) } // 找出需要添加/更新的任务,过滤掉无效数据 val missionsToShow = newMissions.filter { newMission -> (newMission.firstMission == true && newMission.owned == true) || alreadyDisplayedMissions.none { it.first.id == newMission.id } || (newMission.place?.address?.location?.latitude != null && newMission.place?.address?.location?.longitude != null) }.mapNotNull { newMission -> val lat = newMission.place?.address?.location?.latitude ?: return@mapNotNull null val lng = newMission.place?.address?.location?.longitude ?: return@mapNotNull null val markerIcon = when(newMission.state) { MissionState.BOOKED -> markerMissionBooked MissionState.AVAILABLE -> markerMissionAvailable MissionState.PRE_RELEASED -> markerMissionPreReleased else -> markerMissionOwned } val markerBitmap = mapUtils.createMissionMarkerIcon(markerIcon, newMission.costing?.price.toCurrency(requireContext())) val markerLatLng = LatLng(lat, lng) val markerOptions = MarkerOptions() .position(markerLatLng) .icon(BitmapDescriptorFactory.fromBitmap(markerBitmap)) Pair(newMission, markerOptions) } lifecycleScope.launch(Dispatchers.Main) { // 移除不需要的标记 missionsToRemove.forEach { it.second.remove() } // 添加新标记,确保空安全 googleMap?.let { map -> missionsToShow.forEach { (mission, options) -> val marker = map.addMarker(options) marker?.tag = mission marker?.let { m -> alreadyDisplayedMissions.add(Pair(mission, m)) } } } } } }
核心修复要点
- 线程安全集合:从根源上解决并发修改的异常
- 取消旧协程:避免用户多次滚动时的任务冲突
- 优化集合操作:提升效率同时减少出错概率
- 空安全处理:消灭潜在的空指针异常
内容的提问来源于stack exchange,提问作者user17526539
相关产品推荐
相关产品推荐

