腾讯VATeam

QQ直播Android端UI卡顿优化实践

背景 QQ直播是手机QQ的一个子业务,目前包含了娱乐、游戏、交友三种直播类型

‍ 娱乐直播和游戏直播属于音视频直播,交友直播属于音频直播,游戏房间和部分娱乐房间支持横屏模式。为了切房体验的流畅性,在进入上述三种直播间时,会对下一个直播间进行预加载,这样用户在切换到下个直播间时,音视频的播放是无缝的,底部和顶部的UI也会即时展示出来。


在体验QQ直播过程中时常遇到体验不流畅的问题,在相对性能差点的设备上滑动卡顿较为明显,造成用户体验不佳,为了提供一流直播体验,故而着手优化,给用户提供极致的直播体验。

内容摘要

本文首先会介绍卡顿优化的整体思路,然后对具体案例进行讲解分析,读完本文你将了解到:
  • 从常规问题到复杂问题,如何分析和定位
  • 如何运用不同的工具综合分析问题
  • 如何通过压力测试暴露性能问题

优化思路

进行卡顿优化前,我们需要知道卡顿的定义是什么,如何发现卡顿问题,以及哪些因素会导致卡顿。

一、什么是卡顿?

个人而言,认为卡顿总体可以分为两类:UI卡顿及用户感观上的卡顿: 1. UI卡顿 Google流畅度机制-黄油计划(Jank)的卡顿度量思路如下:

考虑视觉惯性,以硬件vsync时间间隔,连续1次vsync没有新画面刷新,则认为是一次卡顿,也就是说下一次vsync时间点没有新画面刷新,则认为是一次Jank

PerfDog:Jank卡顿及stutter卡顿率说明

Google Jank 2. 用户卡顿 对用户来说,卡顿是感官上任何阻塞其使用体验的表现,例如动画、视频的不流畅,点击滑动操作的无响应,界面元素出来的慢等等。这意味着当接口回包慢,导致界面更新不及时或者网络差导致音视频播放不流畅,对用户来说同样是卡顿。本文的话题将围绕着UI卡顿展开

二、如何发现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或第三方业务代码或者库的卡顿

推动第三方业务优化

第二阶段:综合分析与优化 这个阶段在第一阶段使用到的工具基础上,结合更多工具如Systrace/Perfetto/手Q日志/Aegis日志等进行综合分析,这一阶段会从流程上、时序上、应用环境(CPU/内存等)上进行细化分析,并结合压测,找到更多优化空间,问题一般不太常规,分析难度更大。下面着重讲一下这个阶段的典型案例。

案例分析

下面的内容将从View性能、任务拆解、压力测试、高频日志场景中提炼一些典型案例,从问题入手,介绍下我们的卡顿优化方法。

一、 View性能优化

1. 布局 层级优化

在通过监控平台查询现网的卡顿问题时,我们发现交友房有一个卡顿问题占比较大,其堆栈如图(火焰图):

问题分析: 从火焰图可以看出,整个组件 onMeasure 卡顿时长达200ms左右,有四个可疑堆栈,从每个堆栈的叶子节点上看不出明显的卡顿因素,但有一个特点:ViewGroup的嵌套层级很深,最深处达10层。为了确定是onMeasure卡顿,而非误报,我们基于debug包,用Systrace工具,抓取了切房过程的trace,发现首次进房onMeasure耗时确实较长,选中measure片段后,可以看到一个描述: 嵌套ViewGroup可能多次测量子View,可能导致耗时和重复性的工作。


交友房使用了组件框架来组织复杂的UI,下图圈出部分以及背景皮肤均为独立的组件。组件的组织方式如图,是一个树状结构,每个组件有一个View层级。


分 析发现,有一些层级是不必要的,例如基础房间组件没必要挂在交友根组件下。
优化方案: 通过替换组件的父View ID,我们把大部分组件都直接挂在了根View上,再通过特殊方式把团战组件也挂在了根View上。


此外针对团战组件布局层次过深的问题,使用ConstraintLayout减少布局层级,对于自定义View的xml,通过xml的merge标签减少了一层布局,最终交友房间的布局层级 从最深10层,减少为最深5层

2. View复用

世界首张黑洞照片(wikipedia) 在进行了一轮层级优化后,我们通过Systrace发现另外一个问题,交友房间的舞台View inflate耗时很长

问题分析: 交友房间的舞台View中包含很多自定义View(通过inflate xml实现的),自定义View的xml再引用别的自定义View,就会导致inflate的深度变深,而xml的inflate过程是有IO操作的。

Android的UI布局文件xml,最终会被编译优化,通过二进制形式保存在apk中,运行期会通过LayoutInflater进行加载,这个加载过程涉及io操作,可能导致卡顿。Android框架对xml加载有一个缓存机制,用于缓存最近使用的过的、解析后的XmlBlock,但这个缓存容量很小,详见 android.content.res.ResourcesImpl


通过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个,每个类型的房间单独测试,每次测三遍
  • 观看压力测试 :开启房间的压测开关(弹幕,礼物等),进行持续观看
基于PerfDog(腾讯App性能测试工具)记录性能数据(卡顿率,FPS,内存,CPU等)。 目前我们发现以下几种Case:

1. 内存泄漏导致的卡顿

在切房压测的过程中,我们发现两个典型卡顿case,是内存泄露导致的
Case 1: Java内存泄露
娱乐房间的压测过程中,发现内存持续增长,不释放,且后期滑动明显越发卡顿

问题分析: 问题是必现的,通过Android Studio Profiler工具分析后发现了一个版本新增的内存泄漏,解决后发现仍然存在问题,不过只有在release app上才会出现,因此无法用Android Studio Profiler分析,后来通过监控平台线上内存泄漏上报发现了两个可疑的内存泄漏上报。 优化方案:
  • 解决泄露问题
  • 每个版本性能测试要进行压测,验证是否引入泄露问题

