Google之前发布了关于Android性能优化典范的专题,一共16个短视频,每个3-5分钟,帮助开发者创建更快更优秀的Android App。课程专题不仅仅介绍了Android系统中有关性能问题的底层工作原理,同时也介绍了如何通过工具来找出性能问题以及提升性能的建议。主要从三个方面展开,Android的渲染机制,内存与GC,电量优化。下面是对这些问题总结梳理,和一些个人理解。
1.Android渲染机制:
用户感知到的卡顿等性能问题的最主要根源都是因为渲染性能。
Android系统每隔16ms发出VSYNC信号,触发对UI进行渲染,如果每次渲染都成功,这样就能够达到流畅的画面所需要的60fps。
为了能够使得App流畅,我们需要在每一帧16ms以内处理完所有的CPU与GPU计算,绘制,渲染等。
引起卡顿操作
(1)Android系统可能无法在16ms内,及时完成那些复杂的界面渲染操作,就发生卡帧现象。
(2)复杂操作处理超过16ms,比如20ms后处理完成
发出VSYNC信号
,那么
系统在得到信号的时候就无法进行正常渲染,这样就发生了丢帧现象。
我们可以通过一些工具来定位问题,比如可以使用HierarchyViewer来查找Activity中的布局是否过于复杂,也可以使用手机设置里面的开发者选项,打开Show GPU Overdraw等选项进行观察。你还可以使用TraceView来观察CPU的执行情况,更加快捷的找到性能瓶颈。
layout太过复杂,
UI上有层叠太多的绘制单元,
动画执行的次数过多等都导致GPU负载过重,
从这些方面考虑性能优化。
Overdraw(过度绘制)描述的是屏幕上的某个像素在同一帧的时间内被绘制了多次。
在多层次的UI结构里面,如果不可见的UI也在做绘制的操作,这就会导致某些像素区域被绘制了多次。这就浪费大量的CPU以及GPU资源。
设计上追求更华丽的视觉效果的时候,我们就容易陷入采用越来越多的层叠组件来实现这种视觉效果的怪圈。这很容易导致大量的性能问题,为了获得最佳的性能,我们必须尽量减少Overdraw的情况发生。
通过手机设置里面的开发者选项,打开Show GPU Overdraw的选项,可以观察UI上的Overdraw情况。
蓝色,淡绿,淡红,深红代表了4种不同程度的Overdraw情况,我们的目标就是尽量减少红色Overdraw,看到更多的蓝色区域。
Overdraw有时候是因为你的UI布局存在大量重叠的部分,还有的时候是因为非必须的重叠背景。例如某个Activity有一个背景,然后里面的Layout又有自己的背景,同时子View又分别有自己的背景。仅仅是通过移除非必须的背景图片,这就能够减少大量的红色Overdraw区域,增加蓝色区域的占比。这一措施能够显著提升程序性能。
需要考虑布局参数影响:
(1)
需要注意的是:任何时候View中的绘制内容发生变化时,都会重新执行创建DisplayList,渲染DisplayList,更新到屏幕上等一系列操作。这个流程的表现性能取决于你的View的复杂程度,View的状态变化以及渲染管道的执行性能。举个例子,假设某个Button的大小需要增大到目前的两倍,在增大Button大小之前,需要通过父View重新计算并摆放其他子View的位置。修改View的大小会触发整个HierarcyView的重新计算大小的操作。如果是修改View的位置则会触发HierarchView重新计算其他View的位置。如果布局很复杂,这就会很容易导致严重的性能问题。我们需要尽量减少Overdraw,View操作的时候需要谨慎处理。
(2)移除非必需的UI组件,这些操作能够减少Measure,Layout的计算时间。
关于我们自定义View的过程中,在变化的哪个节点,
Measure,layout,draw,都是我们需要优化注意的点。
(3)
非可见的UI组件进行绘制更新会导致Overdraw。android系统组件是会自动避免绘制那些完全不可见的组件来尽量减少overdraw。
但是不幸的是,对于我们自定义的View(重写了onDraw方法),Android系统无法检测具体在onDraw里面会执行什么操作,系统无法监控并自动优化,也就无法避免Overdraw了。
但是我们可以通过canvas.clipRect()来帮助系统识别那些可见的区域。这个方法可以指定一块矩形区域,只有在这个区域内才会被绘制,其他的区域会被忽视。这个API可以很好的帮助那些有多组重叠组件的自定义View来控制显示的区域。同时clipRect方法还可以帮助节约CPU与GPU资源,在clipRect区域之外的绘制指令都不会被执行,那些部分内容在矩形区域内的组件,仍然会得到绘制。
除了clipRect方法之外,我们还可以使用canvas.quickreject()来判断是否没和某个矩形相交,从而跳过那些非矩形区域内的绘制操作
(4)关于RelativeLayout与LinearLayout性能对比
a.RelativeLayout会对子View进行两次measure,LinearLayout只对子View进行一次measure,而在设置了weight时,也会对weight进行两次measure,通常情况下,LinearLayout的性能要优于RelativeLayout。
b.在View的层级扁平,没有过多的嵌套的情况下,用LinearLayout效率更高,并且要尽量减少使用weight属性。
c.如果View的层级嵌套过多,则需要使用RelativeLayout来降低层级,因为Android是递归生成View的,过多的层级嵌套会严重影响View的绘制效率。
2.内存
虽然Android有自动管理内存的机制,但是对内存的不恰当使用仍然容易引起严重的性能问题。在同一帧里面创建过多的对象是件需要特别引起注意的事情。
Android系统里面有一个
Generational Heap Memory
的模型,系统会根据内存中不同的内存数据类型分别执行不同的GC操作。
最近刚分配的对象会放在Young Generation区域,这个区域的对象通常都是会快速被创建并且很快被销毁回收的,同时这个区域的GC操作速度也是比Old Generation区域的GC操作速度更快的。
除了速度差异之外,执行GC操作的时候,任何线程的任何操作都会需要暂停,等待GC操作完成之后,其他操作才能够继续运行。
通常来说,单个的GC并不会占用太多时间,但是大量不停的GC操作则会显著占用帧间隔时间(16ms)。如果在帧间隔时间里面做了过多的GC操作,那么自然其他类似计算,渲染等操作的可用时间就变得少了。
导致GC频繁执行有两个原因:
(1)内存抖动:
内存抖动是因为大量的对象被创建又在短时间内马上被释放。
瞬间产生大量的对象会严重占用Young Generation的内存区域,当达到阀值,剩余空间不够的时候,也会触发GC。即使每次分配的对象占用了很少的内存,但是他们叠加在一起会增加Heap的压力,从而触发更多其他类型的GC。这个操作有可能会影响到帧率,并使得用户感知到性能问题。
例如:
a.你需要避免在for循环里面分配对象占用内存,需要尝试把对象的创建移到循环体之外,
b.自定义View中的onDraw方法也需要引起注意,每次屏幕发生绘制以及动画执行过程中,onDraw方法都会被调用到,避免在onDraw方法里面执行复杂的操作,避免创建对象。
3.对于那些无法避免需要创建对象的情况,我们可以考虑对象池模型,通过对象池来解决频繁创建与销毁的问题,但是这里需要注意结束使用之后,需要手动释放对象池中的对象。
原始JVM中的GC机制在Android中得到了很大程度上的优化。Android里面是一个三级Generation的内存模型,最近分配的对象会存放在Young Generation区域,当这个对象在这个区域停留的时间达到一定程度,它会被移动到Old Generation,最后到Permanent
[‘pә:mәnәns],
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工具来查看此时内存中分配的到底有哪些对象。
(2)内存泄漏
Java有自动回收的机制,可是这不意味着Java中不存在内存泄漏的问题,而内存泄漏会很容易导致严重的性能问题。
内存泄漏指的是那些程序不再使用的对象无法被GC识别,这样就导致这个对象一直留在内存当中,占用了宝贵的内存空间。显然,这还使得每级Generation的内存区域可用空间变小,GC就会更容易被触发,从而引起性能问题。
垃圾收集的目的在于清除不再使用的对象。GC通过确定对象是否被活动对象引用来确定是否收集该对象。GC首先要判断该对象是否是时候可以收集。两种常用的方法是引用计数和对象引用遍历
- GC是按照有向图是否可达来判断对象实例是否有用
- 如果不在需要某个实例,却仍然被引用,这个情况叫做内存泄露
- 匿名类/非静态类/内部class,会保持对它所在Activity的引用,使用时要注意它们的生命周期不能超过Activity,否则要用static inner class
- 善于在Activy中的生命周期(比如onPause)中手动控制其他类的生命周期
回到垃圾收集器,在学习GC前,你应该知道一个技术名词:这个词是“
stop-the-world
。“ 无论你选择哪种GC算法,Stop-the-world都会发生。
Stop-the-world
意味着JVM停止应用程序,而去进行垃圾回收。当stop-the-world发生时,除了进行垃圾回收的线程,其他所有线程都将停止运行。被中断的任务将在GC任务完成后恢复执行。GC调优往往意味着减少stop-the-world的时间。
年轻代:新创建的对象都存放在这里。因为大多数对象很快变得不可达,所以大多数对象在年轻代中创建,然后消失。当对象从这块内存区域消失时,我们说发生了一次“minor GC”。
老年代:没有变得不可达,存活下来的年轻代对象被复制到这里。这块内存区域一般大于年轻代。因为它更大的规模,GC发生的次数比在年轻代的少。对象从老年代消失时,我们说“major GC”(或“full GC”)发生了。
永久代(permanent generation):
也称为“
方法区(method area)
”,他存储class对象和字符串常量。所以这块内存区域绝对不是永久的存放从老年代存活下来的对象的。在这块内存中有可能发生垃圾回收。发生在这里垃圾回收也被称为major GC。
下面在总结一些,日常coding中需要注意的点:
(1)comtent对象引用注意:一些脱离页面的生命周期模块,赋予Application的,不要给activity的。
关注对象引用,对象生命周期,自己设计执行模块,需要设计生命周期开关。避免内存泄漏。
(2)避免在Bitmap上浪费内存,
当我们读取一个Bitmap图片的时候,有一点一定要注意,就是千万不要去加载不需要的分辨率。在一个很小的ImageView上显示一张高分辨率的图片不会带来任何视觉上的好处,但却会占用我们相当多宝贵的内存。需要仅记的一点是,将一张图片解析成一个Bitmap对象时所占用的内存并不是这个图片在硬盘中的大小,100K,
1500*1000像素如果不压缩处理,
ARGB_8888颜色类型,
每个像素点就会占用4个字节,总内存就是1500*1000*4字节,也就是5.7M
内存,内存浪费。
BitmapFactory
提供了多个解析方法
创建Bitmap对象。
当我们明知Bitmap不会再被使用时,还是应该手动调用recycle,这样至少可以避免临时的内存泄漏和频繁GC等问题。
当然大多数情况我们都用,universal image loader或者其他Facebook的
Fresco
[‘freskәʋ]来实现
(3) LruCache类简介:
LinkedHashMap map;
Cache保存一个强引用来限制内容数量,每当Item被访问的时候,此Item就会移动到队列的头部。
当cache已满的时候加入新的item时,在队列尾部的item会被回收
LinkedHashMap是HashMap的一个子类,它
保留插入的顺序,
LinkedHashMap是Map接口的哈希表和链接列表实现。
(4)
资源对象未关闭,例如Cursor,File等,虽然系统回收时也有可能关闭资源,但我们最好每次使用完后都手动调用其close函数,并置为null。
(5)
要特别注意非static的匿名内部类的对象,会持有外部类的引用。例如Handler在Activity中定义为非静态的匿名内部类是很危险的。Activity退出时有可能有未处理或正在处理的的消息,消息队列中的message又持有handler的引用。另一个相似的例子是线程的runnable或asynctask被定义为非静态内部类。事实上,我们总是应该尽可能把内部类改为静态内部类,并且在静态内部类中使用弱引用来引用外部类的成员变量。
(6)重用布局文件,和 ,,自定义视图减少过度重徽,减少重叠视图,
(7)Listview ,adapter的优化,
ViewHolder,复用View,减少解析XML,Tag保存信息,局部刷新item,减少直接刷新adapter重复构建
(8)activity加载模式,优化利用activity栈,根据实际需求,合理规划activity栈存储。
为了寻找内存的性能问题,Android Studio提供了工具来帮助开发者。
- (1)Memory Monitor['mɔnitә]:查看整个app所占用的内存,以及发生GC的时刻,短时间内发生大量的GC操作是一个危险的信号。
- 注意上面有两个常用选项,一个是Initate GC,用于手动触发GC;另一个是dump java heap,用于导出一个内存镜像文件hprof。
-
MAT内存分析工具 :
-
MAT(Memory Analyzer)是基于eclipse的java heap分析工具,可以在官网上下载到MAT的独立版本:https://www.eclipse.org/mat/。
Android studio导出的hprof文件是无法直接被MAT识别的,可以在studio中的Cpatures面板内找到hprof文件点右键转化(Export to standard .hprof),或者使用sdk tools中的hprof-conv做个转换:
hprof-conf [-z] infile outfile
使用MAT打开.hprof文件后可以看到一个overview界面,显示了内存使用概况和一个饼状图:
-
选择Histgram将打开一个新的页面,并列出所有的类和其实例化对象的个数。在这个表格的第一行,可以通过输入正则式来筛选类名,例如我们输入Activity筛选出名字中带有Activity的所有类,这样可以很轻松找到我们要分析的类Activity拥有的对象,
选中某个对象,再右键选择Path to GC Roots->exclude weak/soft references打开一个新页面,可以列出所有强引用的GC Roots结点,我们定位到ActivityManager中的mActivities对象强持有了ActivityA对象,导致页面关闭后Activity的内存无法被回收,造成内存泄漏。
如何利用此工具发现内存泄漏
上面分别介绍了使用Android studio和MAT分析内存的方法。Android studio自带的内存分析工具直观方便,但其功能却不如MAT强大,特别是没有有效的搜索、排序等功能。遇到一些棘手的问题,可能还是要借助MAT来分析内存。
实际开发中,我们如何发现内存泄漏呢?我想可以首先使用studio自带或DDMS中的heap分析工具,观察在反复执行某个操作时(例如打开某个页面、点击某个按钮、加载某个资源等等)时,内存在执行GC后能始终维持在稳定的值附近。如果内存呈线性增长的趋势,那一定是发生了内存泄漏。此时,就要dump出内存镜像,然后使用工具分析了。
在分析内存时,第一是可以使用工具自带的泄漏检查器帮助定位。另外,可以在执行操作(怀疑造成内存泄漏的操作)前后,分别dump出一份内存镜像,然后使用MAT的Compare Basket对比两个文件的内存情况,这样可以帮助定位到是哪个对象发生了泄漏。然后再找到这个对象的GC Roots,这样就可以进一步定位到具体的代码了。