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

使用Navigation时出现主线程帧丢失问题求助

问题:拆分页面后主线程丢帧(Skipped 204 frames)

错误日志

I/Choreographer: Skipped 204 frames!  The application may be doing too much work on its main thread.

场景描述

基于Firebase Storage存储、Media3播放的音乐流应用,单页面实现歌曲列表+播放控制时运行正常;使用Navigation拆分为两个页面(点击歌曲跳转至播放控制页)后,出现上述主线程丢帧错误。


相关代码

网络请求代码

TrackRepository.kt

class TrackRepository {

    private val storage = Firebase.storage
    private val trackRef = storage.reference

    suspend fun getTracks() = suspendCoroutine<List<Track>> { result ->
        val trackList = mutableListOf<Track>()
            try {
                Firebase.firestore.collection(Constants.TRACK).get().addOnCompleteListener { task ->
                    var index = 0
                    task.result.forEach { document ->
                        val trackUrl = trackRef.child(document.getString(Constants.FILENAME)!!)

                        trackUrl.downloadUrl.addOnSuccessListener { trackDownloadUrl ->
                            trackList.add(
                                document.toTrack(
                                    trackDownloadUrl.toString()
                                )
                            )
                            if (index == task.result.size() - 1) {
                                result.resume(trackList)
                            }
                            index++
                        }

                    }
                }

            } catch (e: Exception) {
                e.printStackTrace()
            }

    }

}

TrackViewModel.kt

@HiltViewModel
class TrackViewModel @Inject constructor(trackRepository: TrackRepository): ViewModel() {
    private val _trackList = MutableLiveData<List<Track>>()
    val trackList : LiveData<List<Track>> get() = _trackList

    init {
        viewModelScope.launch() {
            trackRepository.getTracks().let {
                _trackList.postValue(it.sortedBy { it.fileName })
            }
        }
    }
}

导航相关代码

NavHost中PlayerScreen配置

composable("player_screen"){
    val result = navController.previousBackStackEntry?.savedStateHandle?.get<MyTrack>("track")
   PlayerScreen(
     navController = navController,
       onMusicPlayerClick = onMusicPlayerClick,
       track = result?.track!!,
       onTrackItemClick = onTrackItemClick,
       trackList = trackList,
       isPlaying = isPlaying,

   )

}

歌曲列表页跳转代码

if (selected){

        val newTrack = MyTrack(track, navController, onMusicPlayerClick)
        navController.currentBackStackEntry?.savedStateHandle?.set(
            key = "track",
            value = newTrack
        )
    onTrackItemClick(track)
    navController.navigate("player_screen")


}

已排查情况

  • 排除网络请求原因:单页面模式下无丢帧问题;
  • 移除离线下载功能后问题依旧;
  • 用LaunchedEffect(Unit)包裹导航代码可解决,但引发其他逻辑问题;
  • Android Profiler未定位到明确的耗时操作。

定位分析与解决方案

核心问题定位

  1. TrackRepository异步逻辑漏洞:当前getTracks中,downloadUrl是异步请求,回调执行顺序不固定,index++写在回调外部,导致result.resume(trackList)可能在所有下载URL请求完成前就被调用,后续回调仍在主线程修改列表,引发主线程额外负载;同时trackList是 mutable 集合,多线程回调修改存在线程安全问题。
  2. 导航数据传递不合理:跳转时传递的MyTrack包含navController和回调对象,这类对象不适合通过savedStateHandle传递,序列化/反序列化过程可能在主线程产生耗时,且容易引发内存泄漏。
  3. 列表排序主线程开销:sortedBy在ViewModel的viewModelScope(默认主线程)执行,若歌曲列表数量大,排序操作会占用主线程时间片。

具体修复方案

1. 修复TrackRepository的异步逻辑

改用coroutineScope并行处理所有下载URL请求,确保所有请求完成后再返回列表,同时避免线程安全问题:

class TrackRepository {

    private val storage = Firebase.storage
    private val trackRef = storage.reference
    private val firestore = Firebase.firestore

    suspend fun getTracks(): List<Track> = coroutineScope {
        try {
            val trackDocs = firestore.collection(Constants.TRACK).get().await()
            trackDocs.map { document ->
                async {
                    val fileName = document.getString(Constants.FILENAME)!!
                    val trackDownloadUrl = trackRef.child(fileName).downloadUrl.await()
                    document.toTrack(trackDownloadUrl.toString())
                }
            }.awaitAll().sortedBy { it.fileName }
        } catch (e: Exception) {
            e.printStackTrace()
            emptyList()
        }
    }
}

说明:使用Firebase Kotlin扩展库的await()方法替代回调,async/awaitAll并行处理所有下载URL请求,效率更高且逻辑安全。

2. 优化导航数据传递

不要传递包含navController和回调的MyTrack,仅传递Track实体:

  • 列表页跳转代码修改:
if (selected) {
    navController.currentBackStackEntry?.savedStateHandle?.set(
        key = "track",
        value = track // 仅传递Track实体
    )
    onTrackItemClick(track)
    navController.navigate("player_screen")
}
  • PlayerScreen获取数据:
composable("player_screen"){
    val track = navController.previousBackStackEntry?.savedStateHandle?.get<Track>("track")!!
    // 通过LocalNavHostController获取导航控制器,无需传递
    val localNavController = LocalNavHostController.current
    PlayerScreen(
        navController = localNavController,
        onMusicPlayerClick = onMusicPlayerClick,
        track = track,
        onTrackItemClick = onTrackItemClick,
        trackList = trackList,
        isPlaying = isPlaying,
    )
}

3. 移除非UI线程的排序操作

在Repository中完成排序,避免主线程开销:

// 在TrackRepository的getTracks方法最后添加排序
return trackDocs.map { ... }.awaitAll().sortedBy { it.fileName }

ViewModel中直接赋值即可:

viewModelScope.launch {
    _trackList.postValue(trackRepository.getTracks())
}

4. 排查PlayerScreen的初始化耗时

检查PlayerScreen中是否在组合阶段直接执行Media3播放器初始化、大量UI计算等操作,这类操作应放到LaunchedEffect或ViewModel的viewModelScope中执行:

@Composable
fun PlayerScreen(...) {
    LaunchedEffect(track) {
        // 初始化播放器、加载媒体资源等耗时操作
        player.prepare()
    }
}

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.08.12 07:45:49