一次播放器架构升级:Android 直播间 ANR 下降 60%

在教育直播场景中,播放器稳定性直接影响课堂体验。在我们直播课项目中,曾经出现一个非常棘手的问题:

直播间 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

十四、总结

整个项目,我们通过

  1. 定位 ANR 根因:同步拉流阻塞主线程
  2. 推动 SDK 提供异步接口 startPullStreamAsync
  3. 使用 suspendCancellableCoroutine 封装为挂起函数
  4. 使用 withContext 区分 IO / Main 线程*
  5. 使用 callbackFlow + StateFlow 实现状态流
  6. 设计 SurfaceView 复用池降低 GPU 开销
  7. 最终直播间 ANR 下降约 60%

十五、架构升级总结

本次播放器优化的核心是:

从同步 SDK 调用 → 协程化架构设计

技术关键点:

  • Coroutine

  • suspendCancellableCoroutine

  • Flow / StateFlow

  • Lifecycle

  • SurfaceView 复用

  • 线程调度设计

最终实现:

稳定性 + 架构可维护性 双提升。

相关推荐
测试工坊4 小时前
Android 视频播放卡顿检测——帧率之外的第二战场
android
Kapaseker6 小时前
一杯美式深入理解 data class
android·kotlin
鹏多多6 小时前
Flutter使用screenshot进行截屏和截长图以及分享保存的全流程指南
android·前端·flutter
Carson带你学Android6 小时前
OpenClaw移动端要来了?Android官宣AI原生支持App Functions
android
黄林晴6 小时前
Android 删了 XML 预览,现在你必须学 Compose 了
android
三少爷的鞋6 小时前
Android 面试系列 | 内存泄露:从"手动配对"到"架构自愈"
android
恋猫de小郭6 小时前
什么 AI 写 Android 最好用?官方做了一个基准测试排名
android·前端·flutter
louisgeek16 小时前
Android MediatorLiveData
android
锋风1 天前
远程服务器运行Android Studio开发aosp源码
android