Android 内存性能优化,灵魂一问 - 如何彻底防止 APK 反编译
为了确保 Android 系统的每个应用都有足够的内存,Android 系统需要有效地管理内存分配,当内存不足的时候,就会触发 GC,GC 采用的垃圾标记算法为根搜索算法。
如果根能到达的对象就是可达对象,不可达的对象就是回收对象。
内存泄漏就是指没有用的对象到 GC Roots 是可达的(对象被引用),导致 GC 无法回收该对象。此时,如果一个对象没有任何用处,但是它可达,如下图所示:
比如上图中的 Obj3,它是 GC Roots 可达的,所以标记算法并不会把 Obj3 标记为回收对象,但是在项目中,Obj3 并没有被引用。
那这个时候 Obj3 就会造成内存泄漏。
内存泄漏产生的原因一般有三个:
由开发人员自己编码造成的泄漏
第三方框架造成的泄漏
由 Android 系统或第三方 ROM 造成的泄漏
在通常情况下,第二种和第三种情况对于 Android 应用开发者来说是不可控的,但是第一种情况是可控的,既然是可控的,我们就要尽量在编码时避免造成内存泄漏。
之前我写过一篇 blog,较为浅层的解析了以下内存泄漏的问题:Android内存泄漏问题
这里面分讲解了好几个场景以及代码示例,所以这边一些代码上的东西就不会再细纠了,下面列出来这些场景:
非静态内部类的静态实例
非静态内部类会持有外部类实例的引用如果非静态内部类的实例是静态的,就会间接长期维持着外部类的引用,阻止被系统回收。
多线程相关的匿名内部类/非静态内部类
匿名内部类也会持有外部类实例的引用。多线程相关的类有 AsyncTask、Thread 和实现 Runnable 接口的类等。
它们的匿名内部类/非静态内部类如果做耗时操作就可能发生内存泄漏。
Handler 内存泄漏
Handler 的 Message 被存储在 MessageQueue 中,有些 Message 并不能马上被处理,它们在 MessageQueue 中存在的时间会很长,这就会导致 Handler 无法被回收。如果 Handler 是非静态的,则 Handler 也会导致引用它的 Activity 或者 Service 不能被回收。解决方法有两个:
(1)一个是使用静态的 Handler 内部类,Handler 持有的对象要使用弱引用
(2)在 Activity 的 donDestroy 去 移除 MessageQueue 中的消息通过使用: removeCallbacksAndMessages(null)
未正确使用 Context
=对于不是必须使用 Activity 的 Context 的情况,我们可以考虑使用 Application Context 来代替 Activity 的 Context,这样可以避免 Activity 的泄漏,比如单例模式。
静态 View
使用静态 View 可以避免每次启动 Activity 都去读取并渲染 View,但是静态 View 会持有 Activity 的引用,导致 Activity 无法被回收,解决办法就是在 onDestory 中将静态 View 置位 null。
WebView
不同 Android 版本的 WebView 会有差异,加上不同厂商定制的 ROM 的 WebView 的差异,这就导致 WebView 存在着很大的兼容性问题。WebView 都会存在内存泄漏的问题,在应用中只要使用一次 WebView,内存就不会被释放掉。通常解决办法是为 WebView 单开一个进程,使用 AIDL 与应用的主进程进行通信。然后这个进程可以在适当的时机销毁。
资源对象未关闭
资源对象比如 Cursor、File 等,往往都使用了缓冲,会造成内存泄漏。因此,在资源对象不使用时,一定要确保它们已经关闭并将它们的引用置为 null。通常都在 finally 语句中进行关闭,防止出现异常时,资源未被释放的问题。
集合中对象没有清理
通常把一些对象的引用加入到了集合中,当不需要该对象的时候,如果没有把他它的引用从集合中清理掉,这样这个集合就会越来越大。如果这个集合是 static 的话,那情况就会更加严重。
Bitmap 对象
临时创建的某个相对比较大的 Bitmap 对象,在经过变化得到新的 Bitmap 后,应该尽快回收原始的 Bitmap,这样能够更快释放原始 Bitmap 所占的空间,避免静态变量持有比较大的 Bitmap 对象或者其他大的数据对象。
监听器未关闭
很多系统服务需要 register
和 ungister
监听器,我们需要确保在合适的时候及时unregister
那些监听器。自己手动添加的 Listener 要记得在合适的时候及时移除这个 Listener
===================================================================================
这里就开始讲解内存分析的工具,本节就来讲解 Memory Monitor。
在 AS 中Android Monitor
是一个主窗口,它包含了 Locat、Memory Monitor,CPU Monitor、GPU Monitor 和 Network Monitor。
但是在 Android Stduio3.0 后,AS 优化了 Android Monitor,把 Android Moniter 的东西整合到了 Profiler
中,在 Android Studio 中连接手机然后打开应用,我们在 Profiler 中可以去查看这个窗口:
可以看到它分为四大块,CPU、MEMORY(内存)、NETWORK(网络)、ENERGY(电量)。
其中 Memory Monitor 可以轻松监视应用程序的性能和内存使用情况,以便找到被分配的对象,定位内存泄漏,并跟踪连接设备中正在使用的内存数量,我们点开 MEMORY,它的界面是这样的:
Memory Monitor 可以报告出你的应用程序的内存分配情况,更形象地呈现出引用程序使用的内存。
它的作用如下:
实时显示可用的和分配的 Java 内存的图表
实时显示 GC 事件
启动垃圾收集事件
快速测试应用程序的缓慢是否和过度的垃圾收集事件有关
快速测试应用程序崩溃是否与内存耗尽有关
而在上面的截屏中我标注了几个常用的该工具的功能,按照标注解释如下:
Force garbage collection
手动触发 GC
Dump Java Heap
保存内存快照
Allocation Tracking
后面解释
Total
当前应用总共使用的内存
Allocated
系统为当前应用分配的内存,图中标识 N/A,说明应用没被限制使用内存
其中横轴是时间,纵轴是分配的内存。
下图是一个大内存分配的场景--------分配得内存在短时间内急速上升:
我们需要判断这次是否是合理分配的内存,是 Bitmap 还是其他的大数据,并且对这种大数据进行优化,减少内存开销。
接下来分配得内存突然下降,这表示发生了 GC,用来释放内存。
内存抖动一般是指 在很短的时间内发生了多次的内存分配和释放:
严重的内存抖动会导致应用卡顿。
内存抖动出现的原因是在短时间内频繁的创建对象(比如在循环中创建很大的对象),内存为了应对这种情况,也会频繁的进行 GC,其他线程都会被挂起,等待 GC 操作完成后恢复工作。
如果是频繁的 GC 就会产生大量的暂停时间,这会导致界面绘制时间减少,从而使得多次绘制一帧的时长超过 16ms,最后导致界面卡顿。
如果出现了内存抖动,我们要查看我们的 循环、onDraw 等会被执行多次的函数体。
因为 new
一次对象是个耗时耗空间的操作,值得引起重视。
=======================================================================================
Allocation Tracker 用来跟踪内存分配,它允许你在执行某些操作的同时监视在何处分配对象,了解这些分配使你能够调整与这些操作相关的方法调用,用来优化程序性能和内存使用。
Allocation Tracker 能够做到如下的事情:
显示代码分配对象类型、大小、分配线程、堆栈跟踪的时间和位置
通过重复的分配/释放模式帮助识别内存的变化
当与 HPROF Viewer 结合使用时,可以帮助你跟踪内存泄漏。例如,你在堆上看到一个 Bitmap 对象,你可以使用 Allocation Tracker 来找到其分配的位置。
我们在柱状图中,随便的截取一段,就能产生内存的分析:
我们能够看到一些对象,它总共的个数、大小,我们还可以根据类、包和线程来进行排序。
这些对象,分为可控和不可控两种类型:
不可控
一些对象是必然会产生的,是伴随着一些对象的产生而产生,并不是我们直接创建,比如我们必须的创建一个对象,而这个对象的构造函数里面会必然的形成一个 int[]
或 String
或其他,在一般情况下,我们不会去考虑这些 int[]
、String
变量
可控
由我们直接的,手动的创建出来,比如通过 new
等声明一个变量,它是在我们程序中直观的表示的出来的。
在上面的图片中,我们随便点击一个对象,如果这个对象是可控的,那么我们就能追溯到代码中,如下图:
图中,我选取了一个 LinearLayout
(标注 1),可以看到 LinearLayout 在程序中有多个 Instance(标注 2),也就是说在我们程序中写了很多个 LinearLayout 出来,然后我们随便点击一个一个,可以看到它创建的代码栈(标注 3),然后我们在代码栈中,可以找到我们在程序中创建出来的地方(标注 4)。
可以看到我是在一个RecyclerView
的Adapter
里面去 new 出了一个 item,我的每一个 Item 的构造就是一个 LinearLayout,而一个 RecyclerView 里面是有多个 item,这也是为什么,在标注 2 中显示我在很短的时间里面创建出了这么多的 LinearLayout。
Heap Dump 的主要功能就是查看不同的数据类型在内存中的使用情况。
它可以帮助我们找到大对象,也可以通过数据的变化发生内存泄漏。
我们通过点击 Dump Java Heap
或者 Crtl + D
来捕获一段 Heap Dump:
可以看到其分析原理也差不多,不过它可以通过点击最左边的图标的保存文件,保存成一个 .hprof
文件,便于我们后面使用 MAT 工具进行分析。
我们可以看到这个图和之前分析内存那个有点不一样,这个图最右边的 Refernces
表示的是该对象的信息:
Depth
GC Roots 到达该对象的层数。
也就说 GC Roots 构成的树里面,这个对象在第 n 层
Native Size
C/C++层中的内存大小(B)
Shallow Size
这个对象在 Java 层中的内存大小(B)
Retained Size
这个类中所引用到的对象的总大小(B)
分析你的 heap,按照一下步骤.
浏览
Class Name
列表,看看有没有大量对象存在,并且这些对象你认为是不应该存在的,可能存在内存泄漏的情况. 点击类名可以看到详细的对象信息.在这个
Instance View
面板中,点击一个实例References
面板就会显示出来,里面都是使用该 Instance 的 Reference,点击箭头可以看到引用它的所有区域。点击鼠标右键可以选择go to instance
去看到引用该引用的引用,或者jump to source
去看调用的源代码.
==============================================================================
在上面的 Heap Dump 中我们提到了一个 .hprof
文件,在我们分析内存的时候,我们有时候无法很确定内存泄漏的地方,我们会通过 Heap Dump 来生成一个 疑似发生内存泄漏时所产生的 堆存储文件,它就是 .hprof
文件。
而MAT
就是用来分析 .hprof
文件的。
MAT 的全称为 Memory Analysis Tool,是对内存进行详细分析的工具,它是 Eclipse 的插件,AS 需要单独下载。
下载地址在 MAT官网下载网址
我们打开来运行一下刚刚生成的 .hprof
文件,可能报以下的错误:
...Unknown HPROF Version (JAVA PROFILE 1.0.3)..
这个意思的是我们由 Android 虚拟机转出来的.hprof
文件和 MAT 所分析的 .hprof
(用 JVM)的格式不一样,我们需要转化文件格式。
我们先通过命令行,进入到 该.hprof
所在目录,比如我有一个叫做 android.hprof
由 Android 导出来的 ,然后执行下面命令:
hprof-conv android.hprof mat.hprof
就会在该目下出现一个 mat.hprof
的文件,我们就可以用这个文件导入啦:
下面我们来举个实例,来介绍怎么使用 MAT。
我们先准备一份内存泄漏的代码出来:
public class MainActivity extends AppCompatActivity {
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
LeakThread leakThread = new LeakThread();
leakThread.start();
}
class LeakThread extends Thread {
@Override
public void run() {
try {
Thread.sleep(60 * 60 * 1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
上面做了一个 非静态内部类 LeakThread 持有外部 MainActivity 的引用,LeakThread 中做了耗时操作,导致 MainActivity 无法被释放。
接着我们运行它,产生一个 .hprof 文件。然后我们打开 Profiler,横竖屏切换,然后导出一个 .hprof 文件,然后通过 hprof-cov 转换它,最后拿转换出来的文件导入到 MAT 中。
用 MAT 打开 hprof 文件后,MAT 就会产生一个报告,分为两个标签页,分别是 Overview
和 Leak Suspects
。
在 Leak Suspects 中给出了 MAT 认为可能出现内存泄漏问题的地方,在上图中共给出了 2 个内存泄漏的猜想,通过单击每个猜想的 Details 可以看到更深入的分析情况。如果内存泄漏不是特别明显,通过 Leak Suspects 很难发现内存泄漏的位置。
打开 Overview 标签页,首先看到的是一个饼状图:
它主要用来显示内存的消耗,饼状图的彩色区域代表被分配的内存,灰色区域表示空闲内存区。
再往下看,能看到饼状图下面有 Actions
:
其中分析内存泄漏最常用的就是 Histogram
和 Dominator Tree
。下面我来介绍它怎么使用。
Dominator Tree
的意思是支配树,它更善于去分析对象的引用关系,如下图所示:
左边主要是 Class Name 类名,右边是三列之前都看到过:
Shallow Heap
对象自身占用的内存大小,不包括它引用的对象。如果是数组类型的对象,它的大小由数组元素的类型和数组长度决定。如果是非数组类型的对象,它的大小由其成员变量的数量和类型决定。
Retained Heap
一个对象的Retained Set
包含对象所占内存的总大小。
换句话说,Retained Heap 就是当前对象被 GC 后,从 Heap 上总共能释放掉都少的内存。
Retained Set 指的是这个对象本身和它持有引用的对象以及这些对象的Retained Set
所占内存大小的总和,引用树官方的图解如下图:
从上图可以看出 E 的Retained Set
为 E 和 G,C 的Retained Set
为 C、D、E、F、G、H。
MAT 所定义的支配树就是从上图中的树演化而来。
在引用树中,如果一条到 Y 的路径必然会经过 X,则称为 X 支配 Y。X 直接支配 Y 则指的是在所有支配 Y 的对象中,X 是 Y 最近的一个对象。支配树反映的就是这种直接支配关系,在支配树中,父节点直接支配子节点。
下图就是官方提供的一个从引用树到支配树的转换示意图。
上图中 由于 C 直接到达 D 和 E,所以 C 是 D 和 E 的父节点。而到达 H 有两条路径 DF 和 EG,但是这两条是互斥的,对于互相来说都是不必要的,抽象一层,相当于 C 能直接到达 H,所以 C 是 H 的父节点~
通过支配树,我们就能很容易的分析一个Retained Set
所以 假如 E 被回收,E、G 的内存会被释放,而 H 不会。因为可能 F 还引用着 H
所以通过 MAT 提供的 Dominator Tree,我们可以很清晰地得到一个对象的直接支配对象,如果直接支配对象中出现了不该有的对象,就说明发生了内存泄漏。在 Dominator Tree 的顶部 Regex 可以输入过滤条件,如果查找的是 Activity 内存泄漏,可以在 Regex 中输入 Activity 的名称,比如这个例子可以输入 MainActivity:
可以看到 MainActivity 出现了 14 个实例,而项目中MainActivity
是不能有那么多实例的,基本可以判定发生了内存泄漏。
具体内存泄漏的原因,可以查看 GC 引用链,在 MainActivity 选项上单击右键->Path To GC Roots。
可以看到根据引用类型会有多种选项,比如 with all references 就是包含所有的引用,这里我们选择 exclude all phantom/weak/soft etc. references,因为这个选项排除了虚引用,弱引用和软引用,这些引用一般是可以被回收的,这是 MAT 就会给出 MainActivity 的 GC 引用链,如下图所示:
可以看到,引用 MainActivity 的就是 LeakThread,this$0 的含义就是内部类自动保留一个指向所在外部类的引用,而这个外部类就是 MainActivity,这将会导致 MainActivity 无法被 GC。
Histogram 与 Dominator Tree 不同的是,Dominator Tree 是从对象实例的角度进行分析,注重引用关系分析,而 Histogram 则从类的角度进行分析,注重量的分析。
这里有四列,Obejcts 表代表对象实例的个数。我们同样可以在这里输入过滤条件 MainActivity。
可以看到 MainActivity 和 LeakThread 的示实例各有 6 个,基本上可以断定发生了内存泄漏,具体内存泄漏的原因,同样可以查看 GC 引用链。在 MainActivity 选项上单击鼠标右键->Merge Shortest Paths to GC roots,并在选项中选择 exclude all phantom/weak/soft ets. references,Path To GC Roots 的信息如下图所示:
Histogram 是从类的角度进行分析,而 Path To GC Roots 是用来分析单个对象的,因此在 Histogram 中无法使用 Path To GC Roots 查询,可以使用 Merge Shortest Paths to GC Roots 查询,他表示从 GC roots 到一个或一组对象的公共路径。得出的结果和之前是相同的,引用 MainActivity 的是 Leak Thread。
OQL 全称为 Object Query Language,类似于 SQL 语句的查询语言,能够用来查询当前内存中满足指定条件的所有对象。
评论