程序员人生 网站导航

Android性能优化典范

栏目:综合技术时间:2015-02-27 08:24:27
新年伊始,Google发布了包括16个短视频的Android性能优化典范专题,详解Android系统中有关性能问题的底层工作原理,并介绍了如何通过工具找出性能问题和提升性能的建议。本文作者对这些问题和建议进行了总结梳理。

2015年伊始,Google发布了关于Android性能优化典范的专题,1共16个短视频,每一个3⑸分钟,帮助开发者创建更快更优秀的Android App。课程专题不单单介绍了Android系统中有关性能问题的底层工作原理,同时也介绍了如何通过工具来找出性能问题和提升性能的建议。主要从3个方面展开,Android的渲染机制,内存与GC,电量优化。下面是对这些问题和建议的总结梳理。


1) Render Performance

大多数用户感知到的卡顿等性能问题的最主要本源都是由于渲染性能。从设计师的角度,他们希望App能够有更多的动画,图片等时尚元夙来实现流畅的用户体验。但是Android系统很有可能没法及时完成那些复杂的界面渲染操作。Android系统每隔16ms发出VSYNC信号,触发对UI进行渲染,如果每次渲染都成功,这样就可以够到达流畅的画面所需要的60fps,为了能够实现60fps,这意味着程序的大多数操作都必须在16ms内完成。


如果你的某个操作花费时间是24ms,系统在得到VSYNC信号的时候就没法进行正常渲染,这样就产生了丢帧现象。那末用户在32ms内看到的会是同1帧画面。


用户容易在UI履行动画或滑动ListView的时候感知到卡顿不流畅,是由于这里的操作相对复杂,容易产生丢帧的现象,从而感觉卡顿。有很多缘由可以致使丢帧,或许是由于你的layout太过复杂,没法在16ms内完成渲染,有多是由于你的UI上有层叠太多的绘制单元,还有多是由于动画履行的次数过量。这些都会致使CPU或GPU负载太重。

我们可以通过1些工具来定位问题,比如可使用HierarchyViewer来查找Activity中的布局是不是过于复杂,也能够使用手机设置里面的开发者选项,打开Show GPU Overdraw等选项进行视察。你还可使用TraceView来视察CPU的履行情况,更加快捷的找到性能瓶颈。

2) Understanding Overdraw

Overdraw(过度绘制)描写的是屏幕上的某个像素在同1帧的时间内被绘制了屡次。在多层次的UI结构里面,如果不可见的UI也在做绘制的操作,这就会致使某些像素区域被绘制了屡次。这就浪费大量的CPU和GPU资源。


当设计上寻求更华丽的视觉效果的时候,我们就容易堕入采取愈来愈多的层叠组件来实现这类视觉效果的怪圈。这很容易致使大量的性能问题,为了取得最好的性能,我们必须尽可能减少Overdraw的情况产生。

荣幸的是,我们可以通过手机设置里面的开发者选项,打开Show GPU Overdraw的选项,可以视察UI上的Overdraw情况。


蓝色,淡绿,淡红,深红代表了4种不同程度的Overdraw情况,我们的目标就是尽可能减少红色Overdraw,看到更多的蓝色区域。

Overdraw有时候是由于你的UI布局存在大量堆叠的部份,还有的时候是由于非必须的堆叠背景。例如某个Activity有1个背景,然后里面的Layout又有自己的背景,同时子View又分别有自己的背景。仅仅是通过移除非必须的背景图片,这就可以够减少大量的红色Overdraw区域,增加蓝色区域的占比。这1措施能够显著提升程序性能。

3) Understanding VSYNC

为了理解App是如何进行渲染的,我们必须了解手机硬件是如何工作,那末就必须理解甚么是VSYNC。

在讲授VSYNC之前,我们需要了解两个相干的概念:

  • Refresh Rate:代表了屏幕在1秒内刷新屏幕的次数,这取决于硬件的固定参数,例如60Hz。
  • Frame Rate:代表了GPU在1秒内绘制操作的帧数,例如30fps,60fps。

