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

Moshi添加Factory时出现ArrayOutOfBoundsException问题排查求助

定位与解决Moshi初始化时的ArrayIndexOutOfBoundsException问题

问题根源分析

没错,你猜的方向完全正确——多线程并发操作同一个Moshi.Builder实例就是导致这个崩溃的核心原因。

具体拆解一下:

  • MoshiUtil里的lazyMoshi通过Kotlin默认的lazy初始化了一个单例的Moshi.Builder,而这个Builder内部是用ArrayList来存储添加的适配器的。
  • ArrayList的add方法本身不是线程安全的,当多个线程同时调用moshi.add(KotlinJsonAdapterFactory())(也就是操作同一个Builder的内部ArrayList)时,就会触发并发修改导致的ArrayIndexOutOfBoundsException。
  • 虽然Kotlin的lazy默认是线程安全的(保证Builder只被初始化一次),但初始化完成后,这个Builder实例会被多个线程共享,后续对它的add操作完全没有线程安全保障。

至于本地复现不了,是因为这种并发竞争场景很难稳定触发,只有在应用启动时的特殊多线程场景(比如某些组件在非主线程初始化时调用了getMyService)才会偶发出现,这也和你“仅上报数据有崩溃”的情况完全吻合。

修复方案

这里提供几种可行的修复方式,按推荐优先级排序:

1. 每次创建新的Moshi.Builder(最推荐)

放弃复用单例Builder,每次调用createMoshi都生成一个全新的Builder实例,从根本上避免并发共享问题:

object MoshiUtil {
    fun createMoshi(): Moshi.Builder {
        return Moshi.Builder().apply {
            add(DateAdapter())
            add(DefaultOnDataMismatchAdapter.newFactory(FeedItem::class.java, null))
            add(SkipListNullValuesAdapter.createFactory(Element::class.java))
            add(SkipListNullValuesAdapter.createFactory(Post::class.java))
            add(SkipListNullValuesAdapter.createFactory(MetadataItem::class.java))
            add(GeoGeometry::class.java, GeometryAdapter())
        }
    }
}

这样每个调用createMoshi的地方都会拿到独立的Builder,后续的add操作不会互相干扰,彻底解决并发问题。

2. 在MoshiUtil中提前完成所有Builder配置

如果希望复用Builder的初始化逻辑,可以把KotlinJsonAdapterFactory也提前加到Builder里,避免外部对Builder进行修改:

object MoshiUtil {
    private val lazyMoshi by lazy {
        Moshi.Builder().apply {
            add(DateAdapter())
            add(DefaultOnDataMismatchAdapter.newFactory(FeedItem::class.java, null))
            add(SkipListNullValuesAdapter.createFactory(Element::class.java))
            add(SkipListNullValuesAdapter.createFactory(Post::class.java))
            add(SkipListNullValuesAdapter.createFactory(MetadataItem::class.java))
            add(GeoGeometry::class.java, GeometryAdapter())
            add(KotlinJsonAdapterFactory()) // 提前添加,外部无需再修改
        }
    }

    fun createMoshi() = lazyMoshi
}

然后修改MyMoshiConverterFactory,移除外部的add操作:

object MyMoshiConverterFactory {
    fun create(setup: (Moshi.Builder.() -> Unit)? = null): Converter.Factory {
        val moshi = MoshiUtil.createMoshi()
        setup?.let { moshi.it() }
        // 移除这里的add操作
        return MoshiConverterFactory.create(moshi.build())
    }
}

这种方式保证Builder的所有修改都在初始化时完成,后续不会有并发修改的风险,但如果需要动态添加适配器的场景,就不太适用了。

3. 给Builder的操作添加线程安全保障

如果必须复用Builder且需要动态修改,可以在操作Builder时加同步锁:

object MyMoshiConverterFactory {
    fun create(setup: (Moshi.Builder.() -> Unit)? = null): Converter.Factory {
        val moshi = MoshiUtil.createMoshi()
        synchronized(moshi) { // 对Builder实例加锁,保证同一时间只有一个线程操作
            setup?.let { moshi.it() }
            moshi.add(KotlinJsonAdapterFactory())
        }
        return MoshiConverterFactory.create(moshi.build())
    }
}

这种方式能避免并发修改,但会带来一定的性能开销,而且如果其他地方也有修改Builder的操作,需要确保所有操作都加锁,否则还是会有问题。

额外建议

  • 尽量不要共享Moshi.Builder实例,因为它本身设计就是用于单次构建Moshi对象的,并非线程安全。
  • 如果需要复用Moshi配置,建议直接缓存构建好的Moshi实例(而不是Builder),因为Moshi实例是线程安全的,可以被多线程共享使用。

内容的提问来源于stack exchange,提问作者juanmeanwhile

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.05.07 00:02:48