4)Why 60fps?


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

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

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

5)Android, UI and the GPU


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

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

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

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

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

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

6)Invalidations, Layouts, and Performance


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

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

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

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

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

7)Overdraw, Cliprect, QuickReject


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

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

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

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

8)Memory Churn and performance


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

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

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

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

导致GC频繁执行有两个原因:

Memory Churn内存抖动,内存抖动是因为大量的对象被创建又在短时间内马上被释放。

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

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

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

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

9)Garbage Collection in Android


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

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

每一个级别的内存区域都有固定的大小,此后不断有新的对象被分配到此区域,当这些对象总的大小快达到这一级别内存区域的阀值时,会触发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工具来查看此时内存中分配的到底有哪些对象。

10)Performance Cost of Memory Leaks


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

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

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

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

11)Memory Performance


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

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

Memory Monitor:查看整个app所占用的内存,以及发生GC的时刻,短时间内发生大量的GC操作是一个危险的信号。

Allocation Tracker:使用此工具来追踪内存的分配,前面有提到过。

Heap Tool:查看当前内存快照,便于对比分析哪些对象有可能是泄漏了的,请参考前面的Case。

12)Tool - Memory Monitor


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

13)Battery Performance


电量其实是目前手持设备最宝贵的资源之一,大多数设备都需要不断的充电来维持继续使用。不幸的是,对于开发者来说,电量优化是他们最后才会考虑的的事情。但是可以确定的是,千万不能让你的应用成为消耗电量的大户。

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

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

我们应该尽量减少唤醒屏幕的次数与持续的时间,使用WakeLock来处理唤醒的问题,能够正确执行唤醒操作并根据设定及时关闭操作进入睡眠状态。

某些非必须马上执行的操作,例如上传歌曲,图片处理等,可以等到设备处于充电状态或者电量充足的时候才进行。

触发网络请求的操作,每次都会保持无线信号持续一段时间,我们可以把零散的网络请求打包进行一次操作,避免过多的无线信号引起的电量消耗。关于网络请求引起无线信号的电量消耗,还可以参考这里:

http://hukai.me/android-training-course-in-chinese/connectivity/efficient-downloads/efficient-network-access.html

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

如果发现我们的App有电量消耗过多的问题,我们可以使用JobScheduler API来对一些任务进行定时处理,例如我们可以把那些任务重的操作等到手机处于充电状态,或者是连接到WiFi的时候来处理。 关于JobScheduler的更多知识可以参考http://hukai.me/android-training-course-in-chinese/background-jobs/scheduling/index.html

14)Understanding Battery Drain on Android


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

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

使用WakeLock或者JobScheduler唤醒设备处理定时的任务之后,一定要及时让设备回到初始状态。每次唤醒无线信号进行数据传递,都会消耗很多电量,它比WiFi等操作更加的耗电,详情请关注

http://hukai.me/android-training-course-in-chinese/connectivity/efficient-downloads/efficient-network-access.html

修复电量的消耗是另外一个很大的课题,这里就不展开继续了。

15)Battery Drain and WakeLocks


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

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

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

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

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

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

Android性能优化典范(二)

================

Google前几天刚发布了Android性能优化典范第2季的课程,一共20个短视频,包括的内容大致有:电量优化,网络优化,Wear上如何做优化,使用对象池来提高效率,LRU Cache,Bitmap的缩放,缓存,重用,PNG压缩,自定义View的性能,提升设置alpha之后View的渲染性能,以及Lint,StictMode等等工具的使用技巧。 下面是对这些课程的总结摘要,认知有限,理解偏差的地方请多多指教!

1)Battery Drain and Networking


对于手机程序,网络操作相对来说是比较耗电的行为。优化网络操作能够显著节约电量的消耗。在性能优化第1季里面有提到过,手机硬件的各个模块的耗电量是不一样的,其中移动蜂窝模块对电量消耗是比较大的,另外蜂窝模块在不同工作强度下,对电量的消耗也是有差异的。当程序想要执行某个网络请求之前,需要先唤醒设备,然后发送数据请求,之后等待返回数据,最后才慢慢进入休眠状态。这个流程如下图所示:

