使用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未定位到明确的耗时操作。
定位分析与解决方案
核心问题定位
- TrackRepository异步逻辑漏洞:当前
getTracks中,downloadUrl是异步请求,回调执行顺序不固定,index++写在回调外部,导致result.resume(trackList)可能在所有下载URL请求完成前就被调用,后续回调仍在主线程修改列表,引发主线程额外负载;同时trackList是 mutable 集合,多线程回调修改存在线程安全问题。 - 导航数据传递不合理:跳转时传递的
MyTrack包含navController和回调对象,这类对象不适合通过savedStateHandle传递,序列化/反序列化过程可能在主线程产生耗时,且容易引发内存泄漏。 - 列表排序主线程开销:
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
相关产品推荐
相关产品推荐