GPU会获得图形数据进行渲染,然后硬件负责把渲染后的内容显现到屏幕上,他们二者不停的进行协作。


不幸的是,刷新频率和帧率其实不是总能够保持相同的节奏。如果产生帧率与刷新频率不1致的情况,就会容易出现Tearing的现象(画面上下两部份显示内容产生断裂,来自不同的两帧数据产生堆叠)。


理解图象渲染里面的两重与3重缓存机制,这个概念比较复杂,请移步查看Graphics和《了解Android 4.1之3:黄油项目 ―― 运作机理及新鲜玩意》。


通常来讲,帧率超过刷新频率只是1种理想的状态,在超过60fps(fps――每秒传输帧数(Frames Per Second))的情况下,GPU所产生的帧数据会由于等待VSYNC的刷新信息而被Hold住,这样能够保持每次刷新都有实际的新的数据可以显示。但是我们遇到更多的情况是帧率小于刷新频率。


在这类情况下,某些帧显示的画面内容就会与上1帧的画面相同。糟的事情是,帧率从超过60fps突然掉到60fps以下,这样就会产生LAG,JANK,HITCHING等卡顿掉帧的不顺滑的情况。这也是用户感受不好的缘由所在。

4) Tool:Profile GPU Rendering

性能问题如此的麻烦,幸亏我们可以有工具来进行调试。打开手机里面的开发者选项,选择Profile GPU Rendering,选中On screen as bars的选项。


选择了这样以后,我们可以在手机画面上看到丰富的GPU绘制图形信息,分别关于StatusBar,NavBar,激活的程序Activity区域的GPU Rending信息。


随着界面的刷新,界面上会转动显示垂直的柱状图来表示每帧画面所需要渲染的时间,柱状图越高表示花费的渲染时间越长。


中间有1根绿色的横线,代表16ms,我们需要确保每帧花费的总时间都低于这条横线,这样才能够避免出现卡顿的问题。


每条柱状线都包括3部份,蓝色代表丈量绘制Display List的时间,红色代表OpenGL渲染Display List所需要的时间,黄色代表CPU等待GPU处理的时间。

5) Why 60fps?

我们通常都会提到60fps与16ms,可是知道为什么会是以程序是不是到达60fps来作为App性能的衡量标准吗?这是由于人眼与大脑之间的协作没法感知超过60fps的画面更新。

12fps大概类似手动快速翻动书籍的帧率,这明显是可以感知到不够顺滑的。24fps使得人眼感知的是连续线性的运动,这实际上是归功于运动模糊的效果。24fps是电影胶圈通常使用的帧率,由于这个帧率已足够支持大部份电影画面需要表达的内容,同时能够最大的减少费用支出。但是低于30fps是没法顺畅表现绚丽的画面内容的,此时就需要用到60fps来到达想要的效果,固然超过60fps是没有必要的。

开发app的性能目标就是保持60fps,这意味着每帧你只有16ms=1000/60的时间来处理所有的任务。

6) Android, UI and the GPU

了解Android是如何利用GPU进行画面渲染有助于我们更好的理解性能问题。那末1个最实际的问题是:activity的画面是如何绘制到屏幕上的?那些复杂的XML布局文件又是如何能够被辨认并绘制出来的?


Resterization栅格化是绘制那些Button,Shape,Path,String,Bitmap等组件最基础的操作。它把那些组件拆分到不同的像素上进行显示。这是1个很费时的操作,GPU的引入就是为了加快栅格化的操作。

CPU负责把UI组件计算成Polygons,Texture纹理,然后交给GPU进行栅格化渲染。


但是每次从CPU转移到GPU是1件很麻烦的事情,所幸的是OpenGL ES可以把那些需要渲染的纹理Hold在GPU Memory里面,在下次需要渲染的时候直接进行操作。所以如果你更新了GPU所hold住的纹理内容,那末之前保存的状态就丢失了。