在上面那个流程中,蜂窝模块的电量消耗差异如下图所示:

从图示中可以看到,激活瞬间,发送数据的瞬间,接收数据的瞬间都有很大的电量消耗,所以,我们应该从如何传递网络数据以及何时发起网络请求这两个方面来着手优化。

1.1)何时发起网络请求

首先我们需要区分哪些网络请求是需要及时返回结果的,哪些是可以延迟执行的。例如,用户主动下拉刷新列表,这种行为需要立即触发网络请求,并等待数据返回。但是对于上传用户操作的数据,同步程序设置等等行为则属于可以延迟的行为。我们可以通过Battery Historian这个工具来查看关于移动蜂窝模块的电量消耗(关于这部分的细节,请点击Android性能优化之电量篇)。在Mobile Radio那一行会显示蜂窝模块的电量消耗情况,红色的部分代表模块正在工作,中间的间隔部分代表模块正在休眠状态,如果看到有一段区间,红色与间隔频繁的出现,那就说明这里有可以优化的行为。如下图所示:

对于上面可以优化的部分,我们可以有针对性的把请求行为捆绑起来,延迟到某个时刻统一发起请求。如下图所示:

经过上面的优化之后,我们再回头使用Battery Historian导出电量消耗图,可以看到唤醒状态与休眠状态是连续大块间隔的,这样的话,总体电量的消耗就会变得更少。

当然,我们甚至可以把请求的任务延迟到手机网络切换到WiFi,手机处于充电状态下再执行。在前面的描述过程中,我们会遇到的一个难题是如何把网络请求延迟,并批量进行执行。还好,Android提供了JobScheduler来帮助我们达成这个目标。

1.2)如何传递网络数据

关于这部分主要会涉及到Prefetch(预取)与Compressed(压缩)这两个技术。对于Prefetch的使用,我们需要预先判断用户在此次操作之后,后续零散的请求是否很有可能会马上被触发,可以把后面5分钟有可能会使用到的零散请求都一次集中执行完毕。对于Compressed的使用,在上传与下载数据之前,使用CPU对数据进行压缩与解压,可以很大程度上减少网络传输的时间。

想要知道我们的应用程序中网络请求发生的时间,每次请求的数据量等等信息,可以通过Android Studio中的Networking Traffic Tool来查看详细的数据,如下图所示:

2)Wear & Sensors


在Android Wear上会大量的使用Sensors来实现某些特殊功能,如何在尽量节约电量的前提下利用好Sensor会是我们需要特别注意的问题。下面会介绍一些在Android Wear上的最佳实践典范。

尽量减少刷新请求,例如我们可以在不需要某些数据的时候尽快注销监听,减小刷新频率,对Sensor的数据做批量处理等等。那么如何做到这些优化呢?

首先我们需要尽量使用Android平台提供的既有运动数据,而不是自己去实现监听采集数据,因为大多数Android Watch自身记录Sensor数据的行为是有经过做电量优化的。

其次在Activity不需要监听某些Sensor数据的时候需要尽快释放监听注册。

还有我们需要尽量控制更新的频率,仅仅在需要刷新显示数据的时候才触发获取最新数据的操作。

另外我们可以针对Sensor的数据做批量处理,待数据累积一定次数或者某个程度的时候才更新到UI上。

最后当Watch与Phone连接起来的时候,可以把某些复杂操作的事情交给Phone来执行,Watch只需要等待返回的结果。

更对关于Sensors的知识,可以点击这里

3)Smooth Android Wear Animation


Android Material Design风格的应用采用了大量的动画来进行UI切换,优化动画的性能不仅能够提升用户体验还可以减少电量的消耗,下面会介绍一些简单易行的方法。

在Android里面一个相对操作比较繁重的事情是对Bitmap进行旋转,缩放,裁剪等等。例如在一个圆形的钟表图上,我们把时钟的指针抠出来当做单独的图片进行旋转会比旋转一张完整的圆形图的所形成的帧率要高56%。

