QQ直播Android端UI卡顿优化实践
在体验QQ直播过程中时常遇到体验不流畅的问题,在相对性能差点的设备上滑动卡顿较为明显,造成用户体验不佳,为了提供一流直播体验,故而着手优化,给用户提供极致的直播体验。
内容摘要
本文首先会介绍卡顿优化的整体思路,然后对具体案例进行讲解分析,读完本文你将了解到:- 从常规问题到复杂问题,如何分析和定位
- 如何运用不同的工具综合分析问题
- 如何通过压力测试暴露性能问题
优化思路
进行卡顿优化前,我们需要知道卡顿的定义是什么,如何发现卡顿问题,以及哪些因素会导致卡顿。一、什么是卡顿?
个人而言,认为卡顿总体可以分为两类:UI卡顿及用户感观上的卡顿: 1. UI卡顿 Google流畅度机制-黄油计划(Jank)的卡顿度量思路如下:考虑视觉惯性,以硬件vsync时间间隔,连续1次vsync没有新画面刷新,则认为是一次卡顿,也就是说下一次vsync时间点没有新画面刷新,则认为是一次Jank
PerfDog:Jank卡顿及stutter卡顿率说明
二、如何发现UI卡顿问题?
可以从两个方面考虑,线上和线下: 线下:通过线下工具、功能体验、性能测试等方式发现 线上:通过监控上报、用户反馈和用户日志发现三、哪些因素会导致UI卡顿?
除了一些比较熟知的会block主线程的因素外,我们需要从应用运行的整体环境来考虑:一个设备的CPU、内存是有限的资源。从多应用的角度,CPU、内存被多个应用使用,从多进程的角度,CPU、内存被多个进程使用,从多线程的角度,CPU、内存被多个线程使用。一个线程、进程不能持续占用CPU时间,操作系统通过调度算法,来进行分配。
Round-robin scheduling(wikipedia)
CPU timeline(Perfetto) 主线程因为某种原因分配不到足够的CPU时,也可能出现卡顿。
四、QQ直播UI卡顿优化的两个阶段
第一阶段:点对点优化 这个阶段主要通过Android Studio Profiler和腾讯公司内部性能监控平台(后面简称“监控平台”)找出明确的卡顿堆栈,发现什么问题,解决什么问题,问题和解决方式一般都比较常规。这部分就不做具体介绍了,典型问题如下表:|
卡顿场景
|
优化手段
|
|
IO读写等阻塞操作、图像处理等耗时操作
|
卡顿操作放到子线程
|
|
SP读写
|
SP KV存储替换为MMKV存储
|
|
View创建
|
延迟创建/延迟加载/按需创建
|
|
同步IPC
|
同步变为异步或者子线程操作
|
|
SDK或第三方业务代码或者库的卡顿
|
推动第三方业务优化
|
案例分析
下面的内容将从View性能、任务拆解、压力测试、高频日志场景中提炼一些典型案例,从问题入手,介绍下我们的卡顿优化方法。一、 View性能优化
1. 布局 层级优化
在通过监控平台查询现网的卡顿问题时,我们发现交友房有一个卡顿问题占比较大,其堆栈如图(火焰图):
交友房使用了组件框架来组织复杂的UI,下图圈出部分以及背景皮肤均为独立的组件。组件的组织方式如图,是一个树状结构,每个组件有一个View层级。
此外针对团战组件布局层次过深的问题,使用ConstraintLayout减少布局层级,对于自定义View的xml,通过xml的merge标签减少了一层布局,最终交友房间的布局层级 从最深10层,减少为最深5层
2. View复用
通过Systrace可以看到View inflate前有一段空白(下图第一个红框),这个时间主要是读取预编译xml并解析的时间,在第二个红框可以看到已经小了很多,这是因为infalte相同的布局时命中了xml cache。
通过 TraceCompat 增加自定义trace section后发现单个xml读取的时间并不长(1ms以内,下图getLayout为自定义section),然而由于交友房有很多自定义View,都需要inflate 布局xml,这个过程总的时间就比较长,且切房以后xml cache就无法再命中
在分析inflate耗时的过程中还发现如图所示的trace:View创建的耗时有一半是空白,这里有耗时,但耗时的原因不明,就像一个“黑洞”!( 黑洞的引力过大,以至于光都无法逃逸,科学家最初通过恒星围绕其的运动规律及其发出的X射线,发现了黑洞 )
通过在View的构造器,增加自定义section后发现, findViewById 耗时较短(下图红色部分),有一个 第三方sdk调用 的耗时很长(棕色部分),占了View创建时间的一半左右
优化方案: 考虑舞台View的创建成本较大,切房的过程中xml cache失效问题,View复用是一个比较好的解决方案,利用系统提供的api,设置定义的Layout factory,在View和窗口detach时,加入到cache中,在创建view时,优先从cache中获取View
不过这里有一个坑,在设置Factory2时,要先判断Factory1和Factory2是否有设置过,有设置过就不能再设置了,否则在部分机型上会crash。
此外,针对sdk导致的卡顿,我们推动sdk方对卡顿做了优化。二、任务拆解
这里,我们将利用Android Studio Profiler工具主动去研究进房和切房过程中的事件,找到优化点。 如背景所述,为了提升切房体验,在用户进入A房间时会预加载B房间,预加载的过程中会创建B房间的UI界面,包括顶部、底部、公屏、及播放器View。如图所示:
通过Android Studio Profiler 抓到的过程trace如图
问题分析: Android通过消息循环的方式来调度主线程任务:需要创建View时,框架会向系统发一条消息,这条消息经过消息循环,最终被调度到,执行View的创建,如果创建的过程比较慢,就会阻塞队列中后面的消息,导致卡顿。
直播切房过程中,A房间切到B房间的过程如图:
这三个操作是在一条消息中被调度的,串行执行,C房间界面创建的耗时过大,整条消息的处理时间就会变长,就会导致卡顿。 优化方案:
- 任务拆解:C房间预创建提交到下一条消息中进行调度
- 优化房间View层级,降低创建耗时
- 按需初始化或延迟初始化View:例如不初始化横屏底部View
三、压力测试
这一部分我们将从通过压力测试的方式发现一些特殊条件下出现的卡顿,压力测试具体分为以下两项:-
切房压力测试 :以每秒1个的速度切房,切100个,每个类型的房间单独测试,每次测三遍 -
观看压力测试 :开启房间的压测开关(弹幕,礼物等),进行持续观看
1. 内存泄漏导致的卡顿
在切房压测的过程中,我们发现两个典型卡顿case,是内存泄露导致的Case 1: Java内存泄露
娱乐房间的压测过程中,发现内存持续增长,不释放,且后期滑动明显越发卡顿
- 解决泄露问题
- 每个版本性能测试要进行压测,验证是否引入泄露问题
Case 2: 线程数过多导致内存泄露
交友房间的压测过程中,发现pss内存和虚拟内存持续增长,且滑动越来越卡顿。
- 保留核心组件,采用组件二分的方式下架功能组件,发现当音频组件下架后,内存不会一直增长。
- 把其他组件全部加回来,单独去掉音频组件,内存不会增长。
随着切房过程,线程数越来越多,线程dump:
- 修复sdk问题
- 每个版本性能测试要进行压测,验证是否引入泄露问题
2.RV Adapter更新卡顿
在观看压测的过程中,我们发现一个典型卡顿caseCase1. RV Adapter pending更新操作过多导致的卡顿
在弹幕压测开启(模拟热门房间,用户发言非常多的场景)的情况下,进入游戏直播间,在横屏模式下观看,当时间达到5分钟以上,切回竖屏,发现会出现长时间卡顿 问题分析: 通过监控平台抓到的trace如图,卡顿可能与RecyclerView有关
为了避免消息过多,有一个消息清除逻辑,保证消息数不超过阈值。实际测试发现了三个特征:
- 消息插入和adapter更新操作在横屏的情况下也会进行,即使横屏情况下公屏是看不到的
- 当消息数达到阈值后,每收到一条新消息,流程图中红色的部分都会执行,由于弹幕消息速度很大,故执行次数很多
- 竖屏模式下停留5分钟以上不会出现卡顿,仅在横屏模式停留5分钟以上,切回竖屏的时候卡顿
再结合对卡顿堆栈和AdapterHelper源码的分析可以猜测,问题就出在红色流程部分,当把阈值调大到 Integer.MAX_VALUE 后,卡顿消失,猜测得到证实。为了搞明白第三个特征的成因,需进一步分析源码。
原来,竖屏切到横屏时,公屏组件会隐藏,竖屏情况下对Adapter执行的更新操作,只会加到AdapterHelper的pending Updates中去,不会触发requestLayout,当横屏切为竖屏后,公屏组件显示,在layout过程中,pending Updates被执行,由于长时间停留在横屏积攒了大量的pending Remove操作,故在执行的过程中出现卡顿。 优化方案: 优化的目标是减少对adapter的更新操作,尤其是在横屏情况下。因此在插入消息前,如果:- 处于横屏,把消息添加到cache中,为避免cache过大,当达到一定阈值时,cache缩水到(cache_size-reduce_size)个元素,把老的消息抛弃。
-
处于竖屏,为了避免消息数超过阈值后,频繁更新adapter,相比之前缩小1,这里缩小reduce_size,这个值越大,下次达到阈值的时间也就越长,adapter的更新频率就越低
四、高频日志
线上线下都可能出现高频日志问题,但由于环境差异,分析的方式也不相同,下面分别讲下线上和线下的分析方法,不保证涵盖所有case1. 线下性能测试中遇到的卡顿
在基于893 release包测试性能数据时,发现切房时的CPU占用,三种房间比上个版本都高出10%,卡顿率基本持平(即使在已经有卡顿优化的前提下) 问题分析: CPU问题,首先想到的分析方法是分析线程的CPU占用,通过Systrace抓取到trace后,在Perfetto的Sql视图下执行以下sql,可以得到每个线程的CPU时间SELECT t.tname, SUM(dur) AS durationFROM (SELECT ts, dur, cpu, end_state, priority, process.name AS pname, thread.name AS tnameFROM sched_sliceLEFT JOIN thread USING (utid)LEFT JOIN process USING (upid)) tWHERE t.pname = <your process name>GROUP BY t.tnameORDER BY duration DESC
进一步测试,发现日志中有高频日志及大日志 优化方案:
- 对高频日志和大日志进行优化,优化后CPU占用和卡顿率达到了预期水平
- 版本性能测试中关注cpu增长及cpu均值,开发过程中注意避免高频日志和大日志
- 开发高频日志及大日志检测工具
2. 线上活动中遇到的卡顿
在一次大房活动中,不少用户反馈,横屏弹幕和视频流卡顿的问题,录屏如图:弹幕没有平滑地向左移动,视频流时断时续。
初步猜测,某种原因使GC频繁触发,导致了周期性卡顿。
- 修复高频日志问题
- 其他方案同上一个高频日志case
优化效果
典型问题讲完了,下面是卡顿优化的整体效果。 一、优化前后对比
优化总结
QQ直播经过两个阶段的卡顿优化,直播的观看和切房体验有比较明显的的提升,整个过程的经验总结如下:一、如何发现问题
线上:- 接入了线上性能监控平台,通过卡顿上报,可以发现线上卡顿问题
- 通过用户的反馈和线上日志(增加GC/CPU/内存等性能日志)发现问题
- 通过功能体验和性能测试发现卡顿问题
- 通过Android Studio Profiler/Systrace/Perfetto等工具找到卡顿问题
- 通过压力测试发现卡顿问题
二、如何分析问题
- 对流程进行分析:某些操作是否过早,过晚,过慢,或者不必要,某些任务是否可以拆分
- 对卡顿问题进行多角度深入分析,不放过细节,找到耗时的根因
- 从CPU/内存/App环境等多个角度分析卡顿的源头,不局限于卡顿堆栈
- 灵活运用多种工具及日志综合分析问题
三、如何判断卡顿的优化效果
线下:可以给每个优化点配置一个开关,这样可以用一个包,控制变量的方式来验证优化效果,然后通过PerfDog,对比优化前后的卡顿率 线上:可以进行AB实验,验证优化对PV/UV等数据的影响四、重视性能测试和压力测试
实践发现,不少问题功能测试不一定能发现,但通过性能测试和压力测试可以发现。此外,不同版本App的性能测试要保证测试环境(包括时间、机型、网络、账号等),测试方式的一致性参考文献
[1] PerfDog Jank卡顿及stutter卡顿率说明:https://perfdog.qq.com/article_detail?id=10162&issue_id=0&plat_id=1