在Android里面那些由主题所提供的资源,例如Bitmaps,Drawables都是1起打包到统1的Texture纹应当中,然后再传递到GPU里面,这意味着每次你需要使用这些资源的时候,都是直接从纹理里面进行获得渲染的。固然随着UI组件的愈来愈丰富,有了更多演化的形态。例如显示图片的时候,需要先经过CPU的计算加载到内存中,然后传递给GPU进行渲染。文字的显示更加复杂,需要先经过CPU换算成纹理,然后再交给GPU进行渲染,回到CPU绘制单个字符的时候,再重新援用经过GPU渲染的内容。动画则是1个更加复杂的操作流程。

为了能够使得App流畅,我们需要在每帧16ms之内处理完所有的CPU与GPU计算,绘制,渲染等等操作。

7) Invalidations, Layouts, and Performance

顺滑精巧的动画是app设计里面最重要的元素之1,这些动画能够显著提升用户体验。下面会讲授Android系统是如何处理UI组件的更新操作的。

通常来讲,Android需要把XML布局文件转换成GPU能够辨认并绘制的对象。这个操作是在DisplayList的帮助下完成的。DisplayList持有所有将要交给GPU绘制到屏幕上的数据信息。

在某个View第1次需要被渲染时,DisplayList会因此而被创建,当这个View要显示到屏幕上时,我们会履行GPU的绘制指令来进行渲染。如果你在后续有履行类似移动这个View的位置等操作而需要再次渲染这个View时,我们就仅仅需要额外操作1次渲染指令就够了。但是如果你修改了View中的某些可见组件,那末之前的DisplayList就没法继续使用了,我们需要回头重新创建1个DisplayList并且重新履行渲染指令并更新到屏幕上。

需要注意的是:任什么时候候View中的绘制内容产生变化时,都会重新履行创建DisplayList,渲染DisplayList,更新到屏幕上等1系列操作。这个流程的表现性能取决于你的View的复杂程度,View的状态变化和渲染管道的履行性能。举个例子,假定某个Button的大小需要增大到目前的两倍,在增大Button大小之前,需要通过父View重新计算并摆放其他子View的位置。修改View的大小会触发全部HierarcyView的重新计算大小的操作。如果是修改View的位置则会触发HierarchView重新计算其他View的位置。如果布局很复杂,这就会很容易致使严重的性能问题。我们需要尽可能减少Overdraw。


我们可以通过前面介绍的Monitor GPU Rendering来查看渲染的表现性能如何,另外也能够通过开发者选项里面的Show GPU view updates来查看视图更新的操作,最后我们还可以通过HierarchyViewer这个工具来查看布局,使得布局尽可能扁平化,移除非必须的UI组件,这些操作能够减少Measure,Layout的计算时间。

8) Overdraw, Cliprect, QuickReject

引发性能问题的1个很重要的方面是由于过量复杂的绘制操作。我们可以通过工具来检测并修复标准UI组件的Overdraw问题,但是针对高度自定义的UI组件则显得有些力不从心。

有1个诀窍是我们可以通过履行几个APIs方法来显著提升绘制操作的性能。前面有提到过,非可见的UI组件进行绘制更新会致使Overdraw。例如Nav Drawer从前置可见的Activity滑出以后,如果还继续绘制那些在Nav Drawer里面不可见的UI组件,这就致使了Overdraw。为了解决这个问题,Android系统会通过避免绘制那些完全不可见的组件来尽可能减少Overdraw。那些Nav Drawer里面不可见的View就不会被履行浪费资源。


但是不幸的是,对那些过于复杂的自定义的View(重写了onDraw方法),Android系统没法检测具体在onDraw里面会履行甚么操作,系统没法监控并自动优化,也就没法避免Overdraw了。但是我们可以通过canvas.clipRect()来帮助系统辨认那些可见的区域。这个方法可以指定1块矩形区域,只有在这个区域内才会被绘制,其他的区域会被忽视。这个API可以很好的帮助那些有多组堆叠组件的自定义View来控制显示的区域。同时clipRect方法还可以帮助节俭CPU与GPU资源,在clipRect区域以外的绘制指令都不会被履行,那些部份内容在矩形区域内的组件,依然会得到绘制。


