App架构师实践指南五之性能优化二

App架构师实践指南五之性能优化二

从UI和CPU方面来说App流畅体验优化,核心为流畅度/卡顿性能优化。

1、基础原理
用户可以感知卡顿等性能问题的根本原因在于渲染性能。
1.1 绘制原理(16ms原则)。Android系统每隔16ms发出VSync信号,触发对UI进行渲染,这就意味着Android系统要求每一帧都要在16ms这个时间内绘制完成。
如图9-3所示。如果某项操作话费的时间是24ms,系统在得到VSync信号的时候就无法正常渲染,发生丢帧现象,如图9-4所示。


1.2 关于VSync信号。VSync(Vertical Synchronization,垂直同步),这涉及图像显示原理,以传统CRT为例进行阐述(液晶显示器原理类似),CRT电子枪是从上到下一行一行扫描,扫描完后呈现一帧画面,然后电子枪回到初始位置进行下一次扫描。为例同步显示器的显示过程和系统的视频控制过程,显示器或相关硬件会通过硬件时钟产生一系列定时信号,具体为,在新的一行准备扫描前,会发出一个HSync(Horizonal Synchronization,水平同步)信号,当一帧绘制完成,电子枪回到原位准备下一帧前,会发出一个VSync信号,显示器通常以VSync这个固定的频率进行刷新,同时VSync也是作为GPU进行新一帧渲染的触发信号。

1.3 60帧/s与16ms。其实这两者是一个统一的概念。1帧16ms,即1/0.016帧/s=62.5帧/s,而60帧/s的标准是源于人眼和大脑之间的协作无法感知超过60帧/s的画面更新。市场上绝大多数Android设备的屏幕刷新频率都是60Hz,超过60帧/s就没有实际意义。生活中一些常见的重要帧率如下。
---12帧/s:大概类似于手快速翻书的帧率,是人眼认知的连贯动作帧率。
---24帧/s:这是电影胶圈通常使用的频率。
---30帧/s:用于早期的高动态电子游戏中,帧率少于30帧/s就会显得不连贯。
---60帧/s:实际体验中,60帧/s相对于30帧/s有着更好的体验。
---85帧/s:一般而言,这是大脑处理视频的极限。

说明:关于“人眼和大脑之间的协作无法感知超过60帧/s的画面更新”的观点,xiaosongluo进行了更深入的探究,最终结论是“人眼的感知极限是高于60帧/s的,目前显示的性能优化的极限是60帧/s”

1.4 UI绘制机制。智能手机绝大多数的画面渲染都依赖CPU和GPU。具体来说,CPU负责计算显示内容,比如视图的创建、布局计算、图片解码、文本绘制等操作;GPU负责删格化(Rasterization)操作,将UI元素绘制到屏幕上。所谓删格化,即把Button、Shape、Path、String、Bitmap等组件拆分到不同的像素上进行显示,完成绘制,这个操作很耗时,所以引入GPU加快删格化的操作。具体在Android系统中,文字的显示是先经过CPU换算成纹理(Texture),再传给GPU渲染;而图片的显示是先经过CPU计算加载到内存中,再传给GPU渲染;动画的显示是结合文字和图片的显示过程,需要保证在16ms中完成CPU和GPU的计算、绘制、渲染,获得应用流畅性。如果要阐述从底层到上层的具体实现,其中涉及SurfaceFlinger、HWComposer、Surface、Choreographer等众多概念,涉及应用、系统和硬件3个层面。


2、流畅度度量
流畅度(Smoothness,SM),Google官方用词Didplay Performance,业界也有称显示性能、卡顿率、帧率等。系统层是基于SurfaceFlinger合成次数,而应用层级是基于绘制过程中每一帧的关键时间点(FrameInfo,Android 6.0)。


流畅度指标可以说是所有性能指标中最难度量的指标之一,因为要衡量流畅度,帧率仅仅是其中一个方面因素,界面的卡顿与帧率也不是完全称正比的,同时帧率的获取也有多种方案。可以采用下面几种方式。

