Android 性能优化实战 - 直播间场景 「涉及到 Kotlin Coroutine, Websocket , SharedFlow, StateFlow 」

说一下场景

线上总有反馈说从直播间掉线,然后测试开始压测,发现对于低端设备在我们业务中推流场景下只能到60,即会发生异常;

先说一下优化前的问题

优化前业务流程图.png
  • 业务逻辑层,在一个协程里顺序处理每一个 ws 接收者,eg:IM,Inner Notification,直播间..., 处理完后,才会开始开始下一条 ws 消息的处理;
  • 对于 IM 的ws 消息,也会走进 直播间 ws 消息的处理,只是直播间 ws 解析 command 后,发现自己无需处理,才会return;
  • 直播间有个特殊命令「简称 special cmd」确保用户始终和直播间保持链接,若 n 秒内未接收到 special cmd,则会执行退房处理. 问题是在第1条的描述中,ws 会有堆积,若之前的消息处理逻辑过多,则会导致这个 special cmd 始终无法处理;
  • 在直播间中,用户会刷评论/送礼等 level 较低的消息,若设备低端,无法处理大量低 level 消息时,可做丢弃或用其他 coroutine 处理,不要影响直播间中处理主流程 ws 的coroutine;
  • ws 中 jsonstring 重复解析问题,Gson 解析耗时问题众所周知,重复解析且解析方式问题,导致 CPU 占用增大 and 发热 and 耗时增多,进而降低设备 CPU 处理能力,恶性循环;

优化思路

  • 不同场景只处理属于自己的 ws;
  • 解决 ws 积压导致后面消息无法及时处理的问题;
  • 对于直播间等重 ws 的业务场景,low_level 且对 UI 线程影响较大的 msg 单独 coroutine 处理,eg:评论,送礼「有动画」;
  • Gson 解析中高频 key 解析优化 and 优化 Gson 解析使用方式;

优化实战

优化后业务流程图.png

二次分发

  • 目前所有的 ws 都在 totalFlow 中,需要做二次分发. 根据目前公司提供的后端数据,只能根据 ws 中携带的 command 做 filter 过滤;
  • 对于少量且低频的 msg,eg:IM, InnerNotification 等封装为一个 otherFlow;
  • 对于大量且重要的 msg,eg:直播间流程中的各 msg 封装为一个 liveFlow;
  • 对于大量且可根据设备处理能力丢弃 or 延迟处理的msg,eg:评论,送礼 等封装为一个 highFreqFlow;

otherFlow, liveFlow, highFreqFlow 均为 MutableSharedFlow;
其中 extraBufferCapacity 分别为 5, 800, 300「5:基本不会有积压; 800:重要流程消息,为防止被丢弃设为 较高阀值,服务端目前最高并发貌似也不会超过 800; 300:评论,送礼都是可丢弃的,且评论区最高存储消息 count 为 150」;
onBufferOverflow 均为 BufferOverflow.DROP_OLDEST;
处理之后也解决了 special cmd 无法及时处理,导致退房的bug.

Gson 解析

对于刚需字段 command,初次解析后,在 ws 的 data 里增加一个 command 即可, so easy.

data class Broadcast(val message: JSONObject, val command: Int)

对于 Gson 解析,改之前写法如下

val a = jsonString.optJSONObject("A")?.toString() ?: ""
val b = jsonString.optJSONObject("B")?.toString() ?: ""
val aa = try {
    GsonManager.gson().fromJson<User>(a, object : TypeToken<User>() {}.type)
} catch (e: Exception) {
    User()
}
val bb = GsonManager.gson().fromJson<User>(b, object : TypeToken<User>() {}.type)
val c = jsonString.optLong("c")
val d = jsonString.optInt("d")
var e = jsonString.optString("e", "")
val f = jsonString.optBoolean("f", false)
val g = jsonString.optJSONObject("g")?.toString() ?: ""
val extra = try {
    GsonManager.gson().fromJson<CCC>(g, object : TypeToken<CCC>() {}.type)
} catch (e: Throwable) {
    null
}

改完后

val data = try {
    GsonManager.gson().fromJson<AB>(jsonString, object : TypeToken<AB>() {}.type)
} catch (e: Throwable) {
    return
}

data class AB(
    @SerializedName("a")
    var a: User,
    @SerializedName("b")
    var b: User,
    @SerializedName("c")
    var c: Long,
    @SerializedName("d")
    var d: Int,
    @SerializedName("e")
    var e: String,
    @SerializedName("f")
    var f: Boolean? = null,
    @SerializedName("g")
    var g: CCC? = null,
)

至于 Gson 解析耗时原理的话,自行查阅吧,我也不太熟...

其他优化

在对上面的 totalFlow 做初步解析的时候,用到了很多 filter 方法,每一次 filter 都是新建的一个 flow,尽量一次 filter 完成功能,如下

public inline fun <T> Flow<T>.filter(crossinline predicate: suspend (T) -> Boolean): Flow<T> = transform { value ->
    if (predicate(value)) return@transform emit(value)
}

//优化前如下:
flow.filter {
//**
}.map {
//***
}.filter {
//**
}.catch {
//**
}

//优化后如下:
flow.mapNotNull{
//**
}.catch {
//**
}

另外对于疯狂刷评论等操作,肯定会导致 评论区的UI 疯狂刷新,可以新建一个队列缓存 comment msg,每秒取 3-4 次,每次取的 msg count 根据设备 level 来定,减少 UI 绘制压力;

成果

单丛直播压测的角度来讲,对于低端设备,结论如下:


成果.png

优化前 推流-评论-60 掉线;
注:以上数字为每秒发送聊天消息*条;

卡顿检测工具

说一下发现 gson 耗时的检测工具:
这版优化前,还有一版优化,当时发现的问题是大量 gson 解析发生在 UI thread, 看下图:


cpu profiler.png

原因是: ws 接收的 coroutine dispatcher 是 Main.
不过也能发现 gson 耗时问题.

拓展
目前进一步的优化所用工具为 tencent matrix.

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 195,783评论 5 462
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 82,360评论 2 373
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 142,942评论 0 325
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 52,507评论 1 267
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 61,324评论 5 358
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 46,299评论 1 273
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 36,685评论 3 386
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 35,358评论 0 254
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 39,652评论 1 293
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 34,704评论 2 312
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 36,465评论 1 326
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 32,318评论 3 313
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 37,711评论 3 299
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 28,991评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 30,265评论 1 251
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 41,661评论 2 342
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 40,864评论 2 335

推荐阅读更多精彩内容