除clipRect方法以外,我们还可使用canvas.quickreject()来判断是不是没和某个矩形相交,从而跳过那些非矩形区域内的绘制操作。做了那些优化以后,我们可以通过上面介绍的Show GPU Overdraw来查看效果。

9) Memory Churn and performance

虽然Android有自动管理内存的机制,但是对内存的不恰当使用依然容易引发严重的性能问题。在同1帧里面创建过量的对象是件需要特别引发注意的事情。

Android系统里面有1个Generational Heap Memory的模型,系统会根据内存中不同的内存数据类型分别履行不同的GC操作。例如,最近刚分配的对象会放在Young Generation区域,这个区域的对象通常都是会快速被创建并且很快被烧毁回收的,同时这个区域的GC操作速度也是比Old Generation区域的GC操作速度更快的。


除速度差异以外,履行GC操作的时候,任何线程的任何操作都会需要暂停,等待GC操作完成以后,其他操作才能够继续运行。


通常来讲,单个的GC其实不会占用太多时间,但是大量不停的GC操作则会显著占用帧间隔时间(16ms)。如果在帧间隔时间里面做了过量的GC操作,那末自然其他类似计算,渲染等操作的可用时间就变得少了。

致使GC频繁履行有两个缘由:

  • Memory Churn内存抖动,内存抖动是由于大量的对象被创建又在短时间内马上被释放。
  • 瞬间产生大量的对象会严重占用Young Generation的内存区域,当到达阀值,剩余空间不够的时候,也会触发GC。即便每次分配的对象占用了很少的内存,但是他们叠加在1起会增加Heap的压力,从而触发更多其他类型的GC。这个操作有可能会影响到帧率,并使得用户感知到性能问题。


解决上面的问题有简洁直观方法,如果你在Memory Monitor里面查看到短时间产生了屡次内存的涨跌,这意味着很有可能产生了内存抖动。


同时我们还可以通过Allocation Tracker来查看在短时间内,同1个栈中不断进出的相同对象。这是内存抖动的典型信号之1。

当你大致定位问题以后,接下去的问题修复也就显得相对直接简单了。例如,你需要避免在for循环里面分配对象占用内存,需要尝试把对象的创建移到循环体以外,自定义View中的onDraw方法也需要引发注意,每次屏幕产生绘制和动画履行进程中,onDraw方法都会被调用到,避免在onDraw方法里面履行复杂的操作,避免创建对象。对那些没法避免需要创建对象的情况,我们可以斟酌对象池模型,通过对象池来解决频繁创建与烧毁的问题,但是这里需要注意结束使用以后,需要手动释放对象池中的对象。

10) Garbage Collection in Android

JVM的回收机制给开发人员带来很大的好处,不用时刻处理对象的分配与回收,可以更加专注于更加高级的代码实现。相比起Java,C与C++等语言具有更高的履行效力,他们需要开发人员自己关注对象的分配与回收,但是在1个庞大的系统当中,还是免不了常常产生部份对象忘记回收的情况,这就是内存泄漏。

原始JVM中的GC机制在Android中得到了很大程度上的优化。Android里面是1个3级Generation的内存模型,最近分配的对象会寄存在Young Generation区域,当这个对象在这个区域停留的时间到达1定程度,它会被移动到Old Generation,最后到Permanent Generation区域。


每个级别的内存区域都有固定的大小,尔后不断有新的对象被分配到此区域,当这些对象总的大小快到达这1级别内存区域的阀值时,会触发GC的操作,以便腾出空间来寄存其他新的对象。


前面提到过每次GC产生的时候,所有的线程都是暂停状态的。GC所占用的时间和它是哪个Generation也有关系,Young Generation的每次GC操作时间是最短的,Old Generation其次,Permanent Generation最长。履行时间的长短也和当前Generation中的对象数量有关,遍历查找20000个对象比起遍历50个对象自然是要慢很多的。

