基于Compose+Room的Android聊天应用性能问题求助
聊天应用页面跳转帧丢包问题排查与优化求助
我正在开发一款聊天应用,发现在从聊天列表页跳转到特定聊天页面时出现帧丢包,甚至默认的NavHost动画都被跳过。所有测试均在开启混淆的release构建下完成。
消息数据通过Room从本地数据库获取(作为唯一可信数据源),但消息UI也较为复杂,因此这两部分都是优化候选方向。
宏基准测试代码
我运行了一系列宏基准测试来定位问题方向,测试代码如下:
@Test fun navigateToChat() = benchmarkRule.measureRepeated( packageName = "my.package", metrics = listOf(FrameTimingMetric()), iterations = 50, startupMode = StartupMode.COLD, compilationMode = CompilationMode.Ignore() ) { pressHome() startActivityAndWait() device.waitForIdle() val chatItem = device.findObject(By.text("Some guy's name")) chatItem.click() device.wait(Until.hasObject(By.text("Some message text")), 500) }
基准测试结果
基础场景结果
考虑到该聊天仅含12条消息,且测试设备为Pixel 7a,以下结果非常糟糕:
DB read, with UI (control, vector chat background): frameDurationCpuMs P50 21.6, P90 136.4, P95 161.8, P99 249.3 frameOverrunMs P50 44.8, P90 183.7, P95 238.3, P99 383.6
多轮测试结果
我通过修改数据库交互和/或UI渲染进行多轮测试,结果如下:
DB read, with UI (no chat BG): frameDurationCpuMs P50 26.4, P90 139.9, P95 161.0, P99 192.3 frameOverrunMs P50 54.3, P90 177.9, P95 219.0, P99 280.6 DB read, with UI (raster chat BG): frameDurationCpuMs P50 31.4, P90 155.3, P95 169.3, P99 262.2 frameOverrunMs P50 64.4, P90 183.4, P95 235.4, P99 307.8 DB read, with UI (raster chat BG, no shadow on a custom shape): frameDurationCpuMs P50 17.5, P90 95.8, P95 137.9, P99 166.4 frameOverrunMs P50 14.5, P90 146.9, P95 169.5, P99 200.7 DB read, no UI: frameDurationCpuMs P50 6.3, P90 45.5, P95 81.9, P99 121.3 frameOverrunMs P50 -1.8, P90 71.7, P95 106.3, P99 157.1 DB read, trivial UI (Text instead of MessageBubble): frameDurationCpuMs P50 17.7, P90 97.6, P95 146.2, P99 239.6 frameOverrunMs P50 26.2, P90 162.9, P95 217.7, P99 252.9 No DB read, UI provided with the same data by hand: frameDurationCpuMs P50 14.7, P90 133.2, P95 211.1, P99 277.6 frameOverrunMs P50 20.1, P90 178.5, P95 231.0, P99 266.4 No DB read, trivial UI: frameDurationCpuMs P50 9.8, P90 79.3, P95 127.2, P99 281.5 frameOverrunMs P50 8.7, P90 124.4, P95 215.1, P99 288.8
测试结果表明数据库查询和UI渲染均存在效率问题,但无法确定核心原因。我尝试过的UI优化和Dispatchers调整均未奏效。
数据库查询细节
查询语句
获取聊天信息的查询语句如下:
@Transaction @Query(""" SELECT * FROM ChatEntity c JOIN ChatMessageEntity m ON c.id = m.chatId WHERE c.id = :id ORDER BY m.createdAt DESC LIMIT :limit OFFSET :offset """) fun getChat( id: Int, limit: Int, offset: Int ): Flow<Map<ChatWithParticipants, List<ChatMessageEntity>>>
关联数据类定义
其中ChatWithParticipants定义为:
data class ChatWithParticipants( @Embedded val chatEntity: ChatEntity, @Relation( parentColumn = "id", entityColumn = "id", associateBy = Junction( value = ParticipantInChatCrossRef::class, parentColumn = "chatId", entityColumn = "participantId" ) ) val participants: List<ChatParticipantEntity> )
所有外键均已建立索引。
ViewModel数据处理
数据通过Repository传递到ViewModel:
val chatState = repo.getChat(chatId, page = 0, pageSize = 15) .mapLatest { it.toChatState() } .stateIn( viewModelScope, SharingStarted.WhileSubscribed(1000), null )
数据映射方法
toChatState()方法实现如下:
fun Map<ChatWithParticipants, List<ChatMessageEntity>>.toChatState(): ChatState { val chat = keys.first() val messages = entries.firstOrNull()?.value ?: emptyList() return ChatState( id = chat.chatEntity.id, type = chat.chatEntity.type, createdAt = chat.chatEntity.createdAt, participants = chat.participants.map { it.toParticipant() }, messages = messages.map { message -> message.toMessage( sender = chat.participants.find { it.id == message.senderId }!! .toParticipant(), contents = emptyList() ) }, owner = chat.participants.first().toParticipant(), participantsLimit = chat.chatEntity.participantsLimit, participantsCount = chat.chatEntity.participantsCount, unreadMessagesCount = chat.chatEntity.unreadCount ) }
这段逻辑看起来不会对CPU造成太大负担,但基准测试结果却相反。
UI相关细节
聊天页面代码
精简后的聊天页面代码如下:
@Composable fun Chat( viewModel: ChatViewModel ) { val chatState by viewModel.chatState.collectAsState() val currentUserId by viewModel.currentUserId.collectAsState() chatState?.let { ChatContent( chatState = it, currentUserId = currentUserId ) } } @Composable private fun ChatContent( chatState: ChatState, currentUserId: Int? ) { val owner = chatState.owner ?: return val context = LocalContext.current val lastSeen = remember(chatState.owner) { DateTimeFormatter.ofPattern( if (DateFormat.is24HourFormat(context)) "kk:mm" else "hh:mm a" ).format(owner.lastOnline) } Scaffold( topBar = { // Avatar with Coil, name, last seen }, content = { val resources = LocalContext.current.resources val brush = remember { val pattern = ResourcesCompat.getDrawable( resources, R.drawable.chat_pattern, null )!!.toBitmap().asImageBitmap() ShaderBrush( ImageShader( image = pattern, tileModeX = TileMode.Repeated, tileModeY = TileMode.Repeated ) ) } Box( modifier = Modifier .padding(it) .background(brush) ) { LazyColumn(/*...*/) { itemsIndexed( items = chatState.messages, key = { _, it -> it.id } ) { index, it -> val iPreviousMessageFromOther = remember { chatState.messages.getOrNull(index - 1) ?.sender?.id != it.sender.id } val isNextMessageFromOther = remember { chatState.messages.getOrNull(index + 1) ?.sender?.id != it.sender.id } val isCurrentUserAuthor = remember { it.sender.id == currentUserId } MessageBubble( chatMessage = it, isCurrentUserAuthor = isCurrentUserAuthor, shouldShowTail = iPreviousMessageFromOther ) } } } }, bottomBar = { // ... } ) }
MessageBubble组件代码
MessageBubble是计算量较大的UI组件,原因包括:
- 自定义
MessageBubbleShape,为气泡添加“尾巴” - 自定义布局用于定位消息文本和状态(发送时间、已读、已编辑),需要通过
TextMeasurer测量文本
代码如下:
fun MessageBubble( modifier: Modifier = Modifier, chatMessage: ChatMessage, isCurrentUserAuthor: Boolean, shouldShowTail: Boolean ) { val largeShape = MaterialTheme.shapes.large val shape = remember(shouldShowTail) { if (shouldShowTail) LargeMessageBubbleShape else largeShape } val layoutDirection = LocalLayoutDirection.current val context = LocalContext.current val shouldRotate = remember(layoutDirection, isCurrentUserAuthor) { layoutDirection == LayoutDirection.Ltr && !isCurrentUserAuthor || layoutDirection == LayoutDirection.Rtl && isCurrentUserAuthor } val messageTime = remember { DateTimeFormatter.ofPattern( if (DateFormat.is24HourFormat(context)) "kk:mm" else "hh:mm a" ).format(chatMessage.createdAt) } Box( modifier = Modifier // bla bla .then(modifier) ) { Box( modifier = Modifier .graphicsLayer { if (shouldRotate) { rotationY = 180f } } .shadow( elevation = 1.dp, shape = shape ) .background( color = // bla, shape = shape ) //.bla(bla) ) { Box( modifier = Modifier .graphicsLayer { if (shouldRotate) { rotationY = 180f } } ) { val textMeasurer = rememberTextMeasurer() val messageTextStyle = LocalTextStyle.current val density = LocalDensity.current Layout( content = { Text( text = chatMessage.text, style = messageTextStyle ) Row( verticalAlignment = Alignment.CenterVertically ) { Text( text = messageTime, // ... ) if (isCurrentUserAuthor) { Icon( // ... ) } } }, measurePolicy = { measurables, constraints -> val messageMeasurement = textMeasurer.measure( text = chatMessage.text, constraints = constraints, style = messageTextStyle ) // A bunch of calculations which don't seem important because // in spite of them the list of bubbles scrolls smoothly. layout( width = layoutWidth, height = layoutHeight ) { text.placeRelative(0, 0) if (timeHasSpaceHorizontally) { time.placeRelative( x = layoutWidth - time.width, y = messageMeasurement.size.height - time.height ) } else { time.placeRelative( x = layoutWidth - time.width, y = layoutHeight - time.height ) } } } ) } } } }
UI效果展示

布局检查器未发现不必要的重组,数据库查询看似正常,自定义布局计算量不大,svg或png聊天背景对性能无显著影响。唯一有统计差异的是移除自定义凹形的阴影。
核心疑问
我是否遗漏了什么?阴影真的是罪魁祸首吗?如果是,原因是什么?如何优化其性能?
感谢您阅读这篇长文,我实在不知所措。
内容的提问来源于stack exchange,提问作者Calamity
相关产品推荐
相关产品推荐