另外尽量减少每次重绘的元素可以极大的提升性能,假如某个钟表界面上有很多需要显示的复杂组件,我们可以把这些组件做拆分处理,例如把背景图片单独拎出来设置为一个独立的View,通过setLayerType()方法使得这个View强制用Hardware来进行渲染。至于界面上哪些元素需要做拆分,他们各自的更新频率是多少,需要有针对性的单独讨论。

如何使用Systrace等工具来查看某些View的渲染性能,在前面的章节里面有提到过,感兴趣的可以点击这里

对于大多数应用中的动画,我们会使用PropertyAnimation或者ViewAnimation来操作实现,Android系统会自动对这些Animation做一定的优化处理,在Android上面学习到的大多数性能优化的知识同样也适用于Android Wear。

想要获取更多关于Android Wear中动画效果的优化,请点击WatchFace这个范例。

4)Android Wear Data Batching


在Android Training里面有关于Wear上面如何利用Wearable API与Phone进行沟通协作的课程(详情请点击这里)。因为Phone的CPU与电量都比Wear要强大,另外Phone还可以直接接入网络,而We
ar要接入网络则相对更加困难,所以我们在开发Wear应用的时候需要尽量做到把复杂的操作交给Phone来执行。例如我们可以让Phone来获取天气信息,然后把数据返回Wear进行显示。更进一步,在之前的性能优化课程里面我们有学习过如何使用JobScheduler来延迟批量处理任务,假设Phone收到来自Wear的其中一个任务是每隔5分钟检查一次天气情况,那么Phone使用JobScheduler执行检查天气任务之后,先判断这次返回的结果和之前是否有差异,仅仅当天气发生变化的时候,才有必要把结果通知到Wear,或者仅仅把变化的某一项数据通知给Wear,这样可以更大程度上减少Wear的电量消耗。

下面我们总结一下如何优化Wear的性能与电量:

  • 仅仅在真正需要刷新界面的时候才发出请求

  • 尽量把计算复杂操作的任务交给Phone来处理

  • Phone仅仅在数据发生变化的时候才通知到Wear

  • 把零碎的数据请求捆绑一起再进行操作

5)Object Pools


在程序里面经常会遇到的一个问题是短时间内创建大量的对象,导致内存紧张,从而触发GC导致性能问题。对于这个问题,我们可以使用对象池技术来解决它。通常对象池中的对象可能是bitmaps,views,paints等等。关于对象池的操作原理,不展开述说了,请看下面的图示:

使用对象池技术有很多好处,它可以避免内存抖动,提升性能,但是在使用的时候有一些内容是需要特别注意的。通常情况下,初始化的对象池里面都是空白的,当使用某个对象的时候先去对象池查询是否存在,如果不存在则创建这个对象然后加入对象池,但是我们也可以在程序刚启动的时候就事先为对象池填充一些即将要使用到的数据,这样可以在需要使用到这些对象的时候提供更快的首次加载速度,这种行为就叫做预分配。使用对象池也有不好的一面,程序员需要手动管理这些对象的分配与释放,所以我们需要慎重地使用这项技术,避免发生对象的内存泄漏。为了确保所有的对象能够正确被释放,我们需要保证加入对象池的对象和其他外部对象没有互相引用的关系。

6)To Index or Iterate?


遍历容器是编程里面一个经常遇到的场景。在Java语言中,使用Iterate是一个比较常见的方法。可是在Android开发团队中,大家却尽量避免使用Iterator来执行遍历操作。下面我们看下在Android上可能用到的三种不同的遍历方法:

使用上面三种方式在同一台手机上,使用相同的数据集做测试,他们的表现性能如下所示:

从上面可以看到for index的方式有更好的效率,但是因为不同平台编译器优化各有差异,我们最好还是针对实际的方法做一下简单的测量比较好,拿到数据之后,再选择效率最高的那个方式。

7)The Magic of LRU Cache


这小节我们要讨论的是缓存算法,在Android上面最常用的一个缓存算法是LRU(Least Recently Use),关于LRU算法,不展开述说,用下面一张图演示下含义:

LRU Cache的基础构建用法如下:

为了给LRU Cache设置一个比较合理的缓存大小值,我们通常是用下面的方法来做界定的:

