cover-img
avatar

托码特人

分享科技与人文

一个关注互联网的技术博客

一个直播业务死活不出画面的问题排查

前段时间,基于Expo Module开发了一个跨平台的直播组件,基于腾讯云 IoT

在安卓端,因为有一个现成的 demo 供参考,所以图省事就大量复制了原来的Java代码。本来弄完后可以播放(应付差事),但是腾讯的XP2P很耗时导致进入PlayerView时要卡差不多 2 秒!

这让我如何能忍?! 😂

于是就用Kotlin的协程改造了一番,结果一顿操作猛如虎,改完一看,画面没了,然而 logcat 没有明显报错。这就让人抓狂了…于是有了这篇文档。

果与因

一、画布变量申明修饰符错误

在优化过程中,试图使用类似lateinitlazy的关键词去声明可能有大消耗的变量,结果某个环节翻车了。以下是通过GPT学到的姿势:

使用 lateinit 的注意事项

  1. 初始化保证:lateinit 变量必须在访问前被初始化,否则会抛出 UninitializedPropertyAccessException。确保在使用变量前它已经被初始化。
  2. 线程安全:初始化 lateinit 变量的操作应该是线程安全的。如果你在一个多线程环境中使用 lateinit,确保只有一个线程负责初始化。
  3. 可变性:lateinit 只能修饰 var 类型的属性,因为属性初始化后可能需要更改引用。如果你想要一个只读的延迟初始化属性,应该使用 lazy
  4. 非空类型:lateinit 只能用于非空引用类型,不能用于基本类型(如 IntDouble 等)。
  5. 反射:使用反射初始化 lateinit 属性时,需要特别小心,因为反射操作可能绕过 Kotlin 的空安全检查。
  6. 调试困难:由于 lateinit 绕过了 Kotlin 的空安全检查,它可能使得调试更难,因为你不能依赖于编译器来捕获未初始化的引用错误。

不应该使用 lateinit 的情况

  1. 可提前初始化的属性:如果一个属性可以在构造函数或初始化块中被安全地初始化,那么不应该使用 lateinit
  2. 可选或可能为 null 的属性:lateinit 不应该用于可选的或可能为 null 的属性,因为 lateinit 要求属性非空。
  3. 只读属性:对于只读属性,应该使用 lazy 而不是 lateinit,因为 lazy 提供了一次性、线程安全的延迟初始化。
  4. 性能关键路径:在性能关键的代码中,如果延迟初始化不是必要的,应该避免使用 lateinit,因为它可能引入额外的运行时检查。
  5. 多线程环境:在多线程环境下,如果多个线程可能同时尝试初始化同一个 lateinit 属性,可能会导致竞态条件或异常。

总之,lateinit 应该谨慎使用,主要用于那些在对象构造后才能安全初始化的属性,并且这些属性在初始化前不会被访问。在设计类时,优先考虑在构造函数或初始化块中初始化属性,只有在确实需要延迟初始化时才使用 lateinit

二、多线程异步不播放

截止 7.5 日的实际情况来看,并非播放不能在多线程异步条件下进行。前期播不了的根本原因在于多线程下 SDK 初始化和画布初始化存在时间差,即:SDK 初始化好了,但画布还没准备好。所以从 logcat 上看,有大把 ijkplayer 日志输出,但始终没有画面

后来,通过事件的方式,在完成画布初始化后通知 SDK 播放,此时整个流程如丝般顺滑…

1、把画布初始化时就设置上监听回调,原来是在播放那一刻才做这个动作。所以surfaceTextureListener时机很关键

private val playerView = LiveTextureView(context).also {
    it.layoutParams = LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.MATCH_PARENT)
    it.isOpaque = false
    it.surfaceTextureListener = it
    addView(it)
    Log.i(TAG, "playerView added to MainView. called ============ 🏅")
}

2、画布初始化完成后,通过消息通知,让 SDK 开始异步初始化

internal lateinit var surface: Surface

override fun onSurfaceTextureAvailable(surface: SurfaceTexture, width: Int, height: Int) {
    try {
        this.surface = Surface(surface)
        Log.d(TAG, "surface初始化完成,即将进行播放... ")
        EventBus.getDefault().post(AppEventKey.OnSurfaceTextureAvailable)
    } catch (e: Exception) {
        Log.e(TAG, e.message!!)
    }
}

3、收到通知,异步开干!这里用到了androidx.lifecycle.ViewModel

@Subscribe(threadMode = ThreadMode.MAIN)
internal fun calledOnAppEvent(event: AppEventKey) {
    when (event) {
        AppEventKey.OnSurfaceTextureAvailable -> {
            initPlayerService()
        }
    }
}

private fun initPlayerService()  {
    viewModelScope.launch(Dispatchers.IO) {
        xp2PService.start(ctx)
    }
}

三、data class 定义

对于Kotlin这门语言,之前其实用到的并不多,可以说是小白用户。在优化过程中没有注意到一些的细节,然后导致RN那边的变量传过来是错的,而且竟然一直没有发现这个问题。直到一行行的分析 logcat 时,才偶然发现一个简短的 Warn:p2p error message

kotlin datClass

一点想法

  • 日志的重要性:擅于分析日志的重要性不用多说,然而对于开源类库作者,输出有效的、合适等级的日志信息更重要。这直接影响业务对接的效率和体验;
  • 文档的重要性:任何情况下,提供完尽、有条理的业务文档都很重要,然而对于大多数开发者而言,写文档没有直接写代码更有快感。这就导致了对接过程中要深入源码去看,如果能进入倒好。像 iOS 那种.a只提供头文件的,就当场吐血了;
  • 对于编程语言的选择:这次算是对Kotlin有了更全面的体验,从个人码历而言,其快乐体验吊打一众别的大前端语言,如:ObjcJavaScript,甚至我个人觉得比Swift感受还要好。我觉得吧如果有得选,试试现代编程语言还是不错的选择。

写在最后

由于该播放器组件受保密协议约束,所以不能直接发github地址。当然作为expo module本身,也并不见得有多大的通用性。所以呢本文仅针对本人研发过程中遇到的问题做一个整理,如果你也恰好遇到类似的问题或需求,可以直接留言交流。

赞赏

声明: 本文内容由托码斯创作整理,由于知识水平和时效性问题,行文可能存在差错,欢迎留言交流。读者若需转载,请保留出处,谢谢!