虽然Google的工程师在尽可能缩短每次GC所花费的时间,但是特别注意GC引发的性能问题还是很有必要。如果不谨慎在最小的for循环单元里面履行了创建对象的操作,这将很容易引发GC并致使性能问题。通过Memory Monitor我们可以查看到内存的占用情况,每次瞬间的内存下降都是由于此时产生了GC操作,如果在短时间内产生大量的内存上涨与下降的事件,这说明很有可能这里有性能问题。我们还可以通过Heap and Allocation Tracker工具来查看此时内存中分配的到底有哪些对象。


11) Performance Cost of Memory Leaks

虽然Java有自动回收的机制,可是这不意味着Java中不存在内存泄漏的问题,而内存泄漏会很容易致使严重的性能问题。

内存泄漏指的是那些程序不再使用的对象没法被GC辨认,这样就致使这个对象1直留在内存当中,占用了宝贵的内存空间。明显,这还使得每级Generation的内存区域可用空间变小,GC就会更容易被触发,从而引发性能问题。

寻觅内存泄漏并修复这个漏洞是件很辣手的事情,你需要对履行的代码很熟习,清楚的知道在特定环境下是如何运行的,然后仔细排查。例如,你想知道程序中的某个activity退出的时候,它之前所占用的内存是不是有完全的释放干净了?首先你需要在activity处于前台的时候使用Heap Tool获得1份当前状态的内存快照,然后你需要创建1个几近不这么占用内存的空白activity用来给前1个Activity进行跳转,其次在跳转到这个空白的activity的时候主动调用System.gc()方法来确保触发1个GC操作。最后,如果前面这个activity的内存都有全部正确释放,那末在空白activity被启动以后的内存快照中应当不会有前面那个activity中的任何对象了。


如果你发现在空白activity的内存快照中有1些可疑的没有被释放的对象存在,那末接下去就应当使用Alocation Track Tool来仔细查找具体的可疑对象。我们可以从空白activity开始监听,启动到视察activity,然后再回到空白activity结束监听。这样操作以后,我们可以仔细视察那些对象,找出内存泄漏的真凶。


12) Memory Performance

通常来讲,Android对GC做了大量的优化操作,虽然履行GC操作的时候会暂停其他任务,可是大多数情况下,GC操作还是相对很安静并且高效的。但是如果我们对内存的使用不恰当,致使GC频繁履行,这样就会引发不小的性能问题。

为了寻觅内存的性能问题,Android Studio提供了工具来帮助开发者。

  • Memory Monitor:查看全部app所占用的内存,和产生GC的时刻,短时间内产生大量的GC操作是1个危险的信号。
  • Allocation Tracker:使用此工具来追踪内存的分配,前面有提到过。
  • Heap Tool:查看当前内存快照,便于对照分析哪些对象有多是泄漏了的,请参考前面的Case。

13) Tool - Memory Monitor

Android Studio中的Memory Monitor可以很好的帮组我们查看程序的内存使用情况。




14) Battery Performance

电量实际上是目前手持装备最宝贵的资源之1,大多数装备都需要不断的充电来保持继续使用。不幸的是,对开发者来讲,电量优化是他们最后才会斟酌的的事情。但是可以肯定的是,千万不能让你的利用成为消耗电量的大户。

Purdue University研究了最受欢迎的1些利用的电量消耗,平均只有30%左右的电量是被程序最核心的方法例如绘制图片,摆放布局等等所使用掉的,剩下的70%左右的电量是被上报数据,检查位置信息,定时检索后台广告信息所使用掉的。如何平衡这二者的电量消耗,就显得非常重要了。

有下面1些措施能够显著减少电量的消耗:

  • 我们应当尽可能减少唤醒屏幕的次数与延续的时间,使用WakeLock来处理唤醒的问题,能够正确履行唤醒操作并根据设定及时关闭操作进入眠眠状态。
  • 某些非必须马上履行的操作,例如上传歌曲,图片处理等,可以等到装备处于充电状态或电量充足的时候才进行。
  • 触发网络要求的操作,每次都会保持无线信号延续1段时间,我们可以把零散的网络要求打包进行1次操作,避免过量的无线信号引发的电量消耗。关于网络要求引发无线信号的电量消耗,还可以参考这里。