使用LRU Cache时为了能够让Cache知道每个加入的Item的具体大小,我们需要Override下面的方法:

使用LRU Cache能够显著提升应用的性能,可是也需要注意LRU Cache中被淘汰对象的回收,否者会引起严重的内存泄露。

8)Using LINT for Performance Tips


Lint是Android提供的一个静态扫描应用源码并找出其中的潜在问题的一个强大的工具。

例如,如果我们在onDraw方法里面执行了new对象的操作,Lint就会提示我们这里有性能问题,并提出对应的建议方案。Lint已经集成到Android Studio中了,我们可以手动去触发这个工具,点击工具栏的Analysis -> Inspect Code,触发之后,Lint会开始工作,并把结果输出到底部的工具栏,我们可以逐个查看原因并根据指示做相应的优化修改。

Lint的功能非常强大,他能够扫描各种问题。当然我们可以通过Android Studio设置找到Lint,对Lint做一些定制化扫描的设置,可以选择忽略掉那些不想Lint去扫描的选项,我们还可以针对部分扫描内容修改它的提示优先级。

建议把与内存有关的选项中的严重程度标记为红色的Error,对于Layout的性能问题标记为黄色Warning。

9)Hidden Cost of Transparency


这小节会介绍如何减少透明区域对性能的影响。通常来说,对于不透明的View,显示它只需要渲染一次即可,可是如果这个View设置了alpha值,会至少需要渲染两次。原因是包含alpha的view需要事先知道混合View的下一层元素是什么,然后再结合上层的View进行Blend混色处理。

在某些情况下,一个包含alpha的View有可能会触发改View在HierarchyView上的父View都被额外重绘一次。下面我们看一个例子,下图演示的ListView中的图片与二级标题都有设置透明度。

大多数情况下,屏幕上的元素都是由后向前进行渲染的。在上面的图示中,会先渲染背景图(蓝,绿,红),然后渲染人物头像图。如果后渲染的元素有设置alpha值,那么这个元素就会和屏幕上已经渲染好的元素做blend处理。很多时候,我们会给整个View设置alpha的来达到fading的动画效果,如果我们图示中的ListView做alpha逐渐减小的处理,我们可以看到ListView上的TextView等等组件会逐渐融合到背景色上。但是在这个过程中,我们无法观察到它其实已经触发了额外的绘制任务,我们的目标是让整个View逐渐透明,可是期间ListView在不停的做Blending的操作,这样会导致不少性能问题。

如何渲染才能够得到我们想要的效果呢?我们可以先按照通常的方式把View上的元素按照从后到前的方式绘制出来,但是不直接显示到屏幕上,而是使用GPU预处理之后,再又GPU渲染到屏幕上,GPU可以对界面上的原始数据直接做旋转,设置透明度等等操作。使用GPU进行渲染,虽然第一次操作相比起直接绘制到屏幕上更加耗时,可是一旦原始纹理数据生成之后,接下去的操作就比较省时省力。

如何才能够让GPU来渲染某个View呢?我们可以通过setLayerType的方法来指定View应该如何进行渲染,从SDK 16开始,我们还可以使用ViewPropertyAnimator.alpha().withLayer()来指定。如下图所示:

另外一个例子是包含阴影区域的View,这种类型的View并不会出现我们前面提到的问题,因为他们并不存在层叠的关系。

为了能够让渲染器知道这种情况,避免为这种View占用额外的GPU内存空间,我们可以做下面的设置。

通过上面的设置以后,性能可以得到显著的提升,如下图所示:

10)Avoiding Allocations in onDraw()


我们都知道应该避免在onDraw()方法里面执行导致内存分配的操作,下面讲解下为何需要这样做。

首先onDraw()方法是执行在UI线程的,在UI线程尽量避免做任何可能影响到性能的操作。虽然分配内存的操作并不需要花费太多系统资源,但是这并不意味着是免费无代价的。设备有一定的刷新频率,导致View的onDraw方法会被频繁的调用,如果onDraw方法效率低下,在频繁刷新累积的效应下,效率低的问题会被扩大,然后会对性能有严重的影响。

