在人生最艰难的日子里,不要想太过遥远的未来,认真过好当下每一天,就好了。------ 杨绛
内存优化的意义
Android性能优化三把板斧:稳定性、内存、启动速度、包体积。这几个方向的知识其实是相互关联的网状结构,例如内存控制得好,就不容易出现OOM导致的稳定性问题。同样,经过精简的包体积,也会大大提升应用启动速度。
相比于C/C++,JVM很大的一个跨越是实现了内存的自动分配与回收。然而,这并不意味着作为开发者可以肆无忌惮地使用内存,作为一种有限的资源,内存再大也有耗尽的时候。我们讲"内存优化",主要是出于稳定性、流畅度、进程存活率三个维度的考虑。
- 稳定性:减少OOM,提高应用稳定性
- 流畅度:减少卡顿,提高应用流畅度
- 进程存活率:减少内存占用,提高应用后台运行时的存活率
内存理论知识
这一部分讲解内存优化相关的理论知识,为实践环节提供理论支持。首先从虚拟机配置
角度说明Android系统里应用可用的内存上限;然后分析在这个上限之内,应用程序内存主要分配为哪几个部分
;最后用比较多的篇幅讲解开发中关系最紧密的对象内存分配与回收
。
应用可用内存:dalvik.vm配置
现在市面上主流旗舰机型的内存已经达到了16G
的级别,比起笔记本电脑也不遑多让,然而在极限的使用条件下,仍然会不可避免地出现卡顿。对于应用程序而言,它能够使用的最大内存是在虚拟机配置里写死的,可以通过命令adb shell getprop dalvik.vm.heapgrowthlimit
来查看,在我的手机(vivo X Note)上是256m
。
bash
> adb shell getprop dalvik.vm.heapgrowthlimit
256m
此外虚拟机配置中还有heapstartsize
、heapsize
等参数,它们都是定义在/system/build.prop
文件当中的,含义说明如下:
参数 | 含义 | vivo X Note |
---|---|---|
heapstartsize | 堆分配的初始大小,影响操作系统对RAM的分配和首次使用应用的流畅性 | 8m |
heapgrowthlimit | 单个应用可用的最大内存,若超出则OOM | 256m |
heapsize | 单个进程可以用最大内存,仅当声明android:largeHeap=true时生效,此时会覆盖heapgrowthlimit的值 | 512m |
Native虚拟内存
我们知道Android 8.0
以后,Bitmap不再存放于Java堆内存,而是位于Native的堆内存,它的上限也就是Native虚拟内存的上限,只与CPU架构相关。Native堆内存不受JVM管控。
CPU架构 | Native堆上限 |
---|---|
32位 | 3G(4G减去1G内核空间) |
64位 | 128T |
进程内存指标:USS, PSS, RSS, VSS
dumpsys meminfo
可以看到整个系统中不同进程所占据的内存情况,最重要的有以下4个缩写指标,它们之间的关系是:VSS >= RSS >= PSS >= USS
。在Android系统中推荐使用PSS曲线来衡量应用的物理内存占用情况。
内存指标 | 英文全称 | 含义 | 等价 |
---|---|---|---|
USS | Unique Set Size | 物理内存 | 进程独占的内存 |
PSS | Proportional Set Size | 物理内存 | PSS = USS + 按比例包含共享库 |
RSS | Resident Set Size | 物理内存 | RSS= USS+ 包含共享库 |
VSS | Virtual Set Size | 虚拟内存 | VSS= RSS+ 未分配实际物理内存 |
可以通过dumpsys
命令查看以上内存信息。
bash
dumpsys meminfo <pid> // 指定pid
dumpsys meminfo --package <packagename> // 指定包名,可能存在多个进程
dumpsys meminfo // 系统中所有进程的内存占用,有排序
按照范围从大到小,Android系统管理内存可以分为进程、对象、变量
三个层级。
进程内存分配与回收
进程内存分配和回收受到Linux内核的管控,Android将进程分为5
个优先级,当进程空间紧张时,按照优先级从低到高的顺序进行回收。
对象/变量内存分配与回收
对象/变量内存分配和回收受到JVM虚拟机的管控,在JVM中,内存按区域分配如下,其中方法区、堆区是供所有线程共享的。
- 方法区:被虚拟机加载的类信息、常量、静态变量;存活于程序运行整个周期,不回收
- 堆区:存储Java对象实例,在线程区-Java函数栈帧会定义实例指针(or引用),指向堆内的实例;GC时判断是否回收
- 线程区 :每一个线程单独的区域
- 程序计数器:当前线程运行到的指令位置行数
- Java函数栈帧:存储方法执行时的局部变量、操作数;执行结束后释放
- Native函数栈帧:为Native函数服务;执行结束后释放
Java对象生命周期
从ClassLoader
装载.class
文件开始,一个Java对象的生命周期包含以下阶段:
JVM内存分代
Java虚拟机采用分代策略管理堆内存中的对象,分代的目的是优化GC性能 ,将具有不同生命周期 的对象归属于不同的年代,采取最适合它们的内存回收方式。
JVM运行时内存 可以分为堆(Heap
) 和非堆(Non-heap
) 两大部分。堆在JVM启动时创建,是运行时数据区域 ,所有类实例 和数组 内存从堆分配。堆以外的内存称为非堆内存,方法区、类结构 等数据保存在非堆内存。简单说,堆是开发者可以触及的内存部分,非堆是JVM自留的部分。
从对象分代的角度,JVM内部将对象分为3类:
New Generation
,新生代,位于堆内存,内部分为3块,其中Eden
和Survivor
的默认比例为8:1Eden
,新创建的对象都位于该分区,满时执行GC,并将仍存活的对象复制到From
,GC后此区域被清空From Survivor (S1)
,GC时,将仍存活的对象复制到To
To Survivor (S2)
,满时,将仍存活的对象复制到Old
。GC之后会交换From
和To
区,从而使新的To
(也就是老的From
)永远是空的
Old Generation
,老年代,位于堆内存Permanent Generation
,永生代,位于非堆内存
GC Roots
定义:通过一系列名为GCRoots
的对象作为起始点,从这个节点向下搜索,搜索走过的路径称为ReferenceChain
,当一个对象到GCRoots
没有任何ReferenceChain
相连时,(图论:这个对象不可到达),则证明这个对象不可用。
共有4
类GC Roots
:
- 方法区
- 静态引用的对象
- 常量引用的对象
- 线程区
- Java函数栈帧中引用的对象
- Native函数栈帧中JNI引用的对象
垃圾回收算法
算法 | 解释 | 特点 | 应用场景 |
---|---|---|---|
复制 | 将内存分为2份,使用其中一份进行存储,每次执行清除时将仍然存活的对象复制到另一半,然后清除原来的内存 | 舍弃空间,换取时间上的高效率 | 对象存活率低的场景------新生代 |
标记-清除 | 先扫描一遍,将不存活的对象打上标记。第二遍扫描时清理掉这些对象 | 会产生不连续的内存碎片 | 对象存活率高的场景------老年代 |
标记-整理 | 先扫描一遍,将不存活的对象打上标记。第二遍扫描时清理掉这些对象,并将内存向一侧聚集收拢 | 防止产生内存碎片 | 对象存活率高的场景------老年代 |
内存问题归类
理解了JVM内存的分配与回收原理,我们看一下在Android开发中,发生内存问题的三个表现,它们分别是内存抖动
、内存泄漏
与内存溢出
。其中内存抖动与泄漏是引发内存溢出的重要因素。
内存抖动
什么是内存抖动
在一段时间内,频繁地发生内存的分配和释放,在曲线图上展现为锯齿状
的内存使用曲线。
内存抖动的原因
- 选择了不恰当的数据类型:如使用加号
+
进行字符串拼接,正确的做法是用StringBuilder
- 在循环里面创建对象:则循环执行时会频繁地创建销毁
- 在
onDraw
中创建对象:一次绘制会多次调用onDraw
如何避免内存抖动
- 选择恰当的数据类型(
String
->StringBuilder
) - 在循环体、
onDraw
函数外创建对象并复用,减少不合理的对象创建 - 使用对象池进行缓存复用
内存泄漏
直接原因是长生命周期的对象引用了短生命周期的对象,导致短生命周期的对象无法回收。主要是指Activity的泄漏,即持有Activity的变量生命周期长于Activity的生命周期,导致Activity虽然结束但其对象并未释放,其内部的成员变量也由于被持有而无法释放。反复进入退出页面,会看到内存曲线不断上升,即使手动GC也无济于事,继续下去会导致OOM。
常见泄漏场景
在Android开发中经常会有将Activity作为Context类型参数传递的场景,用到的时候必须谨慎,防止泄漏。
1.非静态内部类
非静态内部类会持有外部类对象的引用,典型的有Handler、Runnable等。内部类在编译后,会生成一个名为this$0
的成员变量,就是外部类实例。
1.1 Handler等待运行
在使用Handler处理消息时,会构造一个Message
对象并调用Handler.post()
,此时Message
对象中的target
成员变量指向该Handler
。由于Message
会在MessageQueue
中按时序处理,在处理到它之前,是无法释放Handler的。也就导致了Handler所持有的外部类(通常是Activity)发生泄漏。
要解决这个问题,需要从两点入手:
- 使用
Handler静态内部类
(或者直接新建一个类文件),Handler通过弱引用持有Activity - 在
Activity.onDestroy()
中清空消息队列
1.2 Runnable长时间运行
与非静态内部类相类似,通过new Runnable() {...}
创建一个匿名内部类的对象时,也会持有外部对象的引用。如果这个Runnable在线程池中排队等待处理,同样会导致Activity无法释放。解决方法与Handler类似,由于Activity与任务队列不强相关,此处不建议清空线程池中的任务队列。
- 创建一个实现了Runnable的静态内部类,内部通过弱引用持有Activity,使用Activity时判空
java
// Runnable错误用法
private Runnable mRunnable = new Runnable() {
@Override
public void run() {
}
};
// Runnable正确用法
private static class MyRunnable implements Runnable {
WeakReference<MainActivity> reference;
public MyRunnable(MainActivity activity){
reference = new WeakReference<>(activity);
}
@Override
public void run() {
MainActivity activity = reference.get();
}
}
2.方法区持有对象引用
方法区包含类结构、常量和静态变量,是GC Roots
之一,与整个应用的生命周期相同,由于长期存活即拿即用,比较方便,但也因此容易导致内存泄漏。
2.1 静态成员变量
静态成员变量在类加载的时候就会赋值,如果其中持有Activity对象,会直接导致无法回收。
2.2 单例
双重检查是单例的常规写法,其实现中也用到了静态变量,因此同样有泄漏的风险。
3.资源未关闭
当Activity销毁时,资源性对象不再使用,应当关闭该对象,然后将对象置为null
,常见的有Bitmap
、InputStream
、OutputStream
等实现了Closable
接口的类。
java
// Bitmap
Bitmap bitmap = new Bitmap();
...
bitmap.recycle();
bitmap = null;
// InputStream
InputStream inp = new FileInputStream("foo.txt");
...
inp.close();
inp = null;
4.注册对象未注销
切记需要成对调用注册-注销,如BroadcastReceiver
、EventBus
,否则会导致Activity一直被持有,从而发生泄漏。在开发总线类框架的时候,也要注意提供相应的注销接口。
5.WebView必然泄漏
这个是Android的痼疾了,一旦创建过WebView对象,就无法回收,与Chromium内
核实现有关,网上有建议用如下方法清空WebView资源,但据网友描述在一些机型让仍然会泄露。
kotlin
override fun onDestroy() {
val parent = webView?.parent
if (parent is ViewGroup) {
parent.removeView(webView)
}
webView?.destroy()
super.onDestroy()
}
既然如此,就得避免创建过多的WebView对象,或者干脆另外起一个进程用作WebView展示。
- 构建WebView对象池,实现复用
- 将WebView单独开辟一个进程使用,与主进程之间跨进程通信传递数据
6.集合未清空
将对象添加进集合后,集合实例会持有该对象的引用,因此在销毁时应当将集合中对应元素移除。
java
// 问题场景
// 通过 循环申请Object 对象 & 将申请的对象逐个放入到集合List
List<Object> objectList = new ArrayList<>();
for (int i = 0; i < 10; i++) {
Object o = new Object();
objectList.add(o);
o = null; // 虽释放了集合元素引用的本身,但仍然被集合持有
}
// 正确写法
objectList.clear();
objectList = null;
内存溢出
当应用在为新的数据结构申请内存时,如果当前可用内存不足以提供给新对象,就会发生内存溢出(OOM,即Out of Memory)。我们面临的大部分都是Java堆内存超限问题。
当发生OOM时,并不一定是当前执行的代码发生了问题,有可能是由于之前的不正确调用,当前已经处于一个内存异常的状态,在申请开辟新内存时导致问题爆发。
优化思路
这里列出一些内存优化的建议,由于在应用运行的整个生命周期里都存在内存的申请和释放,因此内存优化存在于应用开发的方方面面。
1.精简代码
在JVM中类的数据结构位于方法区,伴随应用的整个生命周期都不会销毁,因此应当尽量精简类的结构。
- 减少不用的类,以及类中的成员变量
- 减少不必要的三方依赖包
- 开启应用混淆,精简类名方法名变量名,自动清理无用的类
2.内存复用
通过复用,减少频繁创建销毁对象的行为,防止产生大量离散的内存碎片。
- 资源复用:归拢通用的字符串、颜色、度量值等,进行基础布局的复用
- 视图复用:借助ViewHolder减少视图布局开销
- 对象复用:建立对象池,实现复用逻辑
3.选用性能更高的数据结构
3.1 慎用SharedPreferences
读取SP时会将整个xml加载到内存里,占据几十K、上百K的内存。
3.2 使用SparseArray、ArrayMap代替原生的HashMap
HashMap
最初是为JVM设计的,为了减少哈希冲突,选用了一个较大的数组作为容器来降低冲突,这是一种空间换时间的思路。在Android中提供了不同的替换结构。
Key
是Int
时,选用SparseArray
Key
是其他类型时,选用ArrayMap
它们的思路是时间换空间,内部是两个数组,节约了HashMap
中空置的元素位。从性能角度看,当元素数量在1000
以下时,性能几乎与HashMap
持平。
4.选用性能更高的数据类型
4.1 避免频繁拆箱装箱(AutoBoxing)
在自动装箱时,会创建一个新的对象,从而产生更多的内存和性能开销。int
只占4
字节,而Integer
对象占据16
字节,尤其是HashMap
此类容器,当用Integer
作为Key/Value
,进行添加、删除、查找操作时,会产生大量自动装箱操作。
排查方法:通过TraceView
查看耗时,如果发现调用了大量的Integer.valueOf()
函数,说明存在装箱行为。
4.2 避免使用枚举类型,用IntDef、StringDef等注解代替
枚举的优点是提供强制的类型检查,但缺点是对于dex
构建、运行时内存都会造成更大的消耗(相比于Integer
、String
)。为了满足编译期间的类型检查,可以改用IntDef
和StringDef
,需要引入依赖:
bash
compile 'com.android.support:support-annotations:22.0.0'
5.实现onLowMemory()、onTrimMemory()方法
它们都是在Application、Activity、Fragment、Service、ContentProvider
中提供的,用于应用内存紧张时的通知,在发生这些回调时,应当主动清除缓存,释放次要资源。通过Context.registerComponentCallbacks()
进行注册监听。
- onLowMemory:发生时,所有后台进程已经被杀死
- onTrimMemory:Android 4.1引入,存在多个不同级别的回调
6.声明largeHeap以增大可用内存
在AndroidManifest.xml
中声明android:largeHeap=true
可以将应用的可用内存扩展到dalvik.vm.heapsize。但这是一种治标不治本的方法,并不推荐使用。
7.建立LruCache
对于可能存在复用的对象,建立LruCache,内部由LinkedHashMap
双向链表实现(也可以自定义双向链表)。支持get/set/sizeOf
函数。当收到低内存警告(onLowMemory
、onTrimMemory
)时,清空LruCache。
LruCache的实现可以参考Glide源码。
8.排查内存泄漏
针对上文中提出的内存泄漏风险点,使用Memory Profiler、LeakCanary、MAT等工具,逐页面进行排查。
典型问题:Bitmap优化
Bitmap是内存消耗的大头,在Android 8.0
及以上版本,Bitmap内存位于Native层,可以利用虚拟内存提升存储量,同时避免JVM堆内存耗尽。即使这样,在创建Bitmap时仍然会先占用JVM的堆空间,只不过在创建后会将其复制到Native堆进行维护。
以ARGB_8888
格式为例,一个像素占用4bytes
,所以整个Bitmap所占据的字节数=长*宽*4 bytes
,假设一张图片长宽各为500px,那么它需要占用500*500*4=1,000,000B=1MB
的内存,对于长列表而言,如果使用不当,一次就可能导致几百MB
的内存泄漏。
因此,Bitmap优化是内存优化的重中之重,通常有以下几个思路:
- 缩放减小宽高
- 减少每个像素占用的内存
- 内存复用,避免重复分配
- 大图局部加载
- 使用完毕后释放图片资源
- 设置图片缓存
- 大图监控
1.缩放减小宽高
图片尺寸不应大于View的尺寸,例如图片是200*200
,而View是100*100
的,此时应当对图片进行缩放,通过inSampleSize
实现。
java
BitmapFactory.Options options = new BitmapFactory.Options();
// 宽高都变为原来的1/2
options.inSampleSize = 2;
BitmapFactory.decodeStream(is, null, options);
2.减少每个像素占用的内存
这一条适用于那些些不需要展示高清图的场景,以及低配机器的图片显示。在API 29
中,将Bitmap分为6
个等级,如果图片不含透明通道,可以考虑用RGB_565
代替ARGB_8888
,能够节约一半的内存。
ALPHA_8
:不存储颜色信息,每个像素占1个字节;RGB_565
:仅存储RGB
通道,每个像素占2个字节,对Bitmap
色彩没有高要求,可以使用该模式;ARGB_4444
:已弃用,用ARGB_8888
代替;ARGB_8888
:每个像素占用4个字节,保持高质量的色彩保真度,默认使用该模式;RGBA_F16
:每个像素占用8个字节,适合宽色域和HDR
;HARDWARE
:一种特殊的配置,减少了内存占用同时也加快了Bitmap
的绘制。
3.内存复用,避免重复分配
通过设置BitmapFactory.Options.inBitmap
,可以在创建新的Bitmap时尝试复用inBitmap
的内存,在4.4
之前需要inBitmap
与目标Bitmap同样大小,在4.4
之后只要inBitmap
比目标Bitmap大即可。
4.大图局部加载
有些场景下需要展示巨大的一张图片,比如地铁路线图、微博长图、对高像素图片进行编辑等,如果按照原图尺寸进行加载,一张4800万像素
的照片会占据48M
的内存,非常可怕。
因此,在大图展示的场景下,通常采用BitmapRegionDecoder
来实现。
java
BitmapRegionDecoder decoder = BitmapRegionDecoder.newInstance(inputStream, false);
BitmapFactory.Options options = new BitmapFactory.Options();
options.inPreferredConfig = Bitmap.Config.RGB_565;
// 加载图片中央200*200的局部区域
Bitmap bitmap = decoder.decodeRegion(new Rect(width/2-100, height/2-100, width/2+100, height/2+100), options);
mImageView.setImageBitmap(bitmap);
5.使用完毕后释放图片资源
Bitmap使用完毕后应当及时释放。
5.1 常规释放
java
Bitmap bitmap = ...
// 使用该bitmap后,进行释放
bitmap.recycle();
bitmap = null;
5.2 非常规,泄漏兜底
如果Activity、Fragment发生泄漏,会导致它们持有的View无法释放,进而使View中展示的Bitmap对象无法被系统回收。
此时,可以通过覆写View.onDetatchedFromWindow
进行监控兜底,当View从Window中被移除,且Activity处于destroy
状态时,延迟2s
判断,如果2s后图片仍然没有被移除,说明发生了泄漏。此时应当主动将Bitmap释放。
6.设置图片缓存
可以参考Glide的四级缓存策略,我之前写过文章《Glide源码解析》。
7.进行大图监控
前面说过,如果Bitmap的尺寸大于目标View的尺寸,是没有意义的,这里提供一些技术手段协助进行大图监控,我们声明一个checkBitmapFromView
的函数,用于扫描当前Activity、Fragment的全部View,并取出Bitmap进行尺寸比较,如果发现大图case则报警。
java
/**
* 1. 遍历Activity中每个View
* 2. 获取View加载的Bitmap
* 3. 对比View尺寸和Bitmap尺寸
*/
fun checkBitmapFromView() {
// 实现略
}
如何将这段检查的逻辑插入到代码当中?有以下几种方式。
方案 | 实现思路 | 优点 | 缺点 |
---|---|---|---|
BaseActivity | 在基类的onDestroy()中检查 | 接入简单 | 侵入性强,对全部Activity产生影响,所有Activity需要继承BaseActivity |
ArtHook | 是针对ART虚拟机的Hook方案,通过Hook ImageView的setImageBitmap函数,在其中进行检测 | 无侵入性,一次配置全局生效;可以获取代码调用堆栈,便于定位问题 | Hook方案存在兼容性风险,需要全面覆盖测试 |
ASM | 在编译过程中对setImageBitmap进行插桩 | 无侵入性 | 增加编译耗时,ASM代码维护成本高 |
registerActivityLifecycleCallback | 在Application中注册 | 无侵入性,接入简单 | 暂无 |
分析工具
Memory Profiler
MAT(Memory Analyzer Tool)
MAt是用来分析内存快照,分析对象引用链,从而找出导致内存泄漏根因的工具。
MAT的使用步骤
- 下载 eclipse.org/mat/downloa...
- 在Android Studio中进入Memory Profiler功能,操作应用关键页面
- 点击GC以清理掉那些没有被泄露的对象,此时剩余的是发生泄漏的
- 将内存快照dump到本地
- 使用Android SDK自带的转换工具(位于
platform-tools
),将Dalvik/ART
格式的.hprof
文件转换为MAT能识别的J2SE
格式,最后用MAT打开文件
bash
./hprof-conv aaa.hprof aaa-converted.hprof
MAT中的术语说明
- Dominator:支配者,如果B引用了A,那么B是A的支配者
- Dominator Tree:支配者引用链
- Shallow Heap:当前对象自身的内存占用
- Retained Heap:当前对象及其成员变量加在一起的总内存占用
- Outgoing References:当前对象引用了哪些对象
- Incoming References:当前对象被哪些对象引用
- Top Consumers:通过饼图方式列出内存占用最多的对象
LeakCanary
在开发环境中使用,反复进出同一页面,查看是否有泄漏通知。LeakCanary可以保存.hprof
文件,供进一步分析。关于其原理部分主要有三个考察点:怎样监听Activity销毁时机、如何判断Activity是否被释放、如何自启动,可以参考我之前写的一篇文章《LeakCanary源码解析》。
线上OOM监控:美团Probe与快手KOOM
当线上发生OOM时,我们需要立即采集现场数据,并报警给服务器进行收集。对此,不同公司都提出了各自的方案,例如美团的Probe(未开源),快手的KOOM。这些方案主要解决了以下几个问题:
- 监控OOM:当发生OOM时触发采集上报流程
- 堆转储:即采集内存快照,
dump hprof
- 解析快照:把对分析问题无用的数据裁剪去掉,仅保留有用的信息。进行可达性分析,生成到
GC Roots
的调用链,找出泄漏根因 - 生成报告并上传:KOOM支持在端侧生成
json
格式报告,并进行上传,将解析工作交由端侧进行,能有效减少云端的分析压力
面试常见提问
Q:说说你对内存优化的理解
问题比较大,采用总-分-总方式作答。
- (总)内存管理是应用性能优化中非常重要的一环,在我以往的工作中也从事过一些内存优化的工作,业余也查阅积累了一些内存优化的知识。接下来我从重要性、问题分类、常见问题场景、优化思路几个方面讲一下我的理解
- (分)不要讲的太细,一是占用时间太多,过犹不及,容易让面试官感到啰嗦,二是会留下背答案的印象
- (总)略
Q:开发过程中你是怎样判断有内存泄漏的
在开发过程中,我通常使用以下几种方法,对内存泄漏可能存在的场景进行快速检测和定位。
- 内存泄漏-应用全生命周期分析 :
shell命令 + LeakCanary + MAT
,运行程序并将主要链路页面都打开一遍,完全退出程序并手动触发GC。然后使用adb shell dumpsys meminfo <packagename> -d
打印应用当前进程信息,如果存活的View和Activity、Fragment数量不是0
,说明它们发生了泄漏。接着借助LeakCanary查看哪些对象发生了泄漏,最后用MAT找出这些泄漏对象的引用关系,定位问题根因 - 内存泄漏-单一页面分析:使用Memory Profiler工具,对于目标页面,反复进出5次,在最后一次退出后手动触发GC。如果此时内存曲线没有回到进入页面之前的状态,说明发生内存泄漏。然后,将内存快照保存下来,并在Memory Profiler中查看该Activity,如果存在多个实例,说明没有被回收,即发生泄漏
- 内存上涨-单一页面分析 :利用Memory Profiler观察进入每个页面的内存变化情况,对于上升幅度大的页面,下载进入页面前后的两个内存快照并用MAT的对比分析功能,找出新页面内存的分配情况,被哪些大的对象所占用