内存性能分析
Android Studio 最近改进了很多,有越来越多的工具可以帮助我们找出和分析性能问题。Android 窗口中的内存页告诉我们,随着时间的推移有多少数据在栈上分配。它看上去像这样:
我们在图中看到一个小的下降,这里发生了一次 GC 事件 ,移除了堆上不需要的对象和释放了空间。
图中的左边有两个工具可用:堆转储和分配跟踪器。
堆转储
为了调查堆上分配了什么,我们可以使用左边的堆转储按钮。这将对当前堆上分配的东西进行快照,在 Android Studio中作为一个单独报告呈现在屏幕上:
我们在左边看到堆上实例的一张柱状图,按照它们的类名字进行分组。每一个实例都有分配对象的数量,实例的大小(浅尺寸)和保留在内存中的对象大小。后者告诉我们,如果这些实例被释放,可以释放多少内存。这个视图非常重要,它让我们看到应用程序中内存占用的情况,帮助我们确认大型数据结构和对象关系。这些信息帮助我们构建更多高效的数据结构,解开对象连接以减少保留的内存,并最终尽可能地减少占用的内存。
查看柱状图,我们看到 MemoryActivity 有 39 个实例,对一个 Activity 而言这显得很奇怪。我们在右边选择其中一个实例,底部的引用树里会显示这个实例所有的引用。
其中一个是 ListenersManager 对象中数组的一部分。查看这个Activity 的其它实例,显示出它们都被这个对象保留下来。这就解释了为什么只有这个类的对象会占用这么多内存。
这种情况就是众所周知的“内存泄漏”。因为这些 Activity 被彻底销毁后,由于引用的关系,这些无用的内存不能作为垃圾被收集掉。避免这种情况的方法,就是确保对象不被比其它生命周期更长的其它对象引用。这种情况下,ListenManager 不应该在这个 Activity 被销毁后还保留这个引用。一种解决方法就是在 onDestory() 回调函数中,在这个Activity 被销毁时删除这个引用。
内存泄漏和其它在堆中占用大量空间的大型对象,会减少可用内存并频繁触发GC 事件尝试释放更多的空间。这些 GC 事件会让 CPU 很忙,结果降低了应用程序的性能。如果对应用程序而言没有足够数量的可用内存,而且堆也不能再增长,就会产生一个更为严重的后果 ——OutofMemoryException,会导致应用程序崩溃。
Eclipse 内存分析工具(Elicpse MAT)是一个更高级的工具:
这个工具可以做到 Android Studio 能做到的所有功能,还可以识别可能出现的内存泄漏,而且提供了更高级的实例查找方法,比如查找所有大于 2 MB 的 Bitmap 实例或者所有 Rect 空对象。
LeakCanary 函数库也是一个很好的工具,它可以追踪对象并确保它们不会泄漏。如果内存泄露了 —— 你将收到一个通知告诉你在哪里发生了什么。
分配跟踪器
在内存图中,可以通过左边的其它按钮来启动或停止分配跟踪器。它会生成当时所有被分配实例的报告,可以按照类分组:
或者按照方法分组:
它有很好的可视化效果,告诉我们最大的分配实例是什么。
通过这个信息,我们可以找到占用大量内存且对时序要求严格的方法。它可能会频繁触发 GC 事件。我们还可以找到大量生命周期很短的同一类型实例,这样可以考虑使用一个对象池来减少分配的数量。
常用的内存技巧
这里有一些我在编写代码时常用的小技巧和准则:
- 枚举是性能讨论中的热门主题。这里有一个相关视频,告诉我们枚举类型的大小,还有一个针对这个视频和其中一些误导的讨论。枚举是否会比常量更加占用空间?当然会。这很糟吗?未必。如果你正在编写一个函数库,需要强类型安全,使用这种方法会比其他方法好,比如 @IntDef。如果你只是需要把一堆常量需要汇总起来 —— 这种情况下使用枚举就不太明智。通常情况下,你在做决定的时候需要权衡利弊。
- 自动装箱 —— 自动装箱会自动把原始类型转换成它们的对象表示(比如int->Integer)。每当一个原始类型被“装箱”成一个对象,就创建了一个新的对象(我知道这让人很震惊)。如果我们有很多这样的情况 —— GC 就会频繁地运行。要留意到这些自动装箱的数量并不容易,因为每当一个原始类型给一个对象赋值时,这就自动发生了。尝试保持这些类型的一致性是一个解决方法。如果你在应用程序中使用这些原始类型,避免不要让它们无故被自动装箱。你可以使用内存性能分析工具找到表示一个原始类型的众多对象。你也可以使用Traceview 来查找 Interger.valueOf()、Long.valueOf()等。
- HashMap 与 ArrayMap 或 Sparse*Array 比较 —— HashMap 要求使用对象作为键值,就和自动装箱问题有关 。如果在应用程序中使用了原始的“int” 类型,它在和 HashMap 交互时会被自动装箱成 Interger,这种情况下其实只要使用 SparseIntArray 就好了。如果只是想用对象作为键值,可以使用 ArrayMap 类型。它和 HashMap 非常类似,但是底层的工作机理完全不同。它会更高效地使用内存,代价是速度比较慢。同 HashMap 相比上面两种替代方案占用的内存都比较小,但是花在检索项目和分配空间上的时间会比 HashMap 多一些。除非有 1000 个以上的项目,它们在执行时间上几乎没有什么差别,它们是你实现映射的可行选择。
- 上下文感知 —— 像前面看到的,Activity 中更有可能发生内存泄漏。Activity 是 Android 中最常见的内存泄漏(!),对此你可能并不会感到意外。它们也是非常昂贵的泄漏,因为它们里面包括了 UI 中所有的视图层级,这占用了很多的空间。平台上的很多操作都需要一个 Context 对象,通常用一个 Activity 来传递这些信息。要确保你理解了那个 Activity 上发生了什么。如果一个指向它的引用被缓存了,而且这个对象要比 Activity 生存时间长,若不清除这个引用,就会造成一个内存泄漏。
- 避免非静态内部类 —— 当你创建并实例化了一个非静态内部类,你就建造了一个指向外部类型的隐含引用。如果这个内部类的实例比外部类型存活的时间还要长,那即使不需要这个外部类型,它还是会被保存在内存中。例如,在Activity 类中创建了一个扩展 AsynTask 的非静态类型,开始处理异步任务,在运行过程中杀掉这个 Activity。只要这个异步任务运行时,它就会让这个 Activity 一直活着。解决方案 —— 请不要这样做,如果实在需要的话,就声明一个静态内部类。
GPU 性能分析
Android Studio 1.4 增加了一个新工具,可以对 GPU 渲染进行性能分析。
在 Android 窗口下进入 GPU 页面,你会看到一张图表,上面显示了绘制屏幕上每一帧所花费的时间:
图中的每一条线代表被绘制的一帧,不同颜色表示处理过程中的不同阶段:
- 绘图(蓝色)—— 代表 View#onDraw() 方法。这部分创建和更新了 DisplayList 对象,这些对象后续会被转换成 GPU 可以理解的 OpenGL 命令。比较高的值是由于复杂视图需要更多的时间来创建显示列表,或者有很多视图在很短的时间内失效了。
- 准备(紫色)—— 在 Lollipop (译者注:Android 的一个版本,也被简称为 Android L) 中,增加了另外一个线程来让 UI Thread 可以更快地绘制 UI。这个线程被称为 RenderThread。它负责将显示列表转换成 OpenGL 命令再发给 GPU。当处理这些的时候,UI 线程可以开始处理下一帧。这个步骤 UI 线程需要花时间把相关资源传递给 RenderThread。如果有很多资源需要传递,例如很多和大量的显示列表,这个步骤就会比较耗时。
- 处理(红色)—— 执行显示列表来创建 OpenGL 命令。如果需要执行很多和复杂的显示列表,这个步骤会花费较长的时间,因为有很多视图需要被重新绘制。当这个视图失效了,或者它被暴露在移动的重叠视图下,它都要被重绘。
- 执行(黄色)—— 发送 OpenGL 命令给 GPU。这部分是一个阻塞调用,因为 CPU 发送一个包含命令的缓存给 GPU,预期 GPU 返回一个干净的缓存用来处理下一帧。这些缓存的数量是有限的,如果 GPU 很忙—— CPU 会发现它要等待一个缓存被释放掉。因此如果我们看到在这个步骤看到较高的值,很可能说明 GPU 在忙着绘制 UI,这个 UI 太复杂很难在短时间完成。
在 Marshmallow中(译者注:Android 的一个版本,也被简称为 Android M),增加了更多颜色可以代表更多步骤,比如量测和布局,输入处理和其它的功能:
编辑于2015/09/29:一位来自 Google 的框架工程师,John Reck,增加了新颜色的相关信息:
“动画” 的确切定义是指每一个向 Choreographer 注册为 CALLBACK_ANIMATION 的东西。包含 Choreographer#postFrameCallback and View#postOnAnimation,它们被用在 view.animate()、ObjectAnimator、Transition等等上面,它和 systrace 的“动画”标签是同一个东西。
“misc”是指 vsync 和当前时间标签的延迟。如果你曾经从 Choreographer 的日志中看到过类似“错过 vsync 多少多少毫秒跳过了多少多少帧” 的信息,这些现在都被标记为“misc”。在统计帧的转储中 INTENDED_VSYNC 和 VSYNC 是不同的。(https://developer.android.com/preview/testing/performance.html#timing-info)
但使用这个功能前,你需要先在开发者选项中打开 GPU 渲染这个选项:
这个工具被允许使用 ADB 命令以获取它需要的所有信息,当然对我们也很有用!使用如下命令:
adb shell dumpsys gfxinfo <PACKAGE_NAME>
我们可以收到这些数据并创建下面这张图表。这个命令还会打印其它有用的信息,比如层级中有多少视图,整个显示列表的大小等等。在 Marshmallow 中我们可以获得更多的统计信息。
如果应用程序有相应的自动化 UI 测试,可以在某些交互操作后(列表滑动和大量的动画等),在服务器上运行这个命令来观察这些值是否会随着时间而变化,比如“Janky Frames”。这会帮助我们在某些提交推入后定位一个性能下降的问题,让我们有时间在应用程序面世前解决掉这个问题。使用“framestats”这个关键词,我们可以获得更加详细的绘制信息,可以参考这里。
但不是只有观察图表这样一种方式!
在“Profile GPU Rendering” 开发者选项中,还有一个“On Screen as bars”选项。打开这个选项后,屏幕上每个窗口都显示图表,上面有一个绿线代表 16 毫秒的门限值。
在右面的例子里,我们看到有些帧超出了绿线,这说明绘制这些帧的时间超过了 16 毫秒。因为这些线条中的大部分是蓝色,我们认为有很多或复杂的视图需要绘制。在这个场景下,我滑动新闻供应列表,它里面有不同类型的视图。有些视图已经失效,有些绘制时会更加复杂。有些帧超过门限值可能因为是这个时间内正好有一个复杂的视图要绘制。
层级观察器
我爱死这个工具了,但让我失望的是大部分人根本不使用它!
使用层级观察器,我们可以得到性能的统计信息、观察屏幕上完整的视图层级和访问所有这些视图的属性。单独使用层级观察器,你还可以转储主题的数据,观察每一个样式的属性值,但 Android Monitor 上做不到这一点。我在进行布局设计和优化时会使用这个工具。
在中间,我们看到一个代表视图层级的树。这个视图层级可以很宽,但如果它太深(大概 10 个层级),在布局和量测阶段就会花费很多时间。每次用 View#onMeasure() 中测量一个视图,或者在 View#onLayout() 中定位所有的子视图,这些命令都会传递给子视图,子视图也会做同样的事情。有些布局的每个步骤会执行两次,比如 RelativeLayout 和一些 LinearLayout 配置,如果它们是嵌套的,传递次数就会呈指数增加。
在底部右侧,我们看到一个布局的“设计图”,上面显示了每个视图的位置。我们在这里或者在上面的树中选择一个视图,可以在左边观察它的属性。设计布局时,我有时候不确定为什么一个特定的视图会在那里结束。通过这个工具,我可以在树中追踪它,选择它并观察它在前面窗口的位置。通过查看视图在屏幕上的最终尺寸,我可以设计有趣的动画,还可以使用这些信息准确地移动东西。我可以找到那些被其它视图无意覆盖而看不到的视图,以及更多信息。
对每一个视图和它的子视图,我们都有量测、布局和绘制它们所花费的时间。颜色表明了这个视图和其他视图相比性能如何,很容易通过这个方式找到最薄弱的环节。因为我们还看到这个视图的预览,可以仔细检查这个树并按照创建它的步骤,找到多余的步骤并移除掉。这其中有一个东西对于性能会有很大的影响,那就是过度绘制(Overdraw)。
过度绘制
如同在 GPU 性能分析部分所看到的 —— 如果 GPU 有很多东西要画到屏幕上,增加了绘制每一帧的时间,这样图表中黄色所代表的执行阶段就要花费较长时间才能完成。当我们在其它东西的上面画东西时,就会出现过度绘制,比如说一个红色背景上的黄色按键。GPU 需要先绘制红色背景然后在上面画黄色按键,这样过度绘制就不可避免了。如果有很多过度绘制的层,它会造成 GPU 很忙并且很难达成 16 毫秒的要求。
通过使用开发选项中的 “Debug GPU Overdraw”设定,所有过度绘制会变成不同的颜色来表明这个区域过度绘制的严重程度。有 1 倍或 2 倍的过度绘制还好,甚至有些小的浅红色区域也不算太坏,但是如果我们在屏幕上看到很多红色 —— 那可能就有麻烦了。让我们看些例子:
左边的例子里,有一个被画成绿色的列表,这通常还好,但是顶部有一个覆盖把它变成红色,这就开始有问题了。右边的例子里,整个列表都是浅红色。这两个例子中都有一个不透明列表,存在2倍或3倍的过度绘制。如果在 Activity 或者 Fragment 的窗口中有一个全屏的背景色,列表和其中每一个栏位的视图都可能会出现过度绘制。我们可以通过只为它们中的一个设置背景色来解决这个问题。
注意:默认主题为窗口声明了一个全屏的背景色。如果一个 Activity 上有一个不透明的布局覆盖了整个屏幕,去除这个窗口的背景就可以减少一层过度绘制。这可以在主题或代码中通过在 onCreate() 中调用 getWindow().setBackgroundDrawable(null)实现。
使用层级观察器,你可以将层级中的所有分层导出来,并生成一个可用 Photoshop 打开的 PSD 文件。在 Photeshop 中查看不同层级,就可以展示出布局中所有的过度绘制。通过这些信息可以减少多余的过度绘制,不要在绿色上止步不前,争取做到蓝色界面的效果!
Alpha
使用透明特性可能会有隐含的性能问题,想要理解为什么 —— 让我们看一下给一个视图设定 alpha 值会发生什么。考虑下面这个布局:
这个布局中有三个 ImageView,一个叠在另一个上面。通过 setAlpha() 可以直接和简单地设定一个 alpha值,这个命令会在传递到 ImageView的子视图上。后面这些 ImageView 被设置成那个 alpha 值绘制到帧缓存上。结果就是:
这不是我们想看到的。
因为每个 ImageView 都用一个 alpha 值绘制,所有重叠的图像都混在一起。幸运的是,操作系统有方法解决这个问题。布局会被复制到一个离屏缓存,这个 alpha 值会被应用到整个缓存上,然后再把它复制到帧缓存。结果是:
但是……我们还是付出了代价。
在把视图绘制到帧缓存之前,先在离屏缓存上绘制这个视图,实际上是增加了另一个未被发现的过度绘制分层。操作系统不确认什么时侯使用这种方法,或者之前展示的直接方法,所以总是默认选择复杂的那个。但是还是有些方法设置 alpha 值并避免加入复杂的离屏缓存。
- 文本视图(TextView)—— 用 setTextColor() 代替 setAlpha() 方法。文本颜色如果使用 alpha 通道,会导致直接用 alpha 绘制文本。
- 图像视图(ImageView)—— 用 setImageAlpha() 代替 setAlpha() 方法。原因同文本视图。
- 自定义视图 —— 如果你自定义的视图不支持视图重叠,这个复杂的行为就和我们无关。那就没有办法,如上面例子所示,子视图会混在一起。通过重载 hasOverlappingRendering() 方法并返回错误,我们可以通知系统对视图采用直接和简单的通道。通过重载 onSetAlpha() 方法并返回正确,我们就有一个选择来手动处理设定一个 alpha 值会发生什么。
硬件加速
硬件加速在 Honeycomb (译者注:Android H版本)上被引入,我们有一个新的绘制模型用来在屏幕上呈现应用程序。DisplayList 这个数据结构被引入,它用来记录视图绘制的命令以便快速呈现。但是有另外一个很好的特性,开发者没有留意或者没有正确地使用 —— 就是视图分层。
使用视图分层,我们可以在一个离屏缓存上绘制视图(之前看到过,应用在一个 Alpha 通道上)并且可以随意处理。这个特性主要用于动画,因为我们可以快速地动画绘制复杂的视图。没有分层,动画绘制一个视图需要在改变动画属性(比如,X 坐标、缩放和 alpha 值等)后,让这个视图失效。对于复杂的视图,这个失效会传播到所有的子视图,它们也会被重绘,这个操作的开销很大。通过使用硬件支持的视图分层,GPU 会创建视图的一个纹理。有一些可以应用在纹理上的操作,不需要让它失效,比如 X 和 Y坐标位置、旋转、alpha等等。这意味着我们可以在屏幕上动画绘制一个复杂视图而不用在过程中让它失效!这使得动画更加流畅。这里有一段示例代码:
// 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();
很简单,对吗?
当然,但是在使用硬件层时需要牢记一些事情:
- 使用视图后清理 —— 硬件层在内存有限的模组(GPU)上占用了一定的空间。只在需要的时候去尝试使用它们,好比动画,使用完后再把它们清理掉。在上面的 ObjectAnimator例子中,我添加了一个监听程序用来在动画结束后移除分层。在 Property 动画的例子中,我使用了 withLayers 方法,这个方法在开始时自动创建分层,动画结束后就移除掉。
- 如果你在采用一个硬件层改变视图,这样会让硬件层失效并在离屏缓存上全部重绘这个视图。当改变一个不能被硬件层优化的属性时,就会发生这些(目前,如下这些是可以优化的:旋转、缩放、X/Y转换、旋转运动和 alpha)。例如,你正在利用硬件层动画绘制一个视图,在屏幕上移动它的同时更新视图的背景色,这会导致硬件层的持续更新。更新硬件层的开销很大,这种情况下使用它划不来。
第二个问题是让这些硬件层的更新变得可视化。使用开发者选项,我们可以打开 “Show hardware layers updates” 选项。
打开这个选项后,当视图更新硬件层时,视图会变成绿色闪一下。我之前曾经用过它,当时有一个 ViewPage 滑动得不如我期望得流畅。打开这个选项后,我继续滑动 ViewPager,看到下面这些:
在整个滑动中两个页面都变绿了!
这说明这两个页面创建了一个硬件层,当滑动 ViewPager 时,它们都失效了。当滑动这些页面时,我通过在背景上运用视差效果和逐渐动画绘制页面上的项目来更新页面。我并没有为 ViewPager 页面创建一个硬件层。阅读 ViewPager 源代码后,我发现当用户开始滑动时,就为两个页面创建了一个硬件层并在滑动停止后移除了这个分层。
结语
由于篇幅限制,文档的详解资料太全面,细节内容太多,所以只把部分知识点截图出来粗略的介绍,每个小节点里面都有更细化的内容!以下是目录截图:
由于整个文档比较全面,内容比较多,篇幅不允许,下面以截图方式展示 。
再附一部分Android架构面试视频讲解:
网上学习资料一大堆,但如果学到的知识不成体系,遇到问题时只是浅尝辄止,不再深入研究,那么很难做到真正的技术提升。
一个人可以走的很快,但一群人才能走的更远!不论你是正从事IT行业的老鸟或是对IT行业感兴趣的新人,都欢迎加入我们的的圈子(技术交流、学习资源、职场吐槽、大厂内推、面试辅导),让我们一起学习成长!
网上学习资料一大堆,但如果学到的知识不成体系,遇到问题时只是浅尝辄止,不再深入研究,那么很难做到真正的技术提升。
一个人可以走的很快,但一群人才能走的更远!不论你是正从事IT行业的老鸟或是对IT行业感兴趣的新人,都欢迎加入我们的的圈子(技术交流、学习资源、职场吐槽、大厂内推、面试辅导),让我们一起学习成长!