如果在onDraw里面执行内存分配的操作,会容易导致内存抖动,GC频繁被触发,虽然GC后来被改进为执行在另外一个后台线程(GC操作在2.3以前是同步的,之后是并发),可是频繁的GC的操作还是会影响到CPU,影响到电量的消耗。

那么简单解决频繁分配内存的方法就是把分配操作移动到onDraw()方法外面,通常情况下,我们会把onDraw()里面new Paint的操作移动到外面,如下面所示:

11)Tool: Strict Mode


UI线程被阻塞超过5秒,就会出现ANR,这太糟糕了。防止程序出现ANR是很重要的事情,那么如何找出程序里面潜在的坑,预防ANR呢?很多大部分情况下执行很快的方法,但是他们有可能存在巨大的隐患,这些隐患的爆发就很容易导致ANR。

Android提供了一个叫做Strict Mode的工具,我们可以通过手机设置里面的开发者选项,打开Strict Mode选项,如果程序存在潜在的隐患,屏幕就会闪现红色。我们也可以通过StrictMode API在代码层面做细化的跟踪,可以设置StrictMode监听那些潜在问题,出现问题时如何提醒开发者,可以对屏幕闪红色,也可以输出错误日志。下面是官方的代码示例:

public void onCreate() {

if (DEVELOPER_MODE) {

StrictMode.setThreadPolicy(new StrictMode.ThreadPolicy.Builder()

.detectDiskReads()

.detectDiskWrites()

.detectNetwork() // or .detectAll() for all detectable problems

.penaltyLog()

.build());

StrictMode.setVmPolicy(new StrictMode.VmPolicy.Builder()

.detectLeakedSqlLiteObjects()

.detectLeakedClosableObjects()

.penaltyLog()

.penaltyDeath()

.build());

}

super.onCreate();

}

12)Custom Views and Performance


Android系统有提供超过70多种标准的View,例如TextView,ImageView,Button等等。在某些时候,这些标准的View无法满足我们的需要,那么就需要我们自己来实现一个View,这节会介绍如何优化自定义View的性能。

通常来说,针对自定义View,我们可能犯下面三个错误:

  • Useless calls to onDraw():我们知道调用View.invalidate()会触发View的重绘,有两个原则需要遵守,第1个是仅仅在View的内容发生改变的时候才去触发invalidate方法,第2个是尽量使用ClipRect等方法来提高绘制的性能。

  • Useless pixels:减少绘制时不必要的绘制元素,对于那些不可见的元素,我们需要尽量避免重绘。

  • Wasted CPU cycles:对于不在屏幕上的元素,可以使用Canvas.quickReject把他们给剔除,避免浪费CPU资源。另外尽量使用GPU来进行UI的渲染,这样能够极大的提高程序的整体表现性能。

最后请时刻牢记,尽量提高View的绘制性能,这样才能保证界面的刷新帧率尽量的高。更多关于这部分的内容,可以看这里

13)Batching Background Work Until Later


优化性能时大多数时候讨论的都是如何减少不必要的操作,但是选择何时去执行某些操作同样也很重要。在第1季以及上一期的性能优化之电量篇里面,我们有提到过移动蜂窝模块的电量消耗模型。为了避免我们的应用程序过多的频繁消耗电量,我们需要学习如何把后台任务打包批量,并选择一个合适的时机进行触发执行。下图是每个应用程序各自执行后台任务导致的电量消耗示意图:

因为像上面那样做会导致浪费很多电量,我们需要做的是把部分应用的任务延迟处理,等到一定时机,这些任务一并进行处理。结果如下面的示意图:

执行延迟任务,通常有下面三种方式:

1)AlarmManager

使用AlarmManager设置定时任务,可以选择精确的间隔时间,也可以选择非精确时间作为参数。除非程序有很强烈的需要使用精确的定时唤醒,否者一定要避免使用他,我们应该尽量使用非精确的方式。

2)SyncAdapter

我们可以使用SyncAdapter为应用添加设置账户,这样在手机设置的账户列表里面可以找到我们的应用。这种方式功能更多,但是实现起来比较复杂。我们可以从这里看到官方的培训课程:http://developer.android.com/training/sync-adapters/index.html

