ExoPlayer 的线程模型与主线程交互
做 ExoPlayer ANR 排查,绕不开一个核心问题:线程模型。这玩意儿要是不搞明白,你连 ANR 是怎么触发的基本都理不清。
我记得刚接手项目时,线上报了个颇诡异的 ANR,日志指向 Thread-6 里有个 Looper.loop() 卡住了。排查半天才发现——那根本不是主线程,而是 ExoPlayer 内部的一个渲染线程因为外部回调阻塞了主线程的消息队列。嗯,今天我们就来把这层关系彻底捋清。
ExoPlayer 的线程家族
ExoPlayer 自己维护了一套内部线程池,不直接跑在主线程上。这么做的好处是不阻塞 UI,但坏处是——如果你不懂它内部怎么跟主线程通信,分分钟踩坑。
MediaSource,加载线程可能会创建多个。但无论如何,它们都通过 Handler 跟主线程沟通。
主线程到底在等什么?
先看看官方文档怎么说——ExoPlayer 的 Player.Listener 和 AnalyticsListener 默认都在你创建 player 的那个线程里回调。绝大多数情况下就是主线程。也就是说,你在回调里写的任何代码,都在主线程的 Looper.loop() 里跑。
那 ANR 是怎么来的?说白了就是:主线程的消息队列被堵住了。ExoPlayer 内部线程把回调事件 post 到主线程,但主线程正在干别的事(比如你的 onPlayerStateChanged 里做了网络请求),队列里的消息一直没被处理,过 5 秒——ANR。
| 线程角色 | 职责 | 与主线程交互方式 | ANR 风险点 |
|---|---|---|---|
| 播放器线程 | 媒体时序、状态转换 | 通过 Handler 通知状态改变 | 回调中做耗时 I/O |
| 渲染线程 | 解码、帧输出 | 渲染完成回调 post | onRenderedFirstFrame 执行阻塞 |
| 数据加载线程 | 下载/缓存 | 加载状态回调 | onLoadCompleted 里做同步锁 |
| 音频处理线程 | AudioTrack 写入 | 音频延迟/错误回调 | onAudioSessionId 中初始化第三方库 |
这些回调,就是 ANR 的引爆点
我见过最典型的两个坑:
- 在
Player.Listener回调里直接调Thread.sleep()或网络请求——这都不用多说,主线程只要 sleep 超过 5 秒必 ANR。 - 在回调里用
Future.get(timeout)等待内部线程结果——如果内部线程也在等主线程(死锁),那直接炸。 - 把
Looper.myLooper()当主线程 Looper 用,但 player 创建在后台线程——这时候回调跑在后台线程,但你自己在回调里又 post 了消息到“主线程”,结果混乱。
关键原则:ExoPlayer 的所有回调都默认在创建 player 的线程执行。如果你在主线程创建 player,那所有回调都在主线程。任何阻塞主线程的操作,都是 ANR 的种子。
避坑指南:我曾经踩过的雷
我曾经接手过一个视频直播项目,线上 ANR 率一直 0.3% 降不下来。抓了个堆栈,怎么看都是 Handler.dispatchMessage 卡住,但主线程代码里明明没耗时操作。后来我用 systrace 一拉,发现 ExoPlayerImplInternal.handleMessage 里调了 onPlayerError,而我恰好在 onPlayerError 回调里做了数据库写操作——用的是主线程的 Room 同步写,直接卡住消息队列。
从那以后我养成了一个习惯:所有 ExoPlayer 回调里,只做状态记录和轻量 UI 刷新,如果需要做网络或数据库,一律切到后台线程。代码就像下面这样:
player.addListener(
object : Player.Listener {
override fun onPlayerStateChanged(playWhenReady: Boolean, playbackState: Int) {
// 只更新 UI 状态(轻量)
binding.playPauseButton.isVisible = playbackState == Player.STATE_READY
// 需要写数据库,切到后台
lifecycleScope.launch(Dispatchers.IO) {
repo.savePlaybackState(playbackState)
}
}
}
)
警告:即使你切到了 Dispatchers.IO,也要注意 lifecycleScope 的生命周期。如果回调发生在 Activity 已销毁但协程还在跑,可能引发内存泄漏。我个人建议用 viewModelScope 配合 lifecycle.repeatOnLifecycle。
怎么判断线程模型是否影响了 ANR?
拿到 ANR 日志后,先看主线程堆栈。如果 android.os.Handler.dispatchMessage 上面有 com.google.android.exoplayer2.Player.Listener 相关的调用,基本就是这个问题。
我自己习惯再跑一份 Thread.getAllStackTraces() 的 dump,确认内部线程是否卡在等待主线程的某个资源上。比如加载线程调了 Future.get() 等主线程的缓存结果,那双向等待就出现了。
说白了,ExoPlayer 的线程模型本身没毛病,用好了相当高效。但一旦你忘记“回调在主线程”这个事实,ANR 就离你不远了。记住:回调里只做轻量操作,耗时任务全切到后台线程——这句话能帮你解决 80% 的 ExoPlayer ANR 问题。