设计师,开发人员,需求研究和测试都会影响到一个app最后的UI展示,所有人都很乐于去建议app应该怎么去展示UI。UI也是app和用户打交道的部分,直接对用户形成品牌意识,需要仔细的设计。无论你的appUI是简单还是复杂,重要的是性能一定要好。
UI性能测试
性能优化都需要有一个目标,UI的性能优化也是一样。你可能会觉得“我的app加载很快”很重要,但我们还需要了解终端用户的期望,是否可以去量化这些期望呢?我们可以从人机交互心理学的角度来考虑这个问题。研究表明,0-100毫秒以内的延迟对人来说是瞬时的,100-300毫秒则会感觉明显卡顿,300-1000毫秒会让用户觉得“手机卡死了”,超过1000ms就会让用户想去干别等事情了。
卡顿(Jank)
内容的快速加载很重要,渲染的流畅性也很重要。android团队把滞缓,不流畅的动画定义为jank,一般是由于丢帧引起的。安卓设备的屏幕刷新率一般是60帧每秒(1/60fps=16.6ms每帧),所以你想要渲染的内容能在16ms内完成十分关键。每丢一帧,用户就会感觉的动画在跳动,会出现违和感。为了保证动画的流畅性,我们接下来看下从哪些方面优化可以让内容在16ms内渲染完成,同时分析一些常见的导致UI卡顿的问题。
android设备的UI渲染性能
搭建Views
RemeasureingViews(重新测量views)
虽然可以通过xml文件查看所有的view,但不一定能轻易的查出哪些view是多余的。要找到那些多余的view(增加渲染延迟的view),可以用androidstudiomonitor里的HierarachyViewer工具,可视化的查看所有的view。(monitor是个独立的app,下载androidstudio的时候会同时下载)
HierarchyViewer
HierarchyViewer可以很方便可视化的查看屏幕上套嵌view结构,是查看你的view结构的实用工具。这个工具包含在androidstudiomonitor当中,需要运行在带有开发者版本的android系统的设备上。后续所有的view和屏幕截图都来自一款三星的NoteII设备,系统版本是JellyBean。在老的设备(处理器慢)上测试渲染性能,更容易发现问题。
如图4-2所示,打开HierarchyViewer之后,会看到几个窗口:左边的窗口列出了连上你电脑的android设备和设备上所有运行的进程。活跃的进程是粗体展示的。第二个tab某一个编译版本的详情(后面细说)。中间的部分是可缩放的view的树形图。点击某一个view能看到在设备上展示的样子和一些额外的数据。右边有两个view:树形结构总览和布局view。树形结构总览显示了整个view的树形结构,里面有一个方块显示了中间窗口在整个树形结构当中所处的位置。布局view当中深红色高亮的区域表示所选中的view被绘制的部分(浅红色展示的是父view)。
我们再看另一个新闻类app是怎么来减少标题view里面的子view数量的。从图4-6里能看到一个和图4-5类似的树形结构图。
为了更好的了解这部分的优化,我们再看另一个例子app。这个例子会展示一个山羊图片等列表。界面使用了几种不同的layout方式,性能差的和性能好的都有。仔细的查看这些布局,然后一步步优化它们,我们就能清楚的理解怎么去优化一个app的渲染性能了。我们分几步来进行优化,每一步改变都可以通过HierarchyView可视化的查看。每换一种layout方式,xml渲染的性能要么变好,要么变差。我们先从性能差的布局方式开始。先快速的扫一眼图4-8里的HierarchyView。
这个简单的app里有59个view。但是和图4-4里的app不同,这个app的树形结构更扁平,水平方向的view更多一些。叠加的view越多,渲染就会越费时,减少view树形结构的深度,app每一帧的渲染就会变快。
蓝色方框里面的view是actionbar。橘色方框里的是屏幕顶部的textbox,紫色方框里展示的是山羊的详细信息(有6个这种view)。红色方框标示了7个view,每个都增加了树形结构的深度。我们仔细看些这7个view其中三个的remeasure数据(图4-9)。
我们可以继续看下山羊信息到row展示部分,来继续减少view结构的深度。每一行山羊信息有6个view,一个有6行数据在屏幕中展示(图4-8中有一行数据是用紫色方框高亮的)。我们用HierarchyView看下一行view的结构是怎么样的(图4-11),先看下左边两个view(一个LinearLayout,一个RelativeLayout),这两个view唯一的作用就是加深了树机构的深度。LinearLayout连接了RelativeLayout,但并没有展示其他什么内容。
但效果还并不理想。我们继续移除LinearLayout,同时调整下RelativeLayout来展示整个row的信息(图4-13),这样深度近一步减少到了2。渲染又快了0.1ms。这样看来优化的途径有很多种,多尝试总是有好处的(看下表格4-1里的结果)。
View的重用
如果一个程序员面向对象编程经验丰富,他就会尽可能重用创建的view(而不是每次都创建)。拿上面山羊app作为例子,其实每一行展示的layout都是重用的。如果xml文件里最外层的view只是用来承载子view的,那这个view只不过是增加了view结构的深度,这种情况下,我们可以移除这个view,用一个merge标签来代替。这种方式可以移除树形结构里多余的层。
HierarchyViewer(不止是树形结构图)
HierarchyViewer还有一个功能,可以帮助开发者发现overdraw(重复的绘制)。从左到右看下树形结构窗口的选项,可以发现这些功能:
资源缩减
这样一个资源文件就可以表示几种不同的状态了(加星或者不加星,在线或者离线等等)。
屏幕的重复绘制
每过几年,就会有传闻说某个博物馆在用x光扫描一副无价的名画之后,发现画作的作者其实重用了老的画布,在名画的底下还藏着另一副没有被发现的画作。有时候,博物馆还能用高级的图像技术来还原画布上的原作。android里面的view的绘制就是类似的情况。当android系统绘制屏幕的时候,先画父view,然后子view,再是更深的子view等等。这会导致所有的view都被绘制到了屏幕上,就像画家的画布一样,这些view都被他们的子view覆盖住了。
文艺复兴时期,有很多伟大的画家要等画干了以后才能重用画布。但在我们的高科技触摸屏上,屏幕重画的速度要快几个数量级,但是多次的重新绘制屏幕会使得绘制延迟变大,最终导致布局的卡顿。重新绘制屏幕的行为叫做overdraw,下面我们会看下怎么检测overdraw。
检测overdraw
android提供了一些很好的工具来检测overdraw。JellyBean4.2里,开发者选项菜单里增加了DebugGPUOverdraw的选项。如果你用的是JellyBean4.3或者KitKat设备,在屏幕的左下角会有一个计数展示屏幕overdraw的程度。我亲身试过这个工具对检测overdraw十分有效。虽然有时候这个会多提示6-7次overdraw(发生的概率还不小)。
图4-14中的截图还是来自上面的山羊app。左下方可以看到overdraw的计数。屏幕中可以看到3个overdraw的计数,其中开发者能控制的是主窗口的计数。overdraw的计数是在左下方。没优化过的appoverdraw的次数是8.43,我们优化过后可以降到1.38。导航栏overdraw的次数是1.2(菜单按钮是2.4),也就是说文字和图标的overdraw贡献了额外的20%。overdraw计数可以在不影响用户体验的前提下,快速便捷的比较不同app的overdraw,但没办法定位overdraw是哪里产生的。
另一种查看overdraw的方式是在DebugGPUoverdraw菜单里选择“ShowOverdrawareas”选项。选择之后,会在app的不同区域覆盖不同的颜色来表示overdraw的次数。比较屏幕上这些不同的颜色,可以快速方便的定位overdraw问题:
白色:没有overdraw蓝色:1xoverdraw(屏幕绘制了2次)绿色:2xoverdraw浅红色:3xoverdraw深红色:4x或者更多overdraw
在图4-15中,可以看到山羊app优化前后overdraw区域的变化。app的菜单栏优化前后都没有颜色(没有overdraw),但android图标和菜单按钮图标都是绿色的(2xoverdraw)。山羊图片等列表在优化之前是深红色的(4x以上的overdraw)。优化app之后,只有checkbox和图片区域是蓝色(1x)的了,说明至少3层overdraw被消灭掉了!text和空白区域都没有overdraw了。
HierarchyViewer当中的overdraw
另一种查看app当中overdraw的方式是把HierarchyViewer中的view的树形结构保存成photoshop识别的文档(树形view里的第二个选项)。如果你没有安装photoshop,有几个其他的免费软件也可以打开这个文档。打开文档查看view,可以清楚看到不同layer里的overdraw。对于大部分的线上app,在一个白色背景上放上另一个白色背景很常见。听起来还好,但这里其实有一次绘制是多余的,完全可以避免的。我们再看下山羊app,所有overdraw图片区域都放在了一张驴子的背景图片上(替换了之前的白色背景)。之前的驴子看不到,是因为被白色背景图挡住了。移除掉之后就可以看到下面的驴子了,这样我们就可以快速的定位哪里出现了overdraw。用GIMP打开文档之后,app里所有可见的view的左边都有一个小眼睛图标。在图4-16中,可以看到我从最上面开始把view一个个隐藏起来了。在右边的layout视图中,可以看到一些其他的全屏layout(都显示了驴子的图片)。
在图4-17中可以看到另一个逐步隐藏view的办法。从最左边的全屏图片开始,到中间的图片,可以看到我们隐藏了两行山羊的图片展示,每一行下面的出现了一张拉伸的驴子的图片。在这些驴子图片的下面是一张白色的背景图(从最右边的图片可以看出)。再移除这张白色背景可以看到一张大的驴子的图片,在左下角。再往下是另一张白色的全屏背景图。
KitKat里的overdraw
在KitKat或者更新的设备里,overdraw被大幅度的削减了。这项技术叫overdrawavoidance,系统可以检测发现简单的overdraw场景(比如一个view完全盖住了另一个view),然后自动移除额外的绘制,应用到上面的例子,也就是说驴子那张大背景图就不会去绘制了。这很明显会极大的提高设备的绘制性能。但开发者还是要尽可能的避免额外的overdraw(为了更好的性能,也为了能兼容JellyBean及更老的设备)。
当用上面提到的overdraw检测工具时,KitKat的overdrawavoidance功能会被禁止,这只是为了方便你查看view的布局,和在设备上真正运行的情况并不一样。
分析卡顿(测量GPU的渲染性能)
我们快速来看下怎么分析,我比较喜欢在屏幕上直接展示GPU的渲染数据,这样感觉更直观全面(logfile里面的数据很适合离线的详细分析)。我们最好在不同的设备上都试一下。图4-18展示的是Nexus6运行Lollipop(左边)和MotoG运行KitKat(右边)同时跑山羊app的GPU渲染数据。重点看下GPU测量图表底部的水平绿条。它是设备16ms绘制一帧的分割线,如果你有很多帧都超过了这条绿线,那就表示有卡顿了。在下图里可以看到Nexus6上有偶尔的卡顿。出现在滑动到页面底部的时候,播放里一个反弹的动画。用户体验不算太糟。每一次屏幕绘制(竖线)被分成四种颜色来表示额外的测量数据:draw(蓝色),prepare(紫色),process(红色),执行(黄色)。在KitKat和更早的版本里,prepare的数据没有独立出来,包含在其他项里面(因此只有看到3种颜色)。
一般来说,GPUProfiler可以帮你发现问题。在山羊app里,如果我打开Fibonacci延迟(在创建view多时候进行耗时的递归计算),GPUprofiler看不出任何卡顿,因为计算都发生在主线程而且完全阻止了渲染(在低端机上,可能会出现ANR消息)。
Fibonacci算法
Fibonacci序列是这样一组数的集合:每个数字都是它前面两个数字的和。比如0,1,1,2,3,5,8等等。程序里一般用来表示递归,这里我用了最低效的方式来生成Fibonacci序列。
生成这些数字的计算次数呈指数级增长。这样做的目的是在渲染的时候增加CPU的压力,这样渲染事件就无法得到及时处理,出现延迟。计算n=40就把app变得很慢了(低端机上会crash)。这个例子虽然有点牵强,但我们定位卡顿是由Fibonacci产生的过程会很有意义。
AndroidMarshmallow里的GPU渲染
在androidmarshmallow里,运行adbshelldumpsysgfxinfo.可以发现一些检测卡顿的新功能。首先,数据报告开头部分能看到每一帧渲染的信息了。
androidmarshmallow在gfxinfo库里增加了另一个好用的测试工具,adbshelldumpsysgfxinfoframestats。它能够输出每一帧里发生的某些事件耗时,格式是逗号分隔的一张大表。列名没有给出,但在AndroidDeveloper网站里有解释。为了算出渲染里每一步的费时,我们要计算出报告里不同framestats的差异。下面是一些绘制事件:
有时候即使出现了超过16ms的绘制,但由于有vsyncbuffer的存在,也不会出现丢帧。对于没有额外buffer的低端设备,就可能会出现卡顿了。
不只是卡顿(丢帧)
有时候GPUProfile里看不到超过16ms的数据,但你从屏幕上看到明显的卡顿或跳动。出现这种情况可能是由于CPU在做别的事情被堵住了,从而导致里丢帧。在Monitor或者AndroidStudio中,可以查看DDMS里的logfiles。通过过滤log更容易查看app的运行情况。可以重点看下类似下图中的log。
Systrace
Systrace和之前的工具不同的是,它记录的是整个android系统的状态,并不是针对某一个app的。所以最好是用运行app比较少的设备来做检测,这样就不会受到其他app的干扰了。Systrace图标是绿色和粉红色组成的(下图红色的椭圆里)。点击下,会弹出一个带几个选项的窗口。
trace数据记录在一个html文件里,可以用浏览器打开。这里主要研究屏幕的交互数据,主要收集CPU,graphics和view数据(如图4-22所示)。duration留空(默认是5秒)。点击OK之后,Systrace会马上开始采集设备上的数据(最好马上开始操作)。因为采集的数据非常之多,所以最好一次只针对一个问题。
traces里面的数据看着有点吓人(我们只是勾选里4个选项!)。鼠标可以控制滑动,WASD可以用来zoomin/out(W,S)和左右滑动(A,D)。在刚跑的trace数据最上面,能看到CPU的详细数据,CPU数据的下面是几个可折叠的区域,分别表示不同的活跃进程。每一个色条表示系统的一个行为,色条的长度表示该行为的耗时(放大可以看到更多细节)。选中屏幕底部的一个色条,第一眼看到的总览有点吓人,我们一条条分析看下这些数据。
Systrace进化史
就像android生态圈一样,Systrace在不同的系统版本里有不同的界面,展示,和输出结果。
在2015年的googleio大会上,google发布了新版本的Systrace,新版本增加了一些新特性,下面会有更详细的介绍。
SystraceScreenPainting
我们通过图4-24来看下屏幕绘制的步骤。最顶部一行的trace(蓝色高亮)时VSYNC,由一些均匀分布的蓝绿色宽条组成。VSYNC是操作系统发来的信号,表示此时该刷新屏幕了。每个宽条表示16ms(宽条之间的空白也是16ms)。当VSYNC事件发生的时候(在蓝绿色宽条的任意一侧),surfaceflinger(红色高亮方框包含几种颜色的长条)会从viewbuffer(没展示出来)里选一个view,然后绘制到屏幕上。理想情况下,surfaceflinger事件之间相距16ms(没有卡顿),因此如果出现长条空缺则表示surfaceflinger丢掉了一次VSYNC更新事件,屏幕就没有及时的刷新(此时就会有卡顿)。在trace文件2/3的位置可以看到这样的空缺(绿色高亮方框)。
图4-24底部展示的是app的详情。第二行数据(绿色和紫色的线条)表示的app正在创建view,然后是底部的数据(绿色,蓝色,和一些紫色的条状),表示的是RenderThread,view的渲染和发送到buffer(图中没有画出来)都是在这个线程里做的。注意看可以发现大概1/3的位置,这些条状在该区域集中变粗了,表示app此时由于某种原因发生了卡顿。不同app情况不一样,发生卡顿的原因也不同,但是我们可以根据一些共同的现象推测卡顿的发生。
buffer里面有一些view,线条的高度表示了buffer当中view的数量。刚开始,只有一个,当新的view加入到buffer中之后,高度就变成了2倍。
图4-26中,我们看下OS层的行为。我增加了一些箭头来表示16ms的间隔,红色的方框表示surfaceflinger的丢帧。
为什么会出现这种情况?箭头上方的一行是viewbuffer,行的高度表示有多少帧缓存在了buffer里面。trace开始的时候,buffer里缓存的数量是1到2交替出现。surfaceflinger每抓取一个view(buffer里的数量减一),又会马上从app里生成一个新的view来填充。但是当surfaceflinger完成第三个动作之后,buffer被清空了,但是没有从app里及时填充新的view。所以,我们从app层面来检查下这期间发生了什么。
在图4-27中,我们可以看到开始的时候RenderThread发送了一个view到buffer(红色方框)。橘色方框表示app新建了另一个view,渲染,然后交给buffer(droid.yahoo.attmeasure,layout所有的view,RenderThread负责绘制)。不幸的是,app没来得及创建新view就被挂起了(黄色方框内)。为了创建下一个view,droid.yahoo.attapp在运行暗绿色的“performTraversals”(3ms)之前,要先运行“obtainView”7ms,“setupListItem”8.7ms。app然后把数据交给RenderThread,这一步也比较慢(12ms)。创建这一帧总共用了近31ms(上一个只用了6ms)。当创建这一帧开始的时候,buffer里只有一帧的数据,但是设备需要两帧。buffer没有被填满,所以屏幕绘制出现了卡顿。
有意思的是app后面马上就速度追了上来。黄色方框内延迟递交的view创建并交给buffer之后,后续的两帧紧接着创建好了(绿色和蓝色的方框)。通过快速的填充新的帧,app就只丢了一帧。这个trace结果是在Nexus6上运行的(处理器比较快,能快速的跟上)。在三星S4Mini,JellyBean4.2.2上运行同样的结果得到图4-28.
从总览图上可以清晰的看到有很多帧都丢掉了(trace开始的时候surfacelinger部分有很多的空缺)。而且顶部那一行(viewbuffer)里的buffer经常是空的(导致里卡顿),buffer里同时有两个view的情况非常少。对于一个GPU性能比较差的设备来说,app能够像Nexus6一样赶上填满buffer的概率比较小。
小贴示:其实你可以偶尔渲染一帧超过16ms,因为buffer里面一般都有1到2帧准备好的view备用。但是如果超过2-3帧渲染很慢,用户就会感觉到卡顿了。
把这块放大能看到更多的细节(图4-30)。每个垂直的红线表示16ms。从图中可以看出,大概有5,6次SurfaceFlinger错过了红线标记。绿色的“performtraversals”线条都几乎有16ms长(这一步是必须做的,有卡顿)。还有两个蓝绿色的deliverInputEvents(每个都超过了16ms)也阻碍了app的屏幕绘制。
那到底是什么触发了deliverInputEvents呢?这其实是用户在点击屏幕,强制ListView重绘所有的view。这部分影响是CPU,我们接下来简单看下这时候CPU都在干啥。
Systrace和CPU对渲染的影响
如果你频繁的感觉到卡顿,但是在绘制或者surfaceflinger部分看不到什么明显的异常,这时候可以尝试看下CPU在处理什么事情,在Systrace的顶部可以看到这部分的数据。如果你能大概猜到是哪部分的逻辑影响了绘制,可以先把这部分代码注释掉试试。山羊app里有个选项可以开启Fibonacci延迟。打开之后,app在每一行数据渲染的时候都会计算一个很大的Fibonacci值。用膝盖想都知道这时CPU会变得很忙。由于计算是在主线程做的,会妨碍的view的渲染,理所当然就导致里丢帧,滑动也会变的很卡。图4-21里显示的log就能看到这种情况下的丢帧。我们再深挖一点看能不能通过Systrace定位到计算Fibonacci数的代码。
我们再重头看下trace数据,图4-31里是没有优化过的山羊app在Nexus6上跑的数据。
展示做了一些修改,CPU和surfaceflinger之间的一些线被去掉了。这个trace里看不到卡顿,surfaceflingers每16ms的间隔很均匀。RenderThread和每一行view填满buffer的表现也很正常。和CPU那一行数据对比一下,可以发现一个新规律。当RenderThread在绘制layout的时候,CPU1正在运行一个蓝色的任务(注意我们看的是窄一点的CPU1,不是CPU1:C-State)。当山羊app的view正在被measure的时候,CPU0有一个相应的紫色的行为。view的layout和绘制是由两个CPU完成的。注意X轴上的点击是每隔10ms发生的,这里每个行为都没有超过2-4ms。
当我们加入费时的Fibonacci计算之后,Systrace的结果看起来就很不一样了。(图4-32)
Systrace更新-I/O2015
在2015年GoogleI/O大会上,google发布了新版本的systrace,上面提到的分析数据变的更简单了。在图4-27里,我把每一帧的更新都高亮出来了。在新版本的systrace(图4-33)里,每一帧都是由一个带F的小圆圈标示的。正常渲染的帧会有绿点,慢帧则是黄色或者红色。选择一个点,然后按下m就可以高亮某一帧,分析起来更方便。
在systrace里可以看到其它类似的警告,形状像泡泡或是点,屏幕右边的警告面板也列出了这些信息(图4-35)。
这些新功能让Systrace诊断UI问题更加简单了。
第三方工具
每个大的芯片厂商都有自己的GPU评测工具,可以帮助发现更多渲染时遇到瓶颈的信息。这些工具对一些特定的芯片更有针对性,信息也更多。可以帮你针对不同的GPU做更深度的优化。Qualcomm,NVIDIA和Intel都提供了这些开发者工具,有兴趣的可以自己试下。
感知优化
上面的内容都是在讨论怎么通过测试,调试,优化布局来让UI的体验更快。其实还有另外一个办法让你的appUI更快:让用户感觉更快。当然作为开发者要尽可能优化自己的代码,view,overdraw和其它所有可能会影响渲染性能的地方,上面这些都做了之后,再考虑下面这些能让用户觉得你的app更快的方法。
loading菊花:优缺点
瞬时更新的小谎言
如果你的用户在页面上做了更新数据的操作,即使数据还没抵达服务器,可以马上把用户看到的数据更新掉(当然开发者要保证这些数据能100%抵达服务器)。比如说,你在Instagram上点了赞,页面上马上就更新了赞的状态,其实赞的状态甚至可能还没有更新到服务器。Instangram的开发者管这叫“行为最优化”,状态的更新要几秒后才能到服务器并对网站的用户可见(网速不好的时候可能要几分钟),但是更新最后都会成功,等待服务器返回成功其实是没必要的。移动端用户一般都不希望在等待,只要最后能成功就好。
瞬时更新的另一个好处是,用户会感觉你的app在网速或者信号不好(火车经过隧道)的时候也能正常工作。FlipBoard就做过一个离线发送网络请求的框架,可以很方便的应用到更新UI。
另一个优化的小技巧是提前上传。对于像Instagram这种app来说,上传大量的图片会增加主线程的延迟,提前开始上传这些图片会是个好办法。Instagram发现发一个新post是慢在上传图片这一步,所以Instagram就在用户在图片上添加文字的间隙开始上传图片了,图片被真正发布到服务器之前就已经传好了。用户只要一点击Post按钮,就只需要上传文本和创建post的命令了,这样就会让用户感知非常快。Instagram在遇到“是否要添加菊花”这个问题时,他们的答案是通过改变架构的方式永远的杜绝菊花。
提升感知体验的小提示
当app的速度通过优化代码或者view的优化提升之后,你可以用秒表来测试下结果。有些感知是可以用秒表测量的(Instagram的例子),有些则不能(菊花的例子)。当常规的分析或者测量工具不可靠的时候,需要让用户来真正的体验这些优化效果。可以做一些可用性测试,增加测试的范围,A/B测试,这些才能真正的让你确认你的优化是让用户更开心还是更沮丧。