3)JobSchedulor

这是最简单高效的方法,我们可以设置任务延迟的间隔,执行条件,还可以增加重试机制。

14)Smaller Pixel Formats


常见的png,jpeg,webp等格式的图片在设置到UI上之前需要经过解码的过程,而解压时可以选择不同的解码率,不同的解码率对内存的占用是有很大差别的。在不影响到画质的前提下尽量减少内存的占用,这能够显著提升应用程序的性能。

Android的Heap空间是不会自动做兼容压缩的,意思就是如果Heap空间中的图片被收回之后,这块区域并不会和其他已经回收过的区域做重新排序合并处理,那么当一个更大的图片需要放到heap之前,很可能找不到那么大的连续空闲区域,那么就会触发GC,使得heap腾出一块足以放下这张图片的空闲区域,如果无法腾出,就会发生OOM。如下图所示:

所以为了避免加载一张超大的图片,需要尽量减少这张图片所占用的内存大小,Android为图片提供了4种解码格式,他们分别占用的内存大小如下图所示:

随着解码占用内存大小的降低,清晰度也会有损失。我们需要针对不同的应用场景做不同的处理,大图和小图可以采用不同的解码率。在Android里面可以通过下面的代码来设置解码率:

15)Smaller PNG Files


尽量减少PNG图片的大小是Android里面很重要的一条规范。相比起JPEG,PNG能够提供更加清晰无损的图片,但是PNG格式的图片会更大,占用更多的磁盘空间。到底是使用PNG还是JPEG,需要设计师仔细衡量,对于那些使用JPEG就可以达到视觉效果的,可以考虑采用JPEG即可。我们可以通过Google搜索到很多关于PNG压缩的工具,如下图所示:

常见的png,jpeg,webp等格式的图片在设置到UI上之前需要经过解码的过程,而解压时可以选择不同的解码率,不同的解码率对内存的占用是有很大差别的。在不影响到画质的前提下尽量减少内存的占用,这能够显著提升应用程序的性能。

Android的Heap空间是不会自动做兼容压缩的,意思就是如果Heap空间中的图片被收回之后,这块区域并不会和其他已经回收过的区域做重新排序合并处理,那么当一个更大的图片需要放到heap之前,很可能找不到那么大的连续空闲区域,那么就会触发GC,使得heap腾出一块足以放下这张图片的空闲区域,如果无法腾出,就会发生OOM。如下图所示:

所以为了避免加载一张超大的图片,需要尽量减少这张图片所占用的内存大小,Android为图片提供了4种解码格式,他们分别占用的内存大小如下图所示:

随着解码占用内存大小的降低,清晰度也会有损失。我们需要针对不同的应用场景做不同的处理,大图和小图可以采用不同的解码率。在Android里面可以通过下面的代码来设置解码率:

15)Smaller PNG Files


尽量减少PNG图片的大小是Android里面很重要的一条规范。相比起JPEG,PNG能够提供更加清晰无损的图片,但是PNG格式的图片会更大,占用更多的磁盘空间。到底是使用PNG还是JPEG,需要设计师仔细衡量,对于那些使用JPEG就可以达到视觉效果的,可以考虑采用JPEG即可。我们可以通过Google搜索到很多关于PNG压缩的工具,如下图所示:

Android 性能优化探究,不愧是Alibaba技术官相关推荐

  1. Android 性能优化探究

    使用ViewStub动态载入布局.避免一些不常常的视图长期握住引用: ViewStub的一些特点: 1. ViewStub仅仅能Inflate一次,之后ViewStub对象被置空:某个被ViewStu ...

  2. 不愧是Alibaba技术官,java数组实现单向链表

    缓存雪崩 缓存雪崩指的是Redis当中的大量缓存在同一时间全部失效,而假如恰巧这一段时间同时又有大量请求被发起,那么就会造成请求直接访问到数据库,可能会把数据库冲垮. 缓存雪崩一般形容的是缓存中没有而 ...

  3. 解锁Android性能优化的五大误区!面试必问

    安卓开发大军浩浩荡荡,经过近十年的发展,Android技术优化日异月新,如今Android 11.0 已经发布,Android系统性能也已经非常流畅,可以在体验上完全媲美iOS. 但是,到了各大厂商手 ...

  4. Android性能优化:启动优化深入解析,助你全面掌握启动优化知识点

    一.启动优化概念 1.1.为什么要做启动优化? APP优化是我们进阶高级开发工程师的必经之路,而APP启动速度的优化,也是我们开启APP优化的第一步.用户在使用我们的软件时,交互最多最频繁的也就是AP ...

  5. Android 性能优化

    为什么80%的码农都做不了架构师?>>>    原文作者:鸿洋 原文地址:点我跳转原文 一般情况下,我们谈性能优化基本上会从以下几个方面: App启动速度优化 UI流畅度优化 内存优 ...

  6. Android性能优化——腾讯、字节、阿里、百度、网易等互联网公司项目实战+案例分析(附PDF)

    前言 当我们还在用按键.滑盖.翻盖手机的时候,全触屏手机来了; 当我们觉得二维码这项发明没有意义的时候,支付宝和微信等狠狠地给了我们响亮的耳光; 当我们以为扫码支付只有支付宝的时候,微信支付来了; 当 ...

  7. Android性能优化典范第二季

    原文链接:http://hukai.me/android-performance-patterns-season-2/ 1)Battery Drain and Networking 对于手机程序,网络 ...

  8. Android性能优化之渲染篇(一)

    前言 工作有半年多了,自己的技术没有很大的长进,平时也没有注意学习,只是完成了工作任务就可以了,这样下去的话,自己将很难有提高.面对现在激烈的竞争环境以及技术不断的更新,自己真的要去学习,不断的提高自 ...

  9. Android性能优化:手把手教你如何让App更快、更稳、更省(含内存、布局优化等)...

    2019独角兽企业重金招聘Python工程师标准>>> 前言 在 Android开发中,性能优化策略十分重要 因为其决定了应用程序的开发质量:可用性.流畅性.稳定性等,是提高用户留存 ...

最新文章

  1. 【leetcode】力扣刷题(3):无重复字符的最长子串(go语言)
  2. Java面试通关要点汇总集之核心篇参考答案
  3. OC基础--OC内存管理原则和简单实例
  4. 二级c语言努力学可以过吗,考过计算机二级C语言一些感想和建议
  5. 缓存与库先删哪个(转自网络,侵删)
  6. 如何设计一个端计算架构?
  7. sql索引从入门到精通(十亿行数据测试报告)
  8. MybatisPlus添加操作
  9. Spring WebApplicationInitializer和ApplicationContextInitializer的混淆
  10. Mongo DB 简单搭建和部署
  11. kangle服务器搭建java_linux下kangle虚拟主机-架设java空间的教程及心得
  12. Dream City(01背包+小贪心)acm寒假集训日记21/12/30
  13. MFC实现最小化到托盘
  14. Java好还是Python好?一张图告诉你!
  15. 图片不存放文件夹,直接存在数据库中,
  16. 软考-中级软件设计师电子资料合集
  17. 求素数的c语言表达式,使用c语言判断100以内素数的示例(c语言求素数)
  18. 深度学习阅读导航 | 02 Faster R-CNN:基于RPN的实时目标检测
  19. Sprint周期项目开发总结
  20. 在idea中一键部署项目到Docker及CA认证

热门文章

  1. POJ1251丛林之路题解
  2. java电话正则屏蔽_java针对电话号码正则匹配实例
  3. 2012服务器系统自动重启,Windows server 2012r2 自动重启
  4. 实时音视频技术(WebRTC/voip/Linphone/P2P)
  5. 市面上有哪些免费堡垒机品牌?好用吗?
  6. java编写水数仙花,【360高手联盟-小仙】【JAVASE编程基础】02-JAVA程序的基本结构和基础语法...
  7. 安装office未能启动服务器,关于 office 2013 无法安装的解决方法
  8. 我的世界服务器背景音乐修改,我的世界怎么自定义背景音乐教程攻略
  9. 最大化最小值和最小化最大值
  10. 中小企业比08年还难 浙江企业扎堆倒闭