在 Android Profiler 提供的这四种性能数据中,绝大多数场景下我们都更关心 CPU 和内存的使用情况。本文将介绍对应的两种分析工具 —— Memory Profiler 和 CPU Profiler。
Memory Profiler
许多开发者使用 Memory Profiler,是希望发现和定位内存泄漏问题。在介绍 Memory Profile 如何解决这一问题之前,我想先明确 “内存泄漏” 这一概念。无论您当前是否了解内存泄漏,都将帮助我更好地解释 Memory Profile 的工作原理。
内存泄漏
什么是内存泄漏?
通常我们认为,在运行的程序中,如果一个无法访问的对象却仍然占用着内存空间,即为此对象造成了内存泄漏。如果您使用过 C 语言或 C++ 的指针,您会很熟悉这个概念。
但是在 Kotlin 和 Java 的世界中,事情有些许不同。因为这两种语言是运行在 Java 虚拟机 (JVM) 中的。在 JVM 中,有个重要的概念,就是垃圾回收 (GC)。当垃圾回收运行时,虚拟机会首先识别 GC Root。GC Root 是一个可以从堆外部访问的对象,它可以是本地变量或运行中的线程等。虚拟机会识别所有可以从 GC Root 访问的对象,它们将会被保留。而其他无法从 GC root 访问的对象,则会被认为是垃圾并回收掉。
所以,一般意义上的内存泄漏在 JVM 中并不存在。在 JVM 中的内存泄漏通常是指: 内存中含有那些再也不会被使用、但是仍然能够访问的对象。
Activity 和 Fragment 泄漏检测
在 Android 应用中,应当尤为警惕 Activity 和 Fragment 对象的泄漏,因为这两种对象通常都会占用很多内存。在 Android 3.6 中,Memory Profiler 加入了自动检查 Activity 和 Fragment 中的内存泄漏的功能。使用这一功能非常的简单:
- 首先,您需要在 Memory Profiler 中保存 Heap Dump,点击下图所示按钮:
- 在 Heap Dump 加载完成后,勾选 “Activity/Fragment Leaks” 选框:
此时如果有检查到 Activity 或 Fragment 的泄漏,就会在界面中显示出来。
Memory Profiler 通过以下几种场景来判断泄漏是否发生:
- 当我们销毁了一个 Activity 的实例后,这个实例就再也不会被使用了。此时如果仍然有这个 Activity 的引用,Memory Profiler 就会认为它已经泄漏;
- Fragment 的实例应当与一个 Fragment Manager 相关联,如果我们看到一个 Fragment 没有关联任何一个 Fragment Manager,而且它依然被引用时,也可以认为有泄漏发生。
不过要注意的是,针对 Fragment 有个特别的情况: 如果您载入的 Heap Dump 的时机,刚好介于 Fragment 被创建和被使用的时间之间,就会造成 Memory Profiler 误报;相同情况也会发生在 Fragment 被缓存但是没有被复用的时候。
其他内存泄漏检测
Memory Profiler 也可以用于检查其他类型的泄漏,它提供了许多信息,用于帮助您识别内存泄漏是否发生。
当您拿到一段 Heap Dump 之后,Memory Profiler 会展示出类的列表。对于每个类,“Allocation” 这一列显示的是它的实例数量。而在它右边则依次是 “Native Size”、“Shallow Size” 和 “Retained Size”:
这几组数据分别意味着什么呢?下面我会通过一个例子来说明。
我们用下图来表示某段 Heap Dump 记录的应用内存状态。注意红色的节点,在这个示例中,这个节点所代表的对象从我们的工程中引用了 Native 对象:
这种情况不太常见,但在 Android 8.0 之后,使用 Bitmap 便可能产生此类情景,因为 Bitmap 会把像素信息存储在原生内存中来减少 JVM 的内存压力。
先从 “Shallow Size” 讲起,这列数据其实非常简单,就是对象本身消耗的内存大小,在上图中,即为红色节点自身所占内存。
而 “Native Size” 同样也很简单,它是类对象所引用的 Native 对象 (蓝色节点) 所消耗的内存大小:
“Retained Size” 稍复杂些,它是下图中所有橙色节点的大小:
由于一旦删除红色节点,其余的橙色节点都将无法被访问,这时候它们就会被 GC 回收掉。从这个角度上讲,它们是被红色节点所持有的,因此被命名为 “Retained Size”。
还有一个前面没有提到的数据维度。当您点击某个类名,界面中会显示这个类实例列表,这里有一列新数据 —— “Depth”:
“Depth” 是从 GC Root 到达这个实例的最短路径,图中的这些数字就是每个对象的深度 (Depth):
一个对象离 GC Root 越近,它就越有可能与 GC Root 有多条路径相连,也就越可能在垃圾回收中被保存下来。
以红色节点为例,如果从其左边来的任何一个引用被破坏,红色节点就会变成不可访问的状态并且被垃圾回收回收掉。而对于右边的蓝色节点来说,如果您希望它被垃圾回收,那您需要把左右两边的路径都破坏才行。
值得警惕的是,如果您看到某个实例的 “Depth” 为 1 的话,这意味着它直接被 GC root 引用,同时也意味着它永远不会被自动回收。
下面是一个示例 Activity,它实现了 LocationListener 接口,高亮部分代码 “requestLocationUpdates” 将会使用当前 Activity 实例来注册 locationManager。如果您忘记注销,这个 Activity 就会泄漏。它将永远都待在内存里,因为位置管理器是一个 GC root,而且永远都存在:
您能在 Memory Profiler 中查看这一情况。点击一个实例,Memory Profiler 将会打开一个面板来显示谁正在引用这个实例:
我们可以看到位置管理器中的 mListener 正在引用这个 Activity。您可以更进一步,通过引用面板导航至堆的引用视图,它可以让您验证这条引用链是否是您所预期的,也能帮您理解代码中是否有泄漏以及哪里有泄漏。
CPU Profiler
和 Memory Profiler 类似,CPU Profiler 提供了从另一个角度记录和分析应用关键性能数据的方法。
使用 CPU Profiler,首先要产生一些 CPU 的使用记录:
- 进入 Android Studio 中的 CPU Profiler 界面,在您的应用已经部署的前提下,点击 “Record” 按钮;
- 在应用中进行您想要分析的操作;
- 返回 CPU Profiler,点击 “Stop” 按钮。
由于最终呈现的数据是基于线程组织的,所以去观察数据之前,您应该确认是否选择了正确的线程:
我们这里所获得的 CPU 使用记录信息,其实是一个 System Trace 实例的调用栈集合 (下文统称 “调用栈”)。而就算是很短的 CPU 使用记录,也会包含巨量的信息,同时这些信息也是人无法读懂的。所以 CPU Profiler 提供了一些工具来可视化这些数据。
Call Chart
在 CPU Profiler 界面下半部,有四个标签页,分别对应四个不同的数据图表,它们分别是: Call Chart、Flame Chart、Top Down 和 Bottom Up。其中的 Call Chart 可能是最直白的一个,它基本上就是一个调用栈的重新组织和可视化呈现:
Call Chart 横轴就是时间线,用来展示方法开始与结束的确切时间,纵轴则自上而下展示了方法间调用和被调用的关系。Call Chart 已经比原数据可读性高很多,但它仍然不方便发现那些运行时间很长的代码,这时我们便需要使用 Flame Chart。
Flame Chart
Flame Chart 提供了一个调用栈的聚合信息。与 Call Chart 不同的是,它的横轴显示的是百分比数值。由于忽略了时间线信息,Flame Chart 可以展示每次调用消耗时间占用整个记录时长的百分比。同时纵轴也被对调了,在顶部展示的是被调用者,底部展示的是调用者。此时的图表看起来越往上越窄,就好像火焰一样,因此得名:
Flame Chart 是基于 Call Chart 来重新组织信息的。从 Call Chat 开始,合并相同的调用栈,以耗时由长至短对调用栈进行排序,就获得了 Flame Chart:
对比两种图表不难看出,左边的 Call Chart 有详细的时间信息,可以展示每次调用是何时发生的;右边的 Flame Chart 所展示的聚合信息,则有助于发现一个总耗时很长的调用路径:
Top Down Tree
前面介绍的两种图表,可以帮助我们从两种角度纵览全局。而如果我们需要更精确的时间信息,就需要使用 Top Down Tree。在 CPU Profiler 中,Top Down 选项卡展示的是一个数据表格,为了便于理解其中各组数据的意义,接下来我们会尝试构建一个 Top Down Tree。
构建一个 Top Down Tree 并不复杂。以 Flame Chart 为基础,您只需要从调用者开始,持续添加被调用者作为子节点,直到整个 Flame Chart 被遍历一遍,您就获得了一个 Top Down Tree:
对于每个节点,我们关注三个时间信息:
- Self Time —— 运行自己的代码所消耗的时间;
- Children Time —— 调用其他方法的时间;
- Total Time —— 前面两者时间之和。
有了 Top Down Tree,我们能轻易将这三组信息归纳到一个表格之中:
下面我们来看一看这些时间信息是怎么计算的。左手边是和前面一样的 Flame Chart 示例。右边则是一个 Top Down Tree。
自我介绍一下,小编13年上海交大毕业,曾经在小公司待过,也去过华为、OPPO等大厂,18年进入阿里一直到现在。
深知大多数初中级Android工程师,想要提升技能,往往是自己摸索成长或者是报班学习,但对于培训机构动则近万的学费,着实压力不小。自己不成体系的自学效果低效又漫长,而且极易碰到天花板技术停滞不前!
因此收集整理了一份《2024年Android移动开发全套学习资料》,初衷也很简单,就是希望能够帮助到想自学提升又不知道该从何学起的朋友,同时减轻大家的负担。
既有适合小白学习的零基础资料,也有适合3年以上经验的小伙伴深入学习提升的进阶课程,基本涵盖了95%以上Android开发知识点,真正体系化!
由于文件比较大,这里只是将部分目录截图出来,每个节点里面都包含大厂面经、学习笔记、源码讲义、实战项目、讲解视频,并且会持续更新!
如果你觉得这些内容对你有帮助,可以扫码获取!!(备注:Android)
![](https://i-blog.csdnimg.cn/blog_migrate/5d450f429b02b324e3a227c3427f2acf.jpeg)
如何做好面试突击,规划学习方向?
面试题集可以帮助你查漏补缺,有方向有针对性的学习,为之后进大厂做准备。但是如果你仅仅是看一遍,而不去学习和深究。那么这份面试题对你的帮助会很有限。最终还是要靠资深技术水平说话。
网上学习 Android的资料一大堆,但如果学到的知识不成体系,遇到问题时只是浅尝辄止,不再深入研究,那么很难做到真正的技术提升。建议先制定学习计划,根据学习计划把知识点关联起来,形成一个系统化的知识体系。
学习方向很容易规划,但是如果只通过碎片化的学习,对自己的提升是很慢的。
同时我还搜集整理2020年字节跳动,以及腾讯,阿里,华为,小米等公司的面试题,把面试的要求和技术点梳理成一份大而全的“ Android架构师”面试 Xmind(实际上比预期多花了不少精力),包含知识脉络 + 分支细节。
在搭建这些技术框架的时候,还整理了系统的高级进阶教程,会比自己碎片化学习效果强太多。
网上学习 Android的资料一大堆,但如果学到的知识不成体系,遇到问题时只是浅尝辄止,不再深入研究,那么很难做到真正的技术提升。希望这份系统化的技术体系对大家有一个方向参考。
《互联网大厂面试真题解析、进阶开发核心学习笔记、全套讲解视频、实战项目源码讲义》点击传送门即可获取!
网上学习 Android的资料一大堆,但如果学到的知识不成体系,遇到问题时只是浅尝辄止,不再深入研究,那么很难做到真正的技术提升。希望这份系统化的技术体系对大家有一个方向参考。
《互联网大厂面试真题解析、进阶开发核心学习笔记、全套讲解视频、实战项目源码讲义》点击传送门即可获取!