Case 2: 线程数过多导致内存泄露
交友房间的压测过程中,发现pss内存和虚拟内存持续增长,且滑动越来越卡顿。

问题分析: 首先,通过Case1的方法,我们排除了Java内存泄露。由于交友组件化比较彻底,可以通过线上开关直接控制组件的加载来确定哪个组件出现了问题。
  • 保留核心组件,采用组件二分的方式下架功能组件,发现当音频组件下架后,内存不会一直增长。
  • 把其他组件全部加回来,单独去掉音频组件,内存不会增长。
至此确定是音频组件问题 通过Systrace发现无耗时异常长的线程,CPU占用也正常,但发现数量极大的无CPU时间占用的VideoConsumer线程


随着切房过程,线程数越来越多,线程dump:

每创建一个线程,都要分配一段内存,因此可知, 内存增长是因为创建了大量线程 。
交友房间只使用了音频能力,不会使用视频能力,拉sdk同学排查后,最终定位到问题 优化方案:
  • 修复sdk问题
  • 每个版本性能测试要进行压测,验证是否引入泄露问题

2.RV Adapter更新卡顿

在观看压测的过程中,我们发现一个典型卡顿case
Case1. RV Adapter pending更新操作过多导致的卡顿
在弹幕压测开启(模拟热门房间,用户发言非常多的场景)的情况下,进入游戏直播间,在横屏模式下观看,当时间达到5分钟以上,切回竖屏,发现会出现长时间卡顿 问题分析: 通过监控平台抓到的trace如图,卡顿可能与RecyclerView有关


通过Android Studio Profiler进一步抓取堆栈后发现 AdapterHelper#findPositionOffset 的调用像是在循环调用,调用次数很多,上层方法无法返回,从而产生了长时间卡顿。
查看代码发现 公屏ReccyclerView 的嫌疑较大,注释公屏代码后,没有卡顿了,联系到开启弹幕压测后,公屏会有大量消息插入,可能是这个原因导致的卡顿。
根据卡顿trace,查看 AdapterHelper#preProcess 源码,发现它在 循环 执行 pending 的Update操作,上面的卡顿Case里是在执行pending的 Remove 操作。

这里做一个猜测:卡顿与RV的 Remove 操作有关,基于这个操作分析公屏代码,其消息插入流程如图:


为了避免消息过多,有一个消息清除逻辑,保证消息数不超过阈值。实际测试发现了三个特征:
  • 消息插入和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的更新频率就越低


此外,在横屏切回竖屏时,展示横屏cache中的数据,竖屏切为横屏时,将竖屏的消息复制到横屏cache里

四、高频日志

线上线下都可能出现高频日志问题,但由于环境差异,分析的方式也不相同,下面分别讲下线上和线下的分析方法,不保证涵盖所有case

1. 线下性能测试中遇到的卡顿

在基于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 tname  FROM sched_slice    LEFT 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占用和卡顿率达到了预期水平
  • 版本性能测试中关注cpu增长及cpu均值,开发过程中注意避免高频日志和大日志
  • 开发高频日志及大日志检测工具

2. 线上活动中遇到的卡顿

在一次大房活动中,不少用户反馈,横屏弹幕和视频流卡顿的问题,录屏如图:弹幕没有平滑地向左移动,视频流时断时续。

问题分析:
这是一个现网问题,无法用Android Studio Profiler等线下工具进行分析。此外,由于监控平台采样率的问题,从平台上查不到反馈的几个用户的卡顿trace,目前唯一的信息是用户的 日志。
根据日志搜索卡顿性能相关的关键字,没找到有效信息(搜到部分堆栈,但与弹幕或视频流无关),音视频同学通过线上监控数据,排除了流卡顿的因素,视频卡顿和弹幕卡顿很可能都是UI卡顿导致的。 进一步搜到性能相关日志,发现GC触发的频率很高,平均约500ms每次,GC的触发间隔大体与UI卡顿间隔一致


初步猜测,某种原因使GC频繁触发,导致了周期性卡顿。
进一步对GC日志的 上下文 进行搜索,发现H5的log输出非常频繁,达每秒15次,且大日志很多,同一时间,未出现卡顿的用户,没有高频GC和高频日志问题,考虑到活动房间有多个运营H5页面,根据先前高频日志处理经验,初步怀疑是H5输出高频日志导致的卡顿,但由于大房活动期间,弹幕数、点赞飘心数也比较多,无法断言是H5高频日志导致的卡顿。
如何排除非高频日志的因素? 由于大房已经下播,活动已经下线,无法用原来的房间进行测试,与后台同学沟通后,我们选定了一个房间,开启弹幕,点赞飘心的压测,排除弹幕数,点赞飘心数这个因素,测试发现,弹幕数,点赞飘心数过多不会导致卡顿。 如何确认是高频日志问题? 与运营同学及运营H5开发同学沟通后,我们配置了和活动大房一样的运营活动,并本地集成了大房活动期间的js,最终复现了卡顿问题 优化方案:
  • 修复高频日志问题
  • 其他方案同上一个高频日志case

优化效果

典型问题讲完了,下面是卡顿优化的整体效果。 一、优化前后对比
左边为QQ直播老版本, 右边为QQ直播新版本

‍

二、与其他直播App对比
左边为某直播App, 右边为QQ直播

相比老版本, 卡顿率娱乐房间优化 78% ,游戏房间优化 74% ,交友房间优化 82%, 相比对比的直播App,QQ直播体验更顺滑、流畅。具体卡顿率数据如下表:

优化总结

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

‍‍