2.1 基于gfxinfo和GPU配置方案
---手机上直接以条形图形式呈现。
---通过adb shell dumpsys gfxinfo pkg_name命令导出数据(128帧)。根据渲染机制60帧/s原则,可知Draw+Process+Execute<16.67ms,一帧中,如果没有超过的以16.67ms计算,超过的会出现Jank,下一帧必须等VSync到来才开始渲染。此方法中获取的是每一帧的时间信息,但帧与帧之间的时间信息是杂乱的,完全由VSync决定,此时FPS无法简单用总帧数/总时间计算。但是可以通过Jank来计算FPS,具体类似于FrameStatas的方案1.

Draw        Process    Execute
6.24        23.23      0.88


---如果要实时获取帧率数据,可以分析HardwareRender源码中的dumpGfxInfo函数的实现,gfxinfo数据是保存在mProfileData中,而Draw操作填充了mProfileData值,所以通过hook draw方法可实时获取帧率数据。
------FrameStats方案(Android6.0,API 23+)
----------命令。通过以下命令,可以获取每一帧每个关键点的绘制过程中的耗时信息(纳秒时间戳),仅针对应用生成的最后120帧数据。

adb shell dumpsys gfxinfo pkg_name framsatts

例如,如下为某应用卡组页向下滑动一次的数据(操作之前进行reset),第一部分是以聚合的形式呈现,包括Stats since、Janky frames等关键指标,可以整体上感知帧之间的稳定性。


第二部分熟针对每一帧的时间信息,每一行代表一帧数据,,每行每个元素的含义都可以从头部看到,其中比较重要的有IntendedVsync、Vsync、DrawStart、SyncStart和FrameCompleted等。通过这些独立的神数据获取FPS的方案,分别如下。
方案1.公式为FPS = 60XFram/(Frame + Vsync),其中Frame是指获取的帧数,最多为120;Vsync是消耗掉的VSync个数,可以简单理解为Frame中的无效帧数。无效帧数的判别方法为:针对每一帧数据,如果FrameCompleted-IntendedVS有难处<16.67,那么该帧Vsync=0;反之,Vsync=(FrameCompleted-IntendedVsync)/16。67-1(非整数时向上取整)。
方案2.公式为:FPS=time_consumes/(frames_valid-1).其中frames_valid为得到的所有帧中的最大连续有效帧数(前后两帧有效需要满足两帧时间差<100ms),time_consumes为所有frames_valid对应的始终时间。


2.2基于SurfaceFlinger的方案
2.2.1 Android系统中,SurfaceFlinger可以理解为所有Surface管理者角色,由VSync信号驱动执行,当应用对应的Surface更新后,绝大部分都将通过SurfaceFliger合成后在屏幕中显示出来,具体是通过mPageFLipCount统计合成次数(SurfaceFlinger::handleMessageRefresh),合成多少次即向屏幕提交多少帧数据,这个合成次数即我们的FPS数据来源。计算公式为 FPS=(v2-v)(t2-t1),其中t1时刻获取mPageFLipCount的数值v1,t2时刻获取mPageFlipCount的数值v2.
2.2.2 实用命令:service call SurfaceFlinger 1013

2.3 基于Choreographer的方案(Android 4.1,API16+)
2.3.1 Choreographer是用来协调animations、input以及drawing时序的,且每个Looper共用一个Choreographer对象,通过skippedFrames获取在前后两帧时间里(jitterNanos记录)doFrame(Choreographer接收到Vsync信号时的回调渲染接口)中错过了多少个VSync信号,即跳过了多少帧,常用的有以下几种方案。
---借助Logcat。通过修改系统属性debug.choreographer.skipwarning,抓取log中的skipedFrames数据,需要adb root权限,无法实时处理,代码如下:

2.3.2 通过Choreographer.FrameCallback。参考开源TinyDancer-master,需要将代码集成到应用,一般仅适合对自身应用帧率获取,具备实时性。
2.3.3 代码主任。参考腾讯GT,将Choreographer注入带测试应用中,需要root权限,具备实时性。

计算公式:SM=(60x总时间-丢帧数)/总时间。

