You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

协程作用域中处理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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.04.28 13:37:38