本文介绍一些Android的性能优化技巧
绘制
分析工具
开发者选项-过度绘制
过度绘制描述的是屏幕上的某个像素在同一帧的时间内被绘制了多次。在多层次的UI结构里面,如果不可见的UI也在做绘制的操作,这就会导致某些像素区域被绘制了多次。这就浪费大量的CPU以及GPU资源。
开发者选项-GPU渲染
我们需要确保每一帧花费的总时间都低于这条横线(16ms),这样才能够避免出现卡顿的问题。
HierarchyViewer
调整布局,去除过深的布局
优化方案
优化过度绘制
- 移除
Window
默认的背景 - 移除XML布局文件中非必需的背景
- 按需显示占位背景图片
使用API优化
Android系统会通过避免绘制那些完全不可见的组件来尽量减少重绘,但对自定义View无效,因为他们重写了onDraw()
可以通过Canvas.clipRect()
来帮助系统识别那些可见的区域。这个方法可以指定一块矩形区域,只有在这个区域内才会被绘制,其他的区域会被忽视。这个API可以很好的帮助那些有多组重叠组件的自定义View来控制显示的区域。同时还可以帮助节约CPU与GPU资源,区域之外的绘制指令都不会被执行,那些部分内容在矩形区域内的组件,仍然会得到绘制。
还可以使用Canvas.quickReject()
来判断是否没和某个矩形相交,从而跳过那些非矩形区域内的绘制操作。
减少绘制区域
在Android里面一个相对操作比较繁重的事情是对Bitmap进行旋转,缩放,裁剪等等。例如在一个圆形的钟表图上,我们把时钟的指针抠出来当做单独的图片进行旋转会比旋转一张完整的圆形图的所形成的帧率要高56%。
尽量减少每次重绘的元素可以极大的提升性能,假如某个钟表界面上有很多需要显示的复杂组件,我们可以把这些组件做拆分处理,例如把背景图片单独拎出来设置为一个独立的View,通过setLayerType()
方法使得这个View强制用硬件来进行渲染。至于界面上哪些元素需要做拆分,他们各自的更新频率是多少,需要有针对性的单独讨论。
参考文章
内存
分析工具
Memory Monitor
查看整个应用所占用的内存,以及发生GC的时刻,短时间内发生大量的GC操作是一个危险的信号
Allocation Tracker
使用此工具来追踪内存的分配
Heap Tool
查看当前内存快照,便于对比分析哪些对象有可能是泄漏了的
优化方案
对象池
在程序里面经常会遇到的一个问题是短时间内创建大量的对象,导致内存紧张,从而触发GC导致性能问题。对于这个问题,我们可以使用对象池技术来解决它。通常对象池中的对象可能是Bitmap,View,Paint等等。
使用对象池技术有很多好处,它可以避免内存抖动,提升性能,但是在使用的时候有一些内容是需要特别注意的。通常情况下,初始化的对象池里面都是空白的,当使用某个对象的时候先去对象池查询是否存在,如果不存在则创建这个对象然后加入对象池,但是我们也可以在程序刚启动的时候就事先为对象池填充一些即将要使用到的数据,这样可以在需要使用到这些对象的时候提供更快的首次加载速度,这种行为就叫做预分配。使用对象池也有不好的一面,程序员需要手动管理这些对象的分配与释放,所以我们需要慎重地使用这项技术,避免发生对象的内存泄漏。为了确保所有的对象能够正确被释放,我们需要保证加入对象池的对象和其他外部对象没有互相引用的关系。
图片的对象池应用
Android在解码图片的时候引进了inBitmap
属性,使用inBitmap
属性可以告知Bitmap解码器去尝试使用已经存在的内存区域,新解码的Bitmap会尝试去使用之前那张Bitmap在Heap中所占据的内存区域,而不是去问内存重新申请一块区域来存放Bitmap。利用这种特性,即使是上千张的图片,也只会仅仅只需要占用屏幕所能够显示的图片数量的内存大小。
用inBitmap
需要注意几个限制条件:
- 在SDK 11 -> 18之间,重用的Bitmap大小必须是一致的,例如给
inBitmap
赋值的图片大小为100-100,那么新申请的Bitmap必须也为100-100才能够被重用。从SDK 19开始,新申请的Bitmap大小必须小于或者等于已经赋值过的Bitmap大小。
新申请的bitmap与旧的bitmap必须有相同的解码格式,例如大家都是8888的,如
我们可以创建一个包含多种典型可重用Bitmap的对象池,这样后续的Bitmap创建都能够找到合适的“模板”去进行重用。
电量
优化措施
- 我们应该尽量减少唤醒屏幕的次数与持续的时间,使用
WakeLock
来处理唤醒的问题,能够正确执行唤醒操作并根据设定及时关闭操作进入睡眠状态。 - 某些非必须马上执行的操作,例如上传歌曲,图片处理等,可以等到设备处于充电状态或者电量充足的时候才进行。
- 触发网络请求的操作,每次都会保持无线信号持续一段时间,我们可以把零散的网络请求打包进行一次操作,避免过多的无线信号引起的电量消耗。关于网络请求引起无线信号的电量消耗,参考优化下载以高效访问网络
- 如果发现我们的App有电量消耗过多的问题,我们可以使用
JobScheduler API
来对一些任务进行定时处理,例如我们可以把那些任务重的操作等到手机处于充电状态,或者是连接到WiFi的时候来处理,参见管理设备的唤醒状态
Android 5.0开始发布了Battery History Tool
,它可以查看程序被唤醒的频率,又谁唤醒的,持续了多长的时间,这些信息都可以获取到。
Networking Traffic Tool
Android DDMS包含了一个查看网络使用详情的栏目来允许跟踪App 的网络请求。使用这个工具,可以监测App 是在何时,如何传输数据的,从而进行代码的优化。
编码技巧
遍历
数据显示for index
方式在Android上有着更好的效率,但是因为不同平台编译器优化各有差异,我们最好还是针对实际的方法做一下简单的测量比较好,拿到数据之后,再选择效率最高的那个方式。
LRU缓存
使用LRU Cache
能够显著提升应用的性能,可是也需要注意LRU Cache中被淘汰对象的回收,否者会引起严重的内存泄露。
透明区域的性能影响
通常来说,对于不透明的View,显示它只需要渲染一次即可,可是如果这个View设置了alpha
值,会至少需要渲染两次。
大多数情况下,屏幕上的元素都是由后向前进行渲染的。如果后渲染的元素有设置alpha
值,那么这个元素就会和屏幕上已经渲染好的元素做混色处理。很多时候,我们会给整个View设置alpha的来达到淡出的动画效果,如果我们dui做alpha逐渐减小的处理,我们可以看到列表上的组件会逐渐融合到背景色上。但是在这个过程中,我们无法观察到它其实已经触发了额外的绘制任务,我们的目标是让整个View逐渐透明,可是期间ListView在不停的做混色操作,这样会导致不少性能问题。
如何渲染才能够得到我们想要的效果呢?我们可以先按照通常的方式把View上的元素按照从后到前的方式绘制出来,但是不直接显示到屏幕上,而是使用GPU预处理之后,再又GPU渲染到屏幕上,GPU可以对界面上的原始数据直接做旋转,设置透明度等等操作。使用GPU进行渲染,虽然第一次操作相比起直接绘制到屏幕上更加耗时,可是一旦原始纹理数据生成之后,接下去的操作就比较省时省力。
如何才能够让GPU来渲染某个View呢?我们可以通过setLayerType()
的方法来指定View应该如何进行渲染,从SDK 16开始,我们还可以使用ViewPropertyAnimator.alpha().withLayer()
来指定。
优化自定义View
- 我们知道调用
View.invalidate()
会触发View的重绘,有两个原则需要遵守,第一是仅仅在View的内容发生改变的时候才去触发invalidate方法,第二是尽量使用clipRect
等方法来提高绘制的性能。 - 减少绘制时不必要的绘制元素,对于那些不可见的元素,我们需要尽量避免重绘。
- 对于不在屏幕上的元素,可以使用
Canvas.quickReject
把他们给剔除,避免浪费CPU资源。另外尽量使用GPU来进行UI的渲染,这样能够极大的提高程序的整体表现性能。
图像格式
常见格式的图片在设置到UI上之前需要经过解码的过程,而解压时可以选择不同的解码率,不同的解码率对内存的占用是有很大差别的。在不影响到画质的前提下尽量减少内存的占用,这能够显著提升应用程序的性能。
Android的Heap空间是不会自动做兼容压缩的,意思就是如果Heap空间中的图片被收回之后,这块区域并不会和其他已经回收过的区域做重新排序合并处理,那么当一个更大的图片需要放到Heap之前,很可能找不到那么大的连续空闲区域,那么就会触发GC,使得Heap腾出一块足以放下这张图片的空闲区域,如果无法腾出,就会发生OOM。
所以为了避免加载一张超大的图片,需要尽量减少这张图片所占用的内存大小,Android为图片提供了4种解码格式.随着解码占用内存大小的降低,清晰度也会有损失。我们需要针对不同的应用场景做不同的处理,大图和小图可以采用不同的解码率。
位图缩放
对Bitmap做缩放,这也是Android里面经常遇到的问题。对Bitmap做缩放的意义很明显,提示显示性能,避免分配不必要的内存。Android提供了现成的Bitmap缩放的API,叫做createScaledBitmap()
,使用这个方法可以获取到一张经过缩放的图片,可是这个方法能够执行的前提是,原图片需要事先加载到内存中,如果原图片过大,很可能导致OOM。
可以使用inSampleSize
,能够等比的缩放显示图片,同时还避免了需要先把原图加载进内存的缺点。另外,我们还可以使用inScaled
,inDensity
,inTargetDensity
的属性来对解码图片做处理,还有一个经常使用到的技巧是inJustDecodeBounds
,使用这个属性去尝试解码图片,可以事先获取到图片的大小而不至于占用什么内存。
性能优化工作流
大多数开发者在没有发现严重性能问题之前是不会特别花精力去关注性能优化的,通常大家关注的都是功能是否实现。当性能问题真的出现的时候,请不要慌乱。我们通常采用下面三个步骤来解决性能问题。
收集数据
我们可以通过Android SDK里面提供的诸多工具来收集CPU,GPU,内存,电量等等性能数据。
分析数据
通过上面的步骤,我们获取到了大量的数据,下一步就是分析这些数据。工具帮我们生成了很多可读性强的表格,我们需要事先了解如何查看表格的数据,每一项代表的含义,这样才能够快速定位问题。如果分析数据之后还是没有找到问题,那么就只能不停的重新收集数据,再进行分析,如此循环。
解决问题
定位到问题之后,我们需要采取行动来解决问题。解决问题之前一定要先有个计划,评估这个解决方案是否可行,是否能够及时的解决问题。
容器
HashMap
HashMap内部是使用一个默认容量为16的数组来存储数据,而数组中每一个元素却又是一个链表的头结点,所以,更准确的来说,HashMap内部存储结构是使用哈希表的拉链结构(数组+链表),这种存储数据的方法叫做拉链法。
每一个结点都是Entry类型,存储的内容有Key、Value、Hash值、和下一个Entry,通过计算元素Key的Hash值,然后对HashMap中数组长度取余得到该元素存储的位置,如果有多个元素Key的Hash值相同的话,后一个元素并不会覆盖上一个元素,而是采取链表的方式,把之后加进来的元素加入链表末尾,从而解决了Hash冲突的问题,由此我们知道HashMap中处理Hash冲突的方法是链地址法。
我们知道HashMap中默认的存储大小就是一个容量为16的数组,所以当我们创建出一个HashMap对象时,即使里面没有任何元素,也要分别一块内存空间给它,而且,我们再不断的向HashMap里添加数据时,当达到一定的容量限制时(满足这样的一个关系时候将会扩容:HashMap中的数据量>容量*加载因子,而HashMap中默认的加载因子是0.75),HashMap的空间将会扩大,而且扩大后新的空间一定是原来的2倍。
假如我们有几十万、几百万条数据,那么HashMap要存储完这些数据将要不断的扩容,而且在此过程中也需要不断的做哈希运算,这将对我们的内存空间造成很大消耗和浪费,而且HashMap获取数据是通过遍历Entry[]数组来得到对应的元素,在数据量很大时候会比较慢,所以在Android中,HashMap是比较费内存的,我们在一些情况下可以使用SparseArray和ArrayMap来代替HashMap。
SparseArray
SparseArray比HashMap更省内存,在某些条件下性能更好,主要是因为它避免了对Key的自动装箱,它内部则是通过两个数组来进行数据存储的,一个存储Key,另外一个存储Value,为了优化性能,它内部对数据还采取了压缩的方式来表示稀疏数组的数据,从而节约内存空间。
SparseArray只能存储Key为int类型的数据,同时,SparseArray在存储和读取数据时候,使用的是二分查找法。SparseArray存储的元素都是按元素的Key值从小到大排列好的。而在获取数据的时候,也是使用二分查找法判断元素的位置,所以,在获取数据的时候非常快,比HashMap快的多。
SparseArray还提供了两个特有方法,更方便数据的查询:
获取对应的Key:
public int keyAt(int index)
获取对应的Value:
public E valueAt(int index)
虽说SparseArray性能比较好,但是由于其添加、查找、删除数据都需要先进行一次二分查找,所以在数据量大的情况下性能并不明显,将降低至少50%。
满足下面两个条件我们可以使用SparseArray代替HashMap:
- 数据量不大,最好在千级以内
- Key必须为int类型,这中情况下的HashMap可以用SparseArray代替
ArrayMap
ArrayMap是一个
应用场景
- 数据量不大,最好在千级以内
- 数据结构类型为Map类型
自动装箱
我们知道基础数据类型的大小:boolean
(8 bits), int
(32 bits), float
(32 bits),long
(64 bits),为了能够让这些基础数据类型在大多数Java容器中运作,会需要做一个AutoBoxing的操作,转换成Boolean
,Integer
,Float
,Long
等对象
1 | // 基础类型 |
AutoBoxing的行为还经常发生在类似HashMap这样的容器里面,对HashMap的增删改查操作都会发生了大量的AutoBoxing的行为。
Android提供了一些数据结构避免自动装箱行为:SparseBoolMap
、SparseIntMap
、SparseLongMap
、LongSparseMap
枚举
不要在Android中使用枚举,比起静态常量会有超出两倍的额外内存开销
内存使用
Android系统提供了一些回调来通知应用的内存使用情况,通常来说,当所有的
Background应用都被杀掉的时候,Forground应用会收到onLowMemory()
的回调。在这种情况下,需要尽快释放当前应用的非必须内存资源,从而确保系统能够稳定继续运行。Android系统还提供了onTrimMemory()
的回调,当系统内存达到某些条件的时候,所有正在运行的应用都会收到这个回调。
内存泄漏
通常来说,View会保持Activity的引用,Activity同时还和其他内部对象也有可能保持引用关系。当屏幕发生旋转的时候,Activity很容易发生泄漏,这样的话,里面的View也会发生泄漏。Activity以及View的泄漏是非常严重的,为了避免出现泄漏,请特别留意以下的规则:
- 避免使用异步回调
异步回调被执行的时间不确定,很有可能发生在Activity已经被销毁之后,这不仅仅很容易引起Crash,还很容易发生内存泄露。
- 避免使用静态对象
因为静态对象的生命周期过长,使用不当很可能导致泄漏,在Android中应该尽量避免使用静态对象。
- 避免把View添加到没有清除机制的容器里面
假如把View添加到WeakHashMap,如果没有执行清除操作,很可能会导致泄漏。
定位与电量优化
开启定位功能是一个相对来说比较耗电的操作,一般通过setInterval()
设置每隔多长的时间获取一次位置更新,时间相隔越短,自然花费的电量就越多,但是时间相隔太长,又无法及时获取到更新的位置信息。其中存在的一个优化点是,我们可以通过判断返回的位置信息是否相同,从而决定设置下次的更新间隔是否增加一倍,通过这种方式可以减少电量的消耗。
通过GPS定位服务相比起使用网络进行定位更加的耗电,但是也相对更加精准一些。为了提供不同精度的定位需求,同时屏蔽实现位置请求的细节,Android提供了下面4种不同精度与耗电量的参数给应用进行设置调用,应用只需要决定在适当的场景下使用对应的参数就好了,通过LocationRequest.setPriority()
方法传递参数就好了。
多重Layout
RelativeLayout
会发生两次layout
,LinearLayout
等在某些情况下也会触发两次layout
,如果只是少量的重复布局本身并不会引起严重的性能问题,但是如果它们发生在布局的根节点,或者是列表里面的某个列表项,这样就会引起比较严重的性能问题。
我们可以使用Systrace
来跟踪特定的某段操作,如果发现了疑似丢帧的现象,可能就是因为重复布局引起的。通常我们无法避免重复布局,在这种情况下,我们应该尽量保持View Hierarchy的层级比较浅,这样即使发生重复布局,也不会因为布局的层级比较深而增大了重复布局的倍数。另外还有一点需要特别注意,在任何时候都请避免调用requestLayout()
的方法,因为一旦调用了requestLayout,会导致该布局的所有父节点都发生重新布局的操作。
网络请求优化
- 使用回退机制来避免固定频繁的同步请求:在发现返回数据相同的情况下,推迟下次的请求时间
- 使用批处理的方式来集中发出请求,避免频繁的间隔请求
- 使用预取的技术提前把一些数据拿到,避免后面频繁再次发起网络请求
- 压缩传输数据