Android内存优化(二):一分钟发现内存泄漏
在上一篇文章Android内存优化(一):Java内存区域中已经大体上介绍了Java中的内存分布情况,这一篇主要讲一下内存泄漏的产生原因、内存泄漏的危害、内存泄漏一键分析与定位、以及代码中常见的内存泄漏。
1内存泄漏的产生原因
前方高能,18岁以下请避让!!!
惊天大咪咪:内存泄漏产生的原因是对象占着茅坑不拉屎!!!
有必要讲一下Android中的垃圾收集是怎么进行的,Android中使用标记-清除(Mark-Sweep)算法进行垃圾回收(garbage collection,简称GC),就是按照正常套路来说,在坑位(内存)不够的情况下,垃圾收集器会遍历全部对象,看哪些对象是可以被回收掉腾出内存的,这个过程称为Mark(标记),Mark的时候要求除了垃圾收集线程之外,其它的线程都停止,这种吊炸天的现象在垃圾收集算法中称为Stop The World,世界围着他转,这就造成了我们的程序会卡顿,但是一般情况下这个时间就几十毫秒,我根本就感受不到好吗。Mark完之后,就是释放内存空间啦,这个过程称为Sweep(清除)。
这一切看起来很美好,但是就是有内存泄漏发生,所以得提一下,不是所有的对象都是特仑苏,阿呸,不是所有的对象都能被回收的,比如下面的傲娇贱货。
- 垃圾回收的原则:被全局变量(static)、栈变量和寄存器等直接引用和间接引用的对象不能被回收。
所以说,对象即使已经使用完,但却一直被其它对象引用,就会导致这个对象无法被回收,造成内存的浪费,让别的对象无屎可拉。对象无法被GC回收就是造成内存泄露的原因!
2内存泄漏的可能会造成的创伤
如果不是利用工具去找的话,一般情况下内存泄漏是比较难发现的,因为Java中不会报内存泄漏这种异常,所以在轻微的内存泄漏表面上看是跟正常情况下没有区别的。
- 2.1 内存泄漏跟内存溢出(OOM)的区别就是:量变和质变。一个两个内存泄漏表面看起来没毛病,但是量变可以导致质变,内存泄漏多了会炸的,就是报OOM异常,应用直接崩溃,连解释的机会都没有。
- 2.2 堆得内存大小是确定的,出现内存泄漏后可用的内存会减小,这又会造成垃圾回收的频率加剧,上面提到过,垃圾回收的Mark阶段会有一种吊炸天的现象,就是Stop The World,除了垃圾回收线程之外的线程会停止,频繁的垃圾回收卡顿明显的感受到。
- 2.3 应用后台运行的时候,内存占用大,进程被系统杀死的概率就会大咯。
3内存泄漏的发现
内存泄漏的分析的话,必须使用工具才行,庆幸的是,各路大神已经给我们提供了很多强大的内存分析工具,我这里只会讲最方便的。这里提供几个套餐供选择
3.1 套餐一:Studio自带Heap Viewer
想不想知道你的应用到底有没有内存泄漏呢?说真,就一分钟的事。
-
3.1.1打开Studio,连上你的应用,然后Android Monitor (1)->Monitors(2)->Memory,上面有四个图标,暂停图标是开启内存使用状态追踪的开关,默认是开启的,小车图标就是手动GC(3),向下箭头图标(4)是查看堆的分配情况,最后的图标allocation tracker用来跟踪内存分配情况。
-
3.1.2我讲一下我的使用方式,在应用中操作,从activity1跳转到activity2,然后跳回到activity1界面,这样是为了分析activity2是否会产生内存泄漏。接下来就是真刀真枪的干了。
-
3.1.3点击小车图标(3),手动GC进行垃圾回收,这样才能更准确的判断activity2是否有内存泄漏发生,最后点击向下箭头图标(4),Studio会自动生成hprof文件并自动展示在Studio界面中。
-
3.1.4这个就是内存的分析文件了,点击Analyzer Tasks(5),这是让Studio帮我们自动分析是否出现内存泄漏。
-
3.1.5勾上Detect Leaked Activities(6),最后运行(7)就出现分析结果了
-
3.1.6看到没,activity2出现内存泄漏了(8),左下角是引用树(9),通过引用树就可以定位到内存泄漏的具体信息了。
3.2套餐二:Heap Viewer + MAT
是啊,发现有内存泄漏了,然而还有其它的选择,这里就必须使用到其它的工具进行辅助了。
MAT(Memory Analyzer)内存分析工具,这个工具的使用我只简单讲一下,因为我一般不用,不要问为什么,因为用起来比较麻烦一些。
- 3.2.1MAT下载,进入下载的官网,我电脑是64位的,所以选择Windows(x86_64),整个下载安装流程跟一般软件没啥区别,进入新页面然后点击DOWNLOAD
点击click here就可以下载使用了
-
3.2.2 hprof文件导入,这个文件的获取流程跟内存泄漏的发现流程基本一样,按上面说的通过Studio的Heap工具获取的,但是文件导入前需要进行一下转换,因为MAT工具不能直接使用,转换也
不麻烦,Studio已经帮你简化这个过程,一键导出转换文件,请看过来
- 3.2.3 用MAT打开hprof的转换文件,其中Histogram和Dominator Tree比较常用,分析内存泄漏特别需要用到Histogram的两份文件对比分析,就是获取两份内存泄漏前后的hprof转换文件
-
3.2.3 标题栏Window->Navigator History,打开 Navigator History面板,然后点击打开Histogram
-
3.2.4 右键histogram,将两份分析文件的
Histogram结果都添加到 Compare Basket中,点击右上角的!图标就会生成对比文件
-
3.2.5 这就是最后生成的对比文件,你还可以自己选择对比的方式,红圈里面提供不同的对比方式,这样就可以很直观的看出差异,因为我对比的是同一份文件,所以对象间木有差异。
3.3套餐三:Leakcanary
square的开源内存泄漏分析框架,好用得不得了,配置很简单
- 3.3.1建议在app的
build.gradle
文件下添加下面的依赖
dependencies {
debugCompile 'com.squareup.leakcanary:leakcanary-android:1.5'
releaseCompile 'com.squareup.leakcanary:leakcanary-android-no-op:1.5'
testCompile 'com.squareup.leakcanary:leakcanary-android-no-op:1.5'
}
- 3.3.2在你的
Application
中的onCreate()
方法中进行初始化
public class ExampleApplication extends Application {
@Override public void onCreate() {
super.onCreate();
if (LeakCanary.isInAnalyzerProcess(this)) {
// This process is dedicated to LeakCanary for heap analysis.
// You should not init your app in this process.
return;
}
LeakCanary.install(this);
// Normal app init code...
}
}
-
3.3.3然后,就没有然后了,编译完后运行你的项目,会在项目安装成功后出现附加的组件,里面会展示具体的内存泄漏路径。
-
3.3.4通过这个泄漏路径,就对应进行内存泄漏的原因进行分析了,你也可以通过输出的日志进行内存泄漏的定位。
注:到这里3个套餐已经讲完了,关于MAT这个套餐我只是讲一下基本的使用,其实已经够用了,怎么说呢,用起来比较麻烦,所以我自己本身也很少用,我就按自己的使用对比一下三者。
套餐三>套餐一>套餐二
1.套餐三使用最方便,一劳永逸,解析hprof的速度有点慢,但是因为后台自动解析,所以基本上没多大关系;
2.套餐一使用最快,切换一下页面分分钟就知道有没有内存泄漏,但是需要你每一次都要手动操作;
3.套餐三最麻烦,耗时耗力,但是自动分析工具并不能保证找出所有的内存泄漏,这个时候就需要通过MAT辅助分析了。
4代码里头内存泄漏的常见原因
代码中内存泄漏大多数产生的原因是不遵循activity的生命周期。
- 4.1单例模式(静态activity):在你的Activity中定义了一个 static 变量引用了activity,因为static变量的生命周期和app一样长,就算activity被销毁,activity对象还是会被static变量持有,一直到app被销毁,这也是单例模式最容易造成泄漏的原因,如果静态的单例对象持有activity对象的引用,就会使得该对象不能被正常回收,从而导致了内存泄漏。解决办法是使用Application的Context代替activity的context;
/**
* 单例模式
*/
public class SingletonClass{
private static SingletonClass instance;
private Context context;
public static SingletonClass getInstance(Context context){
synchronized(SingletonClass.class){
if(instance==null){
instance=new SingletonClass(Context context);
}
}
return instance;
}
private SingletonClass(Context context){
this.context = context; //传入activity的context就会造成内存泄露咯
}
}
- 4.2静态View:当一个view 被加入到界面中时,它就会持有 context 的强引用,也就是我们的 activity。如果我们通过一个static成员变量引用了这个 view,相当于直接引用了 activity,然后就泄漏了;
private static View view;
view = findViewById(R.id.sv_button);
- 4.3非静态内部类:我们都知道,内部类能够引用外部类的成员,这正是内部类的好处所在,但是恰恰是这个优势会导致activity内存泄漏,因为非静态内部类默认持有外部类的引用。如果我们创建了一个内部类的对象,并且通过静态变量持有这个对象,就会导致内存泄漏;
private static InnerClass inner = new InnerClass();
class InnerClass {
}
- 4.4匿名内部类:匿名类同样会持有定义它们的对象的引用,如果在 activity 内定义了一个匿名的 AsyncTask 对象,就有可能发生内存泄漏了。因为在activity被销毁之后AsyncTask可能仍然在运行,这样只能等到AsyncTask执行结束才能回收activity;
new AsyncTask<Void, Void, Void>() {
@Override
protected Void doInBackground(Void... params) {
while(true);
}
}.execute();
- 4.5Handler+Runnable:定义一个匿名的 Runnable 对象并将其提交到 Handler 上也可能导致 activity 泄漏。Runnable对象引用了定义它的 activity 对象,而它会被提交到 Handler 的 MessageQueue 中,如果它在 activity 销毁时还没有被处理,那就会导致内存泄漏了。
new Handler() {
@Override
public void handleMessage(Message message) {
super.handleMessage(message);
}
}.postDelayed(new Runnable() {
@Override public void run() {
while(true);
}
}, 1000);
- 4.6Thread:原因类似4.5,尽管是在单独的线程执行任务,但是线程还是会默认持有外部对象,任务没有执行完成就不会释放持有的引用;
new Thread() {
@Override public void run() {
while(true);
}
}.start();
- 4.7资源未关闭:如果使用了BraodcastReceiver,ContentObserver,File,Cursor,Stream,Bitmap等资源,应该在Activity销毁时及时关闭或者注销,否则这些资源将不会被回收,从而造成内存泄漏。
- 4.8集合容器:在我们做缓存的时候会用一些数据结构来存储一些数据,当我们不需要它时要及时清理,不然就会像滚雪球一样会越来越大,想不泄露都难。
可以了,造成内存泄露还有很多原因,这就靠慢慢跳坑了,生活太艰难。再话痨一下,“千丈之堤,以蝼蚁之穴溃;百尺之室,以突隙之烟焚。”,所以我推荐套餐三Leakcanary,让你的整个开发过程伴随着内存泄露的监控。