我们可以通过手机设置选项找到对应App的电量消耗统计数据。我们还可以通过Battery Historian Tool来查看详细的电量消耗。


如果发现我们的App有电量消耗过量的问题,我们可使用JobScheduler API来对1些任务进行定时处理,例如我们可以把那些任务重的操作等得手机处于充电状态,或是连接到WiFi的时候来处理。 

15) Understanding Battery Drain on Android

电量消耗的计算与统计是1件麻烦而且矛盾的事情,记录电量消耗本身也是1个费电量的事情。唯1可行的方案是使用第3方监测电量的装备,这样才能够获得到真实的电量消耗。

当装备处于待机状态时消耗的电量是极少的,以N5为例,打开飞行模式,可以待机接近1个月。可是点亮屏幕,硬件各个模块就需要开始工作,这会需要消耗很多电量。


使用WakeLock或JobScheduler唤醒装备处理定时的任务以后,1定要及时让装备回到初始状态。每次唤醒无线信号进行数据传递,都会消耗很多电量,它比WiFi等操作更加的耗电。修复电量的消耗是另外1个很大的课题,这里就不展开继续了。

16) Battery Drain and WakeLocks

高效的保存更多的电量与不断促使用户使用你的App来消耗电量,这是矛盾的选择题。不过我们可使用1些更好的办法来平衡二者。

假定你的手机里面装了大量的社交类利用,即便手机处于待机状态,也会常常被这些利用唤醒用来检查同步新的数据信息。Android会不断关闭各种硬件来延长手机的待机时间,首先屏幕会逐步变暗直相当闭,然后CPU进入眠眠,这1切操作都是为了节俭宝贵的电量资源。但是即便在这类睡眠状态下,大多数利用还是会尝试进行工作,他们将不断的唤醒手机。1个最简单的唤醒手机的方法是使用PowerManager.WakeLock的API来保持CPU工作并避免屏幕变暗关闭。这使得手机可以被唤醒,履行工作,然后回到睡眠状态。知道如何获得WakeLock是简单的,可是及时释放WakeLock也是非常重要的,不恰当的使用WakeLock会致使严重毛病。例如网络要求的数据返回时间不肯定,致使本来只需要10s的事情1直等待了1个小时,这样会使得电量白白浪费了。这也是为什么使用带超时参数的wakelock.acquice()方法是很关键的。但是仅仅设置超时其实不足够解决问题,例如设置多长的超时比较适合?甚么时候进行重试等等?

解决上面的问题,正确的方式多是使用非精准定时器。通常情况下,我们会设定1个时间进行某个操作,但是动态修改这个时间或许会更好。例如,如果有另外1个程序需要比你设定的时间晚5分钟唤醒,最好能够等到那个时候,两个任务捆绑1起同时进行,这就是非精肯定时器的核心工作原理。我们可以定制计划的任务,可是系统如果检测到1个更好的时间,它可以推延你的任务,以节省电量消耗。


这正是JobScheduler API所做的事情。它会根据当前的情况与任务,组合出理想的唤醒时间,例如等到正在充电或连接到WiFi的时候,或集中任务1起履行。我们可以通过这个API实现很多免费的调度算法。

从Android 5.0开始发布了Battery History Tool,它可以查看程序被唤醒的频率,又谁唤醒的,延续了多长的时间,这些信息都可以获得到。

请关注程序的电量消耗,用户可以通过手机的设置选项视察到那些耗电量大户,并可能决定卸载他们。所以尽可能减少程序的电量消耗是非常有必要的。

原文出自:http://www.csdn.net/article/2015-01⑵0/2823621-android-performance-patterns/3
------分隔线----------------------------
------分隔线----------------------------

最新技术推荐