2.4基于Systrace的方案(Android 4.1,API16+)
2.4.1 Systrace工具介绍。它是由Google官方提供的性能分析工具,具有极其强大的功能.它可以监视和跟踪Android系统行为,清晰地知晓你的时间都去哪儿了,CPU周期消耗在哪里,具体线程/进程在具体时间里的所作所为等。它由内核(Linux内核ftrace)、数据采集(atrace程序)和数据分析(systrace.py脚本,Android自带)3部分组成。
2.4.2 Systrace工具使用。
---图形化。Systrace图形化工具集成在DDMS中,如图9-5所示,横坐标以时间为单位,纵坐标则以进程/线程的方式划分,同一进程的线程组放到一块,可折叠展开。


3、卡顿分析和优化
流畅度就是衡量App卡顿程度的指标。但是,所谓“过早的优化是万恶之源”,我们需要从Make it work到 Make it Right,最后再Make it Fast,切勿抛开业务谈优化。

3.1 CPU耗时/消耗
3.1.1 工具和布局.界面性能取决于UI渲染性能,布局层级过深、无效绘制、布局内容繁杂冗余不规范、自定义View中onDraw涉及复杂运算都会导致界面卡顿,影响UI渲染性能。下面主要从检测工具、布局优化和优雅布局3部分阐述。

3.1.1.1 检测工具
---Hierarchy Viewer。Google官方提供的图形化工具,可以用来优化UI布局层级,删除不必要的view曾经,优化布局速度。Android 4.0以下系统需要手机root支持或者使用第三方工具(如ViewServer),Android 4.1+系统可以直接使用。通过Tree View树状图直观分析view层级。详情中会有红、绿、黄三圆点,若红点较多,可能是布局层级太深或者自定义绘制有问题,涉及复杂计算等。
---Lint。Lint是GOOGLE官方提供的一款检测代码质量的工具,可以用来检查和优化UI布局的一些显性问题或建议优化问题。
---Profile GPU Rendering。通过“设置-->开发者选项-->GPU呈现模式分析-->在屏幕上显示条形图”开启,用来分析页面是否在16ms内完成绘制并渲染。
---TraceView。Google提供的安卓平台下数据采集和分析工具,以图形化方式呈现分析结果,可以清晰地知晓每个函数消耗的时间,通过函数调用次数以及时间消耗对比分析可以定位一些UI性能问题。
---Android Monitor(CPU).Google Studio提供的Android Monitor工具,也可以直观地了解当前App的CPU使用现状(百分比方式呈现实时CPU消耗,如图9-7所示,非常直观)
---Trepn Profiler。这是高通提供的检测分析手机CPU消耗工具,手机需要root,且支持CPU为高通手机。

3.1.1.2布局优化
---善用Tag,布局模块化,Google官方建议如下。
(1)使用<include>标签来重用布局,布局模块化。
(2)使用<merge>标签来减少View层级结构,主要解决<include>或自定义组合ViewGroup导致的冗余层级问题。
(3)使用<ViewStub>标签代替setVisibility,需按需载入,只有在布局文件重用时才加载,再inflate。
---减少布局层级和复杂度,减少overdraw
(1)尽量多使用RelativeLayout和LinearLayout,不要使用绝对布局AbsoluteLayout。
(2)尽量不要嵌套使用RelativLayout。
(3)尽量不要在嵌套的LinearLayout中使用weight属性。
(4)去掉多余的背景颜色,去掉不必要的父布局。
(5)尽量使Layout宽而浅,而不是窄而深,以减少view树的层级为主。
(6)采用自定义view代替复杂嵌套的深层级布局(业务需求,无法优化下)。
(7)Hierarchy Viewer超过5层,考虑一下优化。
---ListView优化
(1)contentView服用。
(2)避免重复调用findViewById(holder)
(3)优化Item布局
(4)分页加载
---TextView优化。TextView的优化主要在其渲染上,其文字渲染主要是BoringLayout、DynamicLayout、StaticLayout 3个类。如果单纯文字显示的话,建议用StaticLayout,其可以把StaticLayout加入TextLayoutCache,起到缓存效果,从而避开TextView的一系列操作。
3.1.1.3 优雅布局
---代码格式化布局,删除注释。
---避免Hard code(硬编码)。
---不使用废弃关键字,如dip、fill_parent等。
---尽可能消除warning、单词编写错误等。


