前段时间,基于Expo Module开发了一个跨平台的直播组件,基于腾讯云 IoT。
在安卓端,因为有一个现成的 demo 供参考,所以图省事就大量复制了原来的Java
代码。本来弄完后可以播放(应付差事),但是腾讯的XP2P
很耗时导致进入PlayerView
时要卡差不多 2 秒!
这让我如何能忍?! 😂
于是就用Kotlin
的协程改造了一番,结果一顿操作猛如虎,改完一看,画面没了,然而 logcat 没有明显报错。这就让人抓狂了…于是有了这篇文档。
果与因
一、画布变量申明修饰符错误
在优化过程中,试图使用类似lateinit
、lazy
的关键词去声明可能有大消耗的变量,结果某个环节翻车了。以下是通过GPT
学到的姿势:
使用 lateinit 的注意事项
- 初始化保证:
lateinit
变量必须在访问前被初始化,否则会抛出UninitializedPropertyAccessException
。确保在使用变量前它已经被初始化。 - 线程安全:初始化
lateinit
变量的操作应该是线程安全的。如果你在一个多线程环境中使用lateinit
,确保只有一个线程负责初始化。 - 可变性:
lateinit
只能修饰var
类型的属性,因为属性初始化后可能需要更改引用。如果你想要一个只读的延迟初始化属性,应该使用lazy
。 - 非空类型:
lateinit
只能用于非空引用类型,不能用于基本类型(如Int
、Double
等)。 - 反射:使用反射初始化
lateinit
属性时,需要特别小心,因为反射操作可能绕过Kotlin
的空安全检查。 - 调试困难:由于
lateinit
绕过了Kotlin
的空安全检查,它可能使得调试更难,因为你不能依赖于编译器来捕获未初始化的引用错误。
不应该使用 lateinit 的情况
- 可提前初始化的属性:如果一个属性可以在构造函数或初始化块中被安全地初始化,那么不应该使用
lateinit
。 - 可选或可能为
null
的属性:lateinit
不应该用于可选的或可能为null
的属性,因为lateinit
要求属性非空。 - 只读属性:对于只读属性,应该使用
lazy
而不是lateinit
,因为lazy
提供了一次性、线程安全的延迟初始化。 - 性能关键路径:在性能关键的代码中,如果延迟初始化不是必要的,应该避免使用
lateinit
,因为它可能引入额外的运行时检查。 - 多线程环境:在多线程环境下,如果多个线程可能同时尝试初始化同一个
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
一点想法
- 日志的重要性:擅于分析日志的重要性不用多说,然而对于开源类库作者,输出有效的、合适等级的日志信息更重要。这直接影响业务对接的效率和体验;
- 文档的重要性:任何情况下,提供完尽、有条理的业务文档都很重要,然而对于大多数开发者而言,写文档没有直接写代码更有快感。这就导致了对接过程中要深入源码去看,如果能进入倒好。像 iOS 那种
.a
只提供头文件的,就当场吐血了; - 对于编程语言的选择:这次算是对
Kotlin
有了更全面的体验,从个人码历而言,其快乐体验吊打一众别的大前端语言,如:Objc
、JavaScript
,甚至我个人觉得比Swift
感受还要好。我觉得吧如果有得选,试试现代编程语言还是不错的选择。
写在最后
由于该播放器组件受保密协议约束,所以不能直接发github
地址。当然作为expo module
本身,也并不见得有多大的通用性。所以呢本文仅针对本人研发过程中遇到的问题做一个整理,如果你也恰好遇到类似的问题或需求,可以直接留言交流。