Android Room数据库触发SQLiteBlobTooBigException问题排查
首先,这个偶发崩溃的核心原因是单条places表的记录大小超过了SQLite CursorWindow的容纳阈值,而且只在查询结果包含那些"超大行"的时候才会触发——这就是为什么相同数据有时正常、有时崩溃的原因。
为什么会出现这个问题?
你现在用TypeConverter把List<Experience>、List<Image>这类集合转成JSON字符串存在places表的单个列里。当某个PlaceVO的otherExperiences或者images包含大量数据时,对应的JSON字符串会变得异常长,直接导致整行记录的总大小突破了CursorWindow的限制(Android默认CursorWindow大小通常在2MB左右,单条记录不能超过这个值的一半左右,具体取决于系统版本)。
举个例子:如果某个Place有20个Experience,每个Experience又包含多个Image和Highlight,转成JSON后可能会有几十KB甚至上百KB,当这样的行被包含在查询结果里,Cursor在加载的时候就会因为装不下这条记录而抛出SQLiteBlobTooBigException。
怎么解决?
最彻底、符合Room最佳实践的方案是拆分实体,用一对多关联表代替TypeConverter存储集合,把大字段从主表中剥离出来:
1. 拆分集合类型为独立表
比如针对otherExperiences,创建单独的experience表,用placeId作为外键关联places表:
@Entity( tableName = "experience", foreignKeys = [ForeignKey( entity = PlaceVO::class, parentColumns = ["placeId"], childColumns = ["placeId"], onDelete = ForeignKey.CASCADE )] ) data class Experience( @PrimaryKey val experienceId: String, val placeId: String, // 关联Place的外键 @Embedded val owner: User, val description: String?, val highlights: List<Highlight>?, val images: List<Image> = emptyList(), val createdDate: Date, val updatedDate: Date, var privacyLevel: AddExperience.Privacy? = null )
同理,images、primaryCategoryTags这些集合都可以拆成独立的关联表,每个元素对应一条记录,通过placeId和主表关联。
2. 用Room的Relation关联查询
之后,你可以创建一个包含关联数据的PlaceWithExperiences类,用来一次性查询Place和它的关联数据:
data class PlaceWithExperiences( @Embedded val place: PlaceVO, @Relation( parentColumn = "placeId", entityColumn = "placeId" ) val experiences: List<Experience> )
然后在PlacesDao里添加对应的查询方法:
@Transaction @Query("SELECT * FROM places WHERE placeId = :id") abstract fun getPlaceWithExperiences(id: String): Flowable<PlaceWithExperiences>
这样一来,places主表只存储基础的文本和数值字段,大小会大幅降低,再也不会出现行过大的问题;关联的集合数据存在单独的表中,Room会帮你自动关联查询。
其他临时缓解方案(不推荐长期使用)
- 限制集合数据量:和后端协商,返回每个Place的Experience、Image数量上限,或者前端存储时截断超出部分,但这会丢失数据,影响用户体验。
- 拆分查询:如果UI不需要立刻展示所有大字段,可以先查询Place的基础信息,当用户需要查看详情时再单独查询对应的Experience、Image数据,避免一次性加载超大行。
总结
偶发崩溃的本质是单条行大小超标,而根本原因是用JSON字符串存储大型集合——这违反了关系型数据库的设计原则。拆分实体为关联表是最根本的解决办法,既符合Room的最佳实践,也能彻底解决CursorWindow大小限制的问题。
内容的提问来源于stack exchange,提问作者MichelReap

