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

基于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效果展示

聊天消息UI

布局检查器未发现不必要的重组,数据库查询看似正常,自定义布局计算量不大,svg或png聊天背景对性能无显著影响。唯一有统计差异的是移除自定义凹形的阴影。


核心疑问

我是否遗漏了什么?阴影真的是罪魁祸首吗?如果是,原因是什么?如何优化其性能?

感谢您阅读这篇长文,我实在不知所措。

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.07.13 10:54:50