在教育直播场景中,播放器稳定性直接影响课堂体验。在我们直播课项目中,曾经出现一个非常棘手的问题:
直播间 ANR 在弱网环境下频繁出现。
经过深入排查,我们最终定位到 流媒体 SDK 的同步拉流接口设计问题 。随后通过 Coroutine + Flow 架构改造 完成播放器异步化升级,最终:直播间 ANR 下降约 60%。
本文将完整分享这次优化的技术思路与架构设计。
一、问题背景:直播间 ANR
典型 ANR 堆栈如下:
arduino
main thread blocked
rtcEngine.startPullStream
↓
nativeStartStream
↓
network connect
↓
wait response
调用链:
主线程
↓
rtcEngine.startPullStream
↓
JNI
↓
网络连接
↓
等待服务器响应
在弱网环境下:
阻塞时间可能达到数秒甚至十几秒。*
二、原始架构:同步阻塞模式
早期 SDK 提供的是纯同步拉流接口,业务层调用逻辑完全阻塞主线程:
底层 SDK 同步接口
class
// 同步阻塞:网络 + JNI 操作全部在调用线程执行
public RtcEngineInputStream startPullStream(
String streamId,
PullStreamOptions options
) {
// JNI 层执行网络建联
nativeStartStream(streamId);
// 阻塞式网络请求(弱网可能阻塞数秒)
connectServer();
return new RtcEngineInputStream();
}
//业务调用
public
// 主线程同步调用 → 可能导致 ANR
RtcEngineInputStream stream =
rtcEngine.startPullStream(streamId, options);{
// 创建渲染视图
SurfaceView surface = new SurfaceView(context);
// 绑定渲染
stream.attachSurface(surface);
return surface;
}
核心问题:
1️⃣ 拉流接口为 同步调用
2️⃣ JNI 内部进行 网络连接
3️⃣ 主线程被阻塞
4️⃣ 弱网环境必现 ANR
三、第一阶段优化:Callback 异步化
为了解决阻塞问题,我们推动 SDK 提供了 异步接口:
arduino
void startPullStreamAsync( String streamId, Options options, IStartStreamCallback callback );
拉流不再是同步等待,而是等流媒体的异步回调。同时SurfaceView的创建也放到IStartStreamCallback这个callBack里执行,创建完毕后再通过业务层的callBack回传回去。
ANR 问题得到缓解,但新的问题出现:
-
Callback 嵌套严重
-
线程切换混乱
-
无法取消任务
-
生命周期难管理
-
状态监听分散
代码示例:
typescript
private void startPull(streamId, options,playerListener){
rtcEngine.startPullStreamAsync(streamId, options, new PullStreamCallback() {
@Override
public void onSuccess(RtcInputStream stream) {
SurfaceView surface = new SurfaceView(context);
stream.attachSurface(surface);
playerListener.onPlayStarted(surface);
}
@Override
public void onError(int code, String msg) {
playerListener.onError(code, msg);
}
});
}
随着业务复杂度增加,主线程切换等,代码可维护性迅速下降。项目陷入嵌套地狱。
四、最终架构:Coroutine + Flow
为了解决 Callback 架构的问题,我们进行了 播放器架构升级。
核心目标:
- 消除 Callback 地狱
- 明确线程模型
- 支持生命周期取消
- 统一状态管理
五、播放器整体架构

六、线程调度模型
播放器的线程调度非常关键:

线程原则:
| 操作 | 线程 |
|---|---|
| 网络 / JNI | IO |
| SurfaceView 创建 | Main |
| attachSurface | IO |
七、Callback → Suspend 封装
核心改造是将 SDK Callback 转换为 挂起函数。
kotlin
suspend fun startPullStream(streamId: String): SurfaceView? =
suspendCancellableCoroutine { cont ->
rtcEngine.startPullStreamAsync(
streamId,
options,
object : PullStreamCallback {
override fun onSuccess(stream: RtcInputStream) {
CoroutineScope(Dispatchers.Main).launch {
val surface =
surfaceManager.obtain(streamId)
withContext(Dispatchers.IO) {
stream.attachSurface(surface)
}
cont.resume(surface)
}
}
override fun onError(code: Int, msg: String) {
cont.resume(null)
}
}
)
cont.invokeOnCancellation {
rtcEngine.stopPullStream(streamId)
}
}
优势:
- 支持 协程取消
- 消除 Callback 嵌套
- 线程调度清晰
八、SurfaceView 复用池
为了避免频繁创建 SurfaceView,我们设计了 复用池。

实现:
kotlin
object SurfaceManager {
private val surfaceMap =
ConcurrentHashMap<String, SurfaceView>()
fun obtain(streamId: String): SurfaceView {
return surfaceMap.getOrPut(streamId) {
SurfaceView(context)
}
}
}
收益:
- 减少 GPU 创建开销
- 降低 UI 卡顿
九、播放器状态 Flow 化
播放器状态统一使用 Flow 管理。

正常流媒体回调写法
csharp
interface StreamStateListener {
void onConnecting();
void onPlaying();
void onBuffering();
void onError(int code, String msg);
void onStopped();
}
在 Flow 层,我们统一映射为:
kotlin
sealed class StreamState {
object Connecting : StreamState()
object Playing : StreamState()
object Buffering : StreamState()
data class Error(
val code: Int,
val msg: String
) : StreamState()
object Stopped : StreamState()
}
实现:
kotlin
fun observeStream(stream: RtcInputStream): Flow<StreamState> =
callbackFlow {
val listener = object : StreamStateListener {
override fun onConnecting() {
trySend(StreamState.Connecting)
}
override fun onPlaying() {
trySend(StreamState.Playing)
}
override fun onBuffering() {
trySend(StreamState.Buffering)
}
override fun onError(code: Int, msg: String) {
trySend(StreamState.Error(code, msg))
}
override fun onStopped() {
trySend(StreamState.Stopped)
}
}
stream.addListener(listener)
awaitClose {
stream.removeListener(listener)
}
}

