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
相关产品推荐
相关产品推荐

