本文基本翻译自Facebook工程师的文章
Speed up your app,也加入了自己的一些内容。
会介绍以下几个主题
- Systrace
- Traceview
- Memory Profiling
- Allocation Tracker
- GPU Profiling
- Hierarchy Viewer
- Overdraw
- Alpha
- Hardware Acceleration
Systrace
Systrace的功能可以在AS的DDMS中找到,但不太稳定,所以这里只介绍命令行模式。
另外Systrace只能分析概况,不能定位问题位置,不太感兴趣的朋友也可以直接跳到第二节Traceview。
在终端(我的环境是MAC)中先进入sdk/platform-tools/systrace目录
cd /Users/apple/Documents/Android/sdk/platform-tools/systrace
然后执行Systrace的命令是
python systrace.py --time=10 -a com.duotin.fm -o mynewtrace.html sched gfx view wm
--time=10 表示记录10秒
更多参数说明请查看官网
执行完打开生成的mynewtrace.html文件
呈现这样的界面
点击第一列的三角形警示图标或者第二列的圆形警示图标,都可以查看警示详情,点击效果分别如下
三角形警示图标,每个图标代表一个警示,点击后查看警示详情。
圆形警示图标,每个图标代表一个frame,显示红色或者黄色表示此frame的时间已经超过16.6 millisecond per frame的标准,会导致界面失帧。点击后查看这个frame所有的警示。
此时按"M"快捷键可以高亮当前选中的frame。“A"和"D"分别为左移和右移视图,"W"放大,"S"缩小。
此外,点击各颜色块,可以查看各颜色块的详情。
点击上图中的第一个Alert: Inflation during ListView recycling
显示详情
可以看出Inflation during ListView recycling的执行时间是32ms(远远超过了16ms的限制),共5个item,平均到每个item为6ms。通过点击该frame范围内的颜色块,可以查看各个方法的详情。
我们再点击一个圆形警示图标并高亮
顶部显示此frame耗时19ms, 点击右下方的Alert,显示有 Scheduled delay。
Scheduled delay 是指告诉CPU执行任务,但CPU太忙了,任务被延迟执行了。
点击下面的一个颜色块,显示如下
Wall duration 是指此颜色块代表的方法从开始到结束的耗时
CPU duration 是指 CPU的执行时间
可以发现CPU duration只有4ms,但Wall duration有18ms。
延迟这么严重,我们来看看原因。
在选中的颜色块上方,我们看到四个CPU都被颜色块填充,表示此时4个CPU都有活干,很忙。
我们选中一个CPU的颜色块
可以发现占用CPU的应用是com.udinic.keepbusyapp
恩,对Systrace的介绍到此结束了,虽然还有些没讲,但Systrace的确只能看个概览。
且慢,再加送一个tip, 点击右边栏的Alerts, 你能看到所有的Alerts.
通过这张图我分析出
inefficient view alpha usage数量最多。Inflation during ListView recycling影响时间最长,耗时长达52ms。
inefficient view alpha usage是因为调用具体View的setAlpha方法,而View的setAlpha在Android中是很昂贵的操作。解决方法是用ARGB设置color代替直接调用setAlpha;如果是ImageView,调用ImageView#setImageAlpha;如果是自定义View,覆盖hasOverlappingRendering()或者onSetAlpha()或者通过paint.setAlpha实现。详细请参考文章1,文章2, 文章3
Traceview
Traceview能够在方法层面上分析APP的性能,非常强大。
可以通过,命令行或者GUI启动,我用的是GUI启动,点击AS的Android Device Monitor, 点击Devices栏目下面的 Start Method Profilling的图标 , 在对话框中选择,(我选的是trace Based Profilling,表示实时分析,会比较慢,但分析结果详细),操作APP, 分析结束的时候,点击同一个图标即可。更多操作请访问官网
先看下界面
列名 | 意义 |
---|---|
Name | 方法名,每个方法的颜色都不一样。 |
Inclusive CPU Time | 此方法占用CPU的时间,Inclusive指包括调用的方法所 |
Exclusive CPU Time | 此方法所占用CPU的时间,Exclusive 指不包括调用的方法 |
Inclusive / Exclusive Real Time | Real Time指方法从开始到结束消耗的时间,跟Systrace中的Wall duration一个意思。 |
Calls+Recursion | 此方法被调用了多少次+多少次是递归调用 |
Calls / Total | 子方法被此父方法调用的次数/子方法被调用的总次数 |
点击某条目下的parent 或者 child 方法时,会跳到该方法的条目。
想找出最影响性能的方法,可以点击Exclusive CPU time一栏,找出消耗时间最长的几个方法。如果是应用的方法,直接看可不可优此方法。如果是系统方法,通过查看其父方法,追溯至应用方法。
而查看子方法,可以看出此方法到底做了什么。
如果要找UI卡顿的原因,可以从 具体Adaper类#getView 具体View#ondraw, 具体View#onMeasure等方法入手。
方法执行时占用了CPU,所以执行时间过长会造成UI渲染被延迟,从而应用不流畅。而GC同样会占用CPU,AS也同样提供了查看GC的工具:
(译者注:
Android性能分析工具Systrace和TraceView的使用)
Memory Profiling
点击AS中的Android Monitor, 选中Memory | CPU 一栏, 界面如下
如图所示,小幅的内存下降一般就是发生了GC。
点击左侧的Heap dump,会生成内存中的所有对象的快照。
列名 | 意义 |
---|---|
Total Counts | 内存中该类的对象个数 |
Heap Count | 在该堆中该类的对象个数,左上角可以选择App heap或Zygote heap |
Sizeof | 单个对象占用的Shallow Size |
Shallow Size | 所有对象所占用的Shallow Size |
Retained Size | 所有对象所占用的Retained Size,即GC后会释放的内存 |
instance | 该类一个具体的对象 |
Reference Tree | 引用这个对象的父对象,点击父对象,展开这些父对象的父对象 |
什么是Shallow Size 和Retained Size
选中一行,点击右侧的一个instance,可以在下方看到Refrence Tree界面
在图中可以看出MemoryActivity的一个instance在ListenerManager中被引用了。如果MemoryActivity已经不在Activity栈中了,这样的引用就是内存泄漏。另外一个检查内存泄漏的工具是leakcanary
通过查看Retained Size和Reference Tree,我们可以知道哪些对象占用了较多的内存,对象间的引用关系,进而分析是否可以优化数据结构,减少引用关系,以减少内存占用和GC频率。
Allocation Tracker
Memory | CPU一栏左侧的另一个按钮Allocation Tracker也是用于分析内存占用。点击一次表示开始记录,再次点击表示停止记录。
在结果页面的左上方点击饼状图。
可以选择 group by Allocator,即按对象划分。
或者 group by method
Allocator下面的饼状图最外围的是具体的类,内部的是包名。图中可以看出包或者对象占用的内存大小或者个数,面积越大,占用或者个数越多。选择size可以查看占用内存最多的对象,选择count可以查看以及个数最多的对象。前者我们可以试着优化类,后者我们可以尝试建立一个Object pool来复用对象。
从group by method可以看出,decode方法占用的总内存达10.91M, 就有可能是方法内新建了太多对象,可以往这方面优化。
内存方面的tips:
1. Enums
Enums比int占的内存大得多,而且有替换方案@IntDef, 所以除了某些情况,比如你需要强制指定类型,不然的话int会更节省内存
2. 自动装箱
自动装箱指从基本类型自动转换到对象形式的(比如int到Integer),鉴于基本类型使用的场景和次数都较多,所以需要尽量避免使用其自动装箱的形式。
3. HashMap vs ArrayMap / Sparse*Array
如果我们需要使用int作为Map的value,可以使用SparseIntArray,比起使用HashMap对int自动装箱,要省内存的多。如果要使用Object作为Map的key,除了HashMap,你也可以考虑使用ArrayMap,功能和HashMap一样,但更省内存,点击了解原理。尽管时间性能上HashMap更胜一筹,但除非你要存储1000个以上对象,否则他们使用起来几乎一样快。
4. 注意Context对象
因为Context在开发中的使用场景较多,所以最容易造成内存泄漏。Activity本身是一个heavy的对象,为了避免内存泄漏,可以穿ApplicationContext的话,就不要传Activity了。
5. 避免非静态内部类
非静态内部类隐式持有外部类的引用,所以如果外部类不再被需要,但内部类仍在使用状态,就造成了内存泄漏。特别是Activity类,在定义内部类的时候尽量定义成static的。
GPU Profiling
首先在手机的开发者选项页面,点击GPU呈现模式分析(Profile GPU rendering),选中“In adb shell dumpsys gfxinfo” 然后在AS的Android Monitor 界面选中GPU一栏,确保左上方的暂停按钮没有选中,此时AS就开始按照选定的包名显示GPU情况了。每一个条直线表示UI渲染中的一帧,不同的颜色表示不同的绘制阶段。
- Draw(蓝色) 执行的是View#onDraw()方法。这个阶段的工作是创建DisplayList对象,这些对象稍后将被转换成OpenGL命令,传送到GPU。如果蓝色较长,一般是因为较复杂的View, 或者短时间内invalidate了较多的View
- Prepare (紫色) Lollipop才引入的阶段,用于加快UI渲染,在线程RenderThread中执行。这个阶段的任务是将第一步产生的display lists转换成OpenGL命令,并传送到GPU。此时UI thread将继续处理下一帧。UI Thread给RenderThread传递所需的资源产生的耗时也记录在此阶段中。当有大量的资源要传递,比如很多/很heavy的display lists,这个阶段耗费的时间会增多。
- Process(红色) 处理display lists,产生OpenGL命令,较多或者较复杂的display lists会使此阶段耗时增加,因为很多View将被redraw。View被redraw的情况有invalidate或者之前覆盖在上面的View现在被移走了。
- Execute (黄色) 将OpenGL命令发送给GPU, 这是个阻塞方法,因为CPU通过buffer将OpenGL命令发送给GPU, 当处理完毕返回空的buffer。buffer的数量有限,所以当GPU很忙,buffer也用完了,CPU就需要等待GPU处理完返回一个空的buffer,才能继续发送OpenGL命令。因此如果这个阶段耗时较多,一般是因为在绘制复杂的View。
在 Marshmallow 版本,增加了更多的颜色
根据谷歌工程师John Reck提供的信息,
图中的Animation 是指所有通过Choreographer 注册的CALLBACK_ANIMATION,包括
Choreographer#postFrameCallback View#postOnAnimation。这两个函数在 view.animate(), ObjectAnimator, Transitions等场景中有用到。systrace中的Animation也是这个意思。
misc指的是接收到的vsync的时间戳和当前时间的延迟。
很多人都看到过Choreographer的log "Missed vsync by。。。ms skipping 。。。 frames",这就是misc。换句话说,就是在记录帧状态时INTENDED_VSYNC和VSYNC的差别
要使用这个功能,你需要在手机的开发者选项中开启GPU rendering
此工具原理是ADB命令
adb shell dumpsys gfxinfo <PACKAGE_NAME>
如果你自己手动敲此命令,也会得到如下相关信息
如果我们的项目有自动化UI测试工具,就可以在构建服务器上在一些UI交互后(列表滑动,复杂动画)运行此命令,查看“Janky Frames”等值是否有变化。这样做能够帮我们确定最近的几次提交(commite)是否影响了性能,在产品发布前发现和解决此问题。如果我们使用framestats作为关键字,还能获得更多详细的渲染信息。
我们还能以其他方式展示此图
在“Profile GPU Rendering”选项里,有“On screen as bars”这个选项,选中它,在手机屏幕上就会出现三个图像,分别代表StatusBar,NavBar和当前程序的Activity的GPU Rending信息,以绿线指示16ms的渲染阈值。
在右侧这张图,我们看到有些帧超过了绿线,即说明渲染时间超过了16ms。这些“越界”的帧大部分是蓝色,我们大概可以认为是因为绘制了太多或者太复杂的View。我滑动了一下此界面的信息流,的确有多种类型的View.有些会被重绘,有些比较复杂。所以那些“越界”的帧可能是因为正在绘制复杂的View.
Hierarchy Viewer
我非常喜欢这个工具,很遗憾好多人都没有使用过。
Hierarchy Viewer能显示性能情况,屏幕上完整的View结构,以及View的属性。如果你单独运行Hierarchy Viewer,而不是从Android Monitor启动,你也能获取主题信息,所有的style属性。当我设计以及优化布局时就会这么做。
在上图中间,我们能看到树形的View结构。View结构可以很宽,但是如果View层数太深(比如10层左右),就会增加昂贵的layout和measurement操作。测量View时调用View.onMeasure, 布局子View时调用View.onLayout。这两个命令会向子View传递。有些布局会调用两次这两个命令,比如RelativeLayout和某些LinearLayout,如果View是嵌套的,命令传递的次数就会以指数增长。
在右下角,是布局实际的效果,标注了每个View放置的位置,我们可以在此图或者树形图中选中一个View, 然后在左边查看所有的属性。当设计布局时,我有时候不能确定为什么某个View会被布局在那里。有了这个工具,我就能在树形图上找到它,选中,然后就能在预览图中看到它的位置。在设计有趣的动画时,我会查看屏幕中View的最终测量数据,据此精确的移动View。我也能发现被无意盖掉而看不见的View。
对于每一个View,我们都可以获知它本身以及子View的measure/layout/draw时间。颜色指示它与其他View比较时,性能情况如何,有助于找出View绘制过程中最慢的一环。而且我们能看到View的预览,能在树形图中看到此View创建的步骤,找出和删除冗余的步骤。影响性能的一个重要原因是Overdraw。
(译者注:
Android Studio的
File——Settings——Inspections——Android Lint可以设置检测layout的深度和宽度:
勾选 Layout has too many views 和 Layout hierarchy is too deep
Layout has too many views表示控件太多,默认超过80个控件会提示该问题。
Layout hierarchy is too deep表示布局太深,默认层级超过10层会提示该问题,可以自定义环境变量ANDROID_LINT_MAX_DEPTH来修改。
布局层数过深会导致StackOverflowErrors,因为draw() dispatchDraw() drawChild()等方法都会消耗stack空间)
Overdraw
正如GPU Profiling所呈现的,如果GPU需要在屏幕上进行大量的绘制,Execute阶段(GPU中的黄色部分)会花费更多的时间,绘制每一帧所需的时间也就拉长了。在已绘制的屏幕上再度绘制,就叫做过度绘制,比如在红色的背景上绘制一个黄色的按钮。GPU需要先绘制红色的背景,然后绘制黄色的按钮,就造成了过度绘制。如果是很多层的过度绘制,GPU的工作负荷就很重,就会影响到16ms的性能指标。
启用开发者选项中的“调试GPU过度绘制”,所有的过度绘制就会按照严重程度用不同的颜色展示。1~2层的过度绘制算合理,有小范围的浅红也还能接受,但是如果红色的区域太多,就需要注意了。举几个例子:
左图中,有一个绿色的列表,通常意味着还行,但顶部的覆盖区域显示为红色,就需要解决了。右图中,整个列表都是浅红。两个图的列表都不透明,都有2~3层的过度绘制。一个可能的原因是持有Activity/Fragment的窗口(window),ListView,以及每个ListView的item都有各自的背景。解决的办法是只设置一个背景。
注意:默认的主题设置了窗口的背景色,如果你的Activity包含的不透明的布局能覆盖全屏幕,你就可以通过移除窗口的背景色来减少过度绘制。可以在主题中设置,或者在onCreate方法中调用 getWindow().setBackgroundDrawable(null)
利用 Hierarchy Viewer, 你能导出所有层级到 PSD 文件。用Photoshop打开此文件,查看不同的层级,你能发现布局中所有的过度绘制. 请利用这些信息去掉冗余的过度绘制,不要调试GPU过度绘制时显示绿色就觉得可以了,争取蓝色。
Alpha
使用透明属性可能会影响性能。为了理解这句话,我们来看看当给View设置alpha属性时,会发生些什么?初始布局如下图所示
此布局包含三个互相有重叠的ImageView。如果对布局设置了Alpha属性,也就是调用 setAlpha(),“直接/简单粗暴的方案”是对各个子View(在此例中是三个ImageView)设置Alpha。这样三个ImageView会依据设置的Alpha重新绘制并进入帧缓冲。结果如下:
这不是我们想要的。
因为每个Image都设置了alpha,重叠的ImageView就混合在一起了。幸运的是,安卓系统找到了解决方案。布局将会被复制到离屏缓冲,并对缓冲区整体设置Alpha,处理结果将会复制回帧缓冲。结果如下:
但是。。这样做是有代价的。
先在离屏缓存区绘制View,然后再绘制帧缓存,实际上是增加了一层未被GPU Profiling检测到的过度绘制。安卓系统不知道何时使用此方案,何时使用之前提到的方案,所以只能默认使用此方案。但我们仍有办法设置Alpha并且避免离屏缓存造成的复杂性。
- TextViews - 调用 setTextColor() 而不是 setAlpha(). 使用alpha 通道设置字体颜色, 绘制文本时会直接使用此alpha。
- ImageView - 使用 setImageAlpha() 而不是 setAlpha() 。理由同上。
- 自定义 View - 如果你的自定义View不支持重叠子View, 就无须理会此合成操作, 因为子View不会像上面的例子混合在一起. 覆写hasOverlappingRendering() 方法,并返回 false,我们就在告诉安卓系统:使用“直接/简单粗暴的方案”。如果要自己处理alpha属性,就覆写onSetAlpha()方法,并返回true。详细请参考文章1,文章2, 文章3
(译者注:
其他过度绘制优化实践
Android性能优化之减少UI过度绘制
实战 Android中的UI过度绘制
)
Hardware Acceleration
安卓蜂巢版引进了硬件加速功能,我们由此有了新的绘制模型来渲染APP。硬件加速引入了DisplayList结构,通过记录 View 的绘制命令来加快渲染。但是有一个很重要的功能是开发者往往遗漏或者没有正确的使用的—View layers。
使用View layer,我们能在离屏缓存中渲染View(就像上面应用Alpha通道的例子),并随意操作View。这个功能在动画的时候很有用,能让复杂的View在动画时更顺畅。 不使用layer的话, 对View执行动画时会先改变其属性(比如 x 坐标,伸缩比,透明度等)然后invalidate. 对于复杂的View, invalidate 操作会传递到各个子View, 各个子View都会重绘,是个昂贵的操作。而Hardware提供的View layer 则是在GPU中为View生成一个纹理(Texture)。利用texture,某些操作(改变x/y轴坐标,旋转,alpha等)就不需要invalidate了。这一切意味着,我们能在一个复杂的View上执行动画,而不需要invalidate! 这会让动画顺畅很多。下面的例子将告诉你如何做:
// Using the Object animator
view.setLayerType(View.LAYER_TYPE_HARDWARE, null);
ObjectAnimator objectAnimator = ObjectAnimator.ofFloat(view, View.TRANSLATION_X, 20f);
objectAnimator.addListener(new AnimatorListenerAdapter() {
@Override
public void onAnimationEnd(Animator animation) {
view.setLayerType(View.LAYER_TYPE_NONE, null);
}
});
objectAnimator.start();
// Using the Property animator
view.animate().translationX(20f).withLayer().start();
很简单,是不是?
不过使用hardware layer时需要注意以下几点:
Hardware layer 占用了GPU有限的内存,所以请只在动画等需要的场合使用Hardware layer,使用完后及时清理。上例中,使用ObjectAnimator时,我设置了一个监听器,在动画结束时移除layer。使用Property animator时, 我使用了withLayers()方法, 此方法会在动画开始时自动创建layer并在动画结束时移除。
如果你在应用了hardware layer以后改变了View的属性, 就会invalidate hardware layer 并在离屏缓存重新渲染一遍View。这种情况在执行Hardware Layer未优化的操作时会发生 ( 到目前为止, 被优化的操作包括: 旋转, 伸缩, 坐标设置, 坐标偏移, pivot(枢轴) 和 透明度)。比如 , 你对使用了hardware layer的View执行动画 ,一边位移一边更新 View 的背景色,就会导致hardware layer不停的更新. 在这种情况下,更新hardware layer的开销会抵消掉使用它带来的好处。
在第二种情况下,我们可以查看hardware layer更新的情况。即在开发者选项中启用 “显示硬件层更新”
启用后,View在更新其hardware layer时会以绿光闪烁。不久前我的一个ViewPager滑动起来不流畅时我就启动了此选项. 下图是我当时所看到的:
在整个的滑动过程中,两个Page都显示绿色!
这意味着有hardware layer被创建,并且滑动的过程中页面被重新渲染。我的确在滑动时更新了页面,对背景使用了视差效果,并且页面中的内容也有渐进的动画效果。但是我并没有主动创建hardware layer。在阅读了ViewPager的源码后,我发现当用户滑动时,会为两个页面创建hardware layer,并且在滑动停止时移除。
尽管滑动页面时创建hardware layer是合理的行为,但不适合我的这种情形。通常情况下,我们并不会在滑动页面的同时更新页面,而页面内容也往往很复杂—创建hardware layer就可以帮助渲染的更快。在我的这个应用中,却并不是这样的,因此我hack了一下,移掉了hardware layer。
hardware layer并不是银弹,理解它是如何工作的,并恰当的使用很重要,否则你会可能会发现得不偿失。
DIY
为了演示我提到的这些例子,我写了很多代码来模拟情景。你能在 Github repository和 Google Play找到所有的演示例子。
更多的信息
随着Android 操作系统的演进,你优化APP的方式也得跟着变化。新的Android SDK会引入新的工具和功能(比如hardware layers)。跟上趋势,在改动代码时权衡利弊,这很重要。
YouTube上有一个很棒的播放列表, Android Performance Patterns,是一系列谷歌出品的小视频,阐述了性能优化的各个方面。比如比较不同的数据结构(HashMap vs ArrayMa),如何优化Bitmaps ,甚至是优化网络请求。我强烈建议把播放列表都看一遍。
请加入 Android Performance Patterns Google+ community ,一起和谷歌工程师在内的人士讨论性能问题,分享观点,文章和问题。
更多有趣的链接:
- 了解安卓图形架构如何运行。文章完整的解释了如下主题:
Android是如何渲染UI的,不同系统组件的差异和通信(比如SurfaceFlinger)。文章很长,但值得一读。 -
Google IO 2012的一则演讲
阐述了绘制模型的工作原理,以及UI失帧的原因。 - Android Performance Workshop talk,来自Devoxx 2013,展示了Android 4.4 引入的绘制模型的优化,以及用于优化性能的不同工具(Systrace, Overdraw等)
- 关于防御性优化的好文章,解释了其与过早优化的区别。
很多开发者不想优化代码,因为他们觉得造成的影响不易觉察。请记住,积少成多会导致大问题。如果有机会做一个小的优化,尽管微小,但请别放弃。 -
Memory managment in Android
Google IO 2011的一个视频,有点老旧,但仍然有价值。说明了安卓如何管理内存,以及检测工具如Eclipse MAT -
Case study
Romain Guy的作品,内容是优化Twitter客户端。介绍了他如何发现APP的性能问题,也推荐了解决方案。这里有个 follow-up post,继续跟进了此APP的性能优化问题。
我希望你们已经获取了足够的信息和自信来优化你们的APP,并从今天就开始优化。
trace用起来,开发者选项用起来,你已经走在优化的路上了。你的任何新发现,都可以在这留言,或者发表在Android Performance Patterns Google+ community