十、StateFlow 共享状态
kotlin
class StreamRepository {
fun streamState(stream: RtcInputStream): StateFlow<StreamState> {
return observeStream(stream)
.stateIn(
scope = CoroutineScope(Dispatchers.Main),
started = SharingStarted.Eagerly,
initialValue = StreamState.Connecting
)
}
}
这段代码是 「将冷流(CallbackFlow)转换为热流(StateFlow)」 为什么要转 StateFlow
| 特性 | 原冷流(CallbackFlow) | 转换后(StateFlow) |
|---|---|---|
| 状态获取 | 只能通过 collect 监听,无法主动获取 |
可通过 value 随时拿最新状态 |
| 多订阅者 | 每个订阅者都会重新注册 Listener | 所有订阅者共享一个 Listener |
| 初始值 | 无,需手动处理 | 有 initialValue,UI 更稳定 |
| 线程分发 | 依赖 SDK 回调线程 | 强制主线程(Dispatchers.Main) |
十一、UI 层收集状态
在 UI 层,如果是 Compose 页面,可以使用 collectAsStateWithLifecycle() 直接将 StateFlow 转换为 Compose State,实现状态驱动 UI。
scss
val state by viewModel.streamState
.collectAsStateWithLifecycle()
when (state) {
StreamState.Connecting ->
LoadingView()
StreamState.Playing ->
VideoView()
StreamState.Buffering ->
BufferingView()
is StreamState.Error ->
ErrorView()
}
在 UI 层我们使用 collectAsStateWithLifecycle 来收集播放器 StateFlow。
这个 API 会根据 Lifecycle 状态自动开始或暂停 collect,当页面进入 STARTED 状态时开始收集,当页面 STOPPED 时暂停收集。
同时当 UI 销毁时协程会自动取消,Flow 会触发 awaitClose() 移除播放器监听器,从而避免内存泄漏。 这样播放器状态流就实现了完整的生命周期安全管理。
如果是传统 View 页面,则可以通过 repeatOnLifecycle 在 Lifecycle.State.STARTED 状态下安全收集 Flow,保证生命周期安全。
scss
lifecycleScope.launch {
repeatOnLifecycle(Lifecycle.State.STARTED) {
viewModel.streamState.collect { state ->
render(state)
}
}
}
在 Fragment 中收集 Flow 时,需要使用 viewLifecycleOwner.lifecycleScope,而不是 lifecycleScope。
因为 Fragment 的 View 生命周期可能早于 Fragment 生命周期结束,如果使用 lifecycleScope,可能在 View 已销毁时仍然更新 UI,从而导致异常。
使用 viewLifecycleOwner 可以保证 Flow 的收集与 View 生命周期一致,在 onDestroyView 时自动取消
kotlin
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
viewLifecycleOwner.lifecycleScope.launch {
viewLifecycleOwner.repeatOnLifecycle(Lifecycle.State.STARTED) {
viewModel.streamState.collect { state ->
render(state)
}
}
}
}
关键点:
| API | 绑定生命周期 |
|---|---|
| lifecycleScope | Fragment |
| viewLifecycleOwner.lifecycleScope | View |
| 这样 UI 就不会访问已经销毁的 View |
compose的collectAsStateWithLifecycle内部使用的是Composition Lifecycle,在fragment中不会有类似问题
十二、业务调用流程

播放器:
kotlin
class CoroutinePlayer(
private val dataSource: StreamDataSource,
private val repository: StreamRepository
) {
suspend fun startPlay(
streamId: String
): Pair<SurfaceView, StateFlow<StreamState>> {
val surface =
dataSource.startPullStream(streamId)
val stream =
dataSource.getStream(streamId)
val stateFlow =
repository.streamState(stream)
return Pair(surface!!, stateFlow)
}
}
十三、优化效果
| 维度 | 优化前 | 优化后 |
|---|---|---|
| 拉流方式 | 同步阻塞 | Coroutine 异步 |
| 线程控制 | 混乱 | 主线程 / IO 明确 |
| 状态监听 | 多 Listener | Flow |
| Surface | 频繁创建 | 复用池 |
| ANR | 高 | 下降 60% |
| 内存泄漏 | 偶发 | 几乎 0 |
十四、总结
整个项目,我们通过
- 定位 ANR 根因:同步拉流阻塞主线程
- 推动 SDK 提供异步接口 startPullStreamAsync
- 使用 suspendCancellableCoroutine 封装为挂起函数
- 使用 withContext 区分 IO / Main 线程*
- 使用 callbackFlow + StateFlow 实现状态流
- 设计 SurfaceView 复用池降低 GPU 开销
- 最终直播间 ANR 下降约 60%
十五、架构升级总结
本次播放器优化的核心是:
从同步 SDK 调用 → 协程化架构设计
技术关键点:
-
Coroutine
-
suspendCancellableCoroutine
-
Flow / StateFlow
-
Lifecycle
-
SurfaceView 复用
-
线程调度设计
最终实现:
稳定性 + 架构可维护性 双提升。