3.1.2 复杂运算。复杂运算也会导致卡顿,特别是UI线程的复杂运算将直接导致UI无响应,极限下导致ANR。一般运算导致的卡顿的分析,可以使用StrictMode工具,其基于线程/VM设置一些策略来检测代码的违规,通过输出trace文件来分析定位卡点。代码的不合理使用也会导致运算的复杂程度增加,如两个float数值的比较执行时间是int数值的4倍左右。

3.2 GPU耗时/消耗
过度绘制。过度绘制(overdraw)其实也属于UI布局,涉及GPU渲染,也称GPU overdraw。其含义是屏幕上的某个像素点在同一帧中被绘制了多次,复杂UI层级叠加、太多View叠加或者inflate时间过长都会导致overdraw。

3.2.1 检测工具。Android提供来Hierarchy Viewer、Tracer for OpenGL和Show GPU overdraw3个工具来辨识解决overdraw问题。
---Hierachy Viewer。
---Tracer for OpenGL。集成在Android Device Moinitor中,通过单击Tracer for OpenGL ES按钮进入,单击Trace按钮开启,单击Stop按钮关闭并生成GLTrace文件,再对GLTrace文件分析,从而解决overdraw问题。
---Show GPU overdraw。通过设置-->开发者选项-->调试GPU过度绘制中打开显示过度绘制区域,会看到多种颜色分别表示过度绘制的次数。从好到差依次为蓝-->绿-->淡红-->红,分别表示1x、2x、3x和4x过度绘制。
---Android Monitor(GPU)。通过Google Studio提供的Android Monitor工具也可以直观地了解当前App的GPU使用现状,比如当前帧消耗了多长时间,如图9-8所示。

3.2.2 优化建议。
---移除window中默认的background。代码为:getWindow().setBackgroundDrawableResource(android.R.color.transparent);
---移除布局中冗余的background
---不需要显示的布局要及时隐藏(如层叠UI中,被遮挡布局等)。
---按需显示占位背景图片,减少Drawable复杂shape使用。
---自定义view中,使用clipRect和quickReject来屏蔽那些重叠画面中被遮盖或者不需要view的绘制,跳过指定区域view的绘制。
---自定义view中,慎待onDraw函数,减少多次使用。
---Show GPU overdraw中,将overdraw控制在2x,不允许存在4x情形,3x面积不允许超过一定比例,如1/3平米面积。
---注意Hierarchy Viewer工具中的红、绿、黄三圆点check以及Lint工具优化建议。


3.3内存相关
GC。频繁的GC会导致卡顿,其原因为执行GC时任何其他线程都会暂停,等待GC执行完后再继续。GC触发的原因有很多,内存抖动,大内存申请等都有可能触发GC.

3.4线程相关
3.4.1 ANR(Application Not Responding,中文为“应用无响应”),当在UI线程(主线程)中做了阻塞耗时操作或者在超时时间里对输入事件或特定操作没有处理完时会发生ANR,常见情景及时间限定如下
---service声明周期函数,20s
---broadcast receiver接收前台优先级广播函数,10s
---broadcast receiver接收后台优先级广播函数,60s
---影响进程启动的函数,10s
---影响输入事件处理的函数,5s
---影响Activity切换的函数,2s。

最后两种情景会弹出系统对话框,因为涉及用户交互。ANR时会在/data/anr/目录下生成一个trace.txt文件,这个文件结合CPU使用率可以分析定位ANR的原因。常见优化建议如下:
---应用进程自身引起。
------祝线程阻塞、挂起、死循环。
------其他线程CPU占用率高,使得主线程无法抢到CPU时间片。
---其他进程间接引起。
------多进程间通信,当前进程超时间未收到其他进程的反馈,等待超时。
------其它进程CPU占用率过高,使得当前进程无法抢到CPU时间片。
总结就是主线程阻塞和CPU满负荷,可以通过开辟单独子线程异步来处理耗时和IO阻塞任务,不做任何阻塞主线程的操作。另外,内存不够用时也可能导致ANR.

3.4.2多线程并发。多线程并发将使UI线程分到的CPU执行时间减少,导致卡顿。

猜你喜欢

转载自www.cnblogs.com/timxgb/p/10534599.html