android 内存泄露

公司的项目基本该有的功能都做完了,之前都是在赶工,改需求,妈的需求改得还真TM频繁,从开始做的时候就改到要发布,这种情况已经持续一年多了,一直到现在,害得每次都赶工,着实是蛋疼,真他妈的不想干了。因为之前都是赶功能,根本就没时间对app进行内存优化,现在没什么事做了,就来优化下内存。

对于android手机而言,内存是很宝贵的,不像PC一样。手机内存本来就不多,如果开发上还不注意节约内存的开销,很容易导致可用内存变得越来越少,到你的app的使用内存超过向系统申请内存时,系统就不得不把你的app进程给kill掉。所以我们为了不给系统增加太多的压力和不让我们app给系统干掉,还是优化下内存开销吧。

  • 什么是内存泄露
在java中,如果一个对象没有可用价值了,但又被其他引用所指向,那么这个对象对于gc来说就不是一个垃圾,
所以不会对其进行回收,但是我们认为这应该是个垃圾,应该被gc回收的。这个对象得不到gc的回收,
就会一直存活在堆内存中,占用内存,就跟我们说的霸着茅坑不拉屎的道理是一样的。这样就导致了内存的泄露。

所以我们在开发中就应该尽量避免内存泄露,让app使用更加流畅。

  • 内存泄露检测工具

1、MAT,下载地址: MAT ,这个工具功能很强大,但是学习成本比较高,我用了几遍就不想用了,实在是麻烦,每次都要导出.hprof文件,然后通过命令行把.hprof转换成MAT可以识别的文件,里面的功能也要学习断时间才行。
2、LeakCanary, 下载地址:LeakCanary ,这个工具实在是太刁了,方便,使用简单,在通知栏通知内存泄露,我非常喜欢。以下讲解下他的使用,当然,你也可以看官方文档。

1.1、首先我们在build.gradle中的dependencies中添加以下代码

debugCompile 'com.squareup.leakcanary:leakcanary-android:1.3.1' // or 1.4-beta1
releaseCompile 'com.squareup.leakcanary:leakcanary-android-no-op:1.3.1' // or 1.4-beta1
testCompile 'com.squareup.leakcanary:leakcanary-android-no-op:1.3.1' // or 1.4-beta1

debugCompile 是在调试的时候使用的,releaseCompile 是在release.apk包中不使用的,这样配置也就不用我们修改任何代码了,我们正式打的包是不会出现leakcanary那些提示的。

1.2、在Application中的onCreat方法添加代码:

LeakCanary.install(this);

经过以上配置就可以监听activity是否存在内存泄露了,什么代码都不用加了。如果我们需要监听某个对象是否存在内存泄露,我们可以这样做:

RefWatcher refWatcher = ExampleApplication.getRefWatcher(getActivity()); 
refWatcher.watch(testInstance);
  • 内存泄露案例1
public class LeakActivity extends Activity {
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        new Handler().postDelayed(new Runnable() {
            @Override
            public void run() {

            }
        },1000 * 60);
    }
}

以上代码很简单,handler在post了一个1分钟后执行的runnable,而这个runnable是挂载在主线程的。看似很平常,没什么不对,我们运行程序后,可以看到以下截图:

device-2016-02-26-171945.png

从上图我们可以看到,LeakActivity的对象发生了内存泄露,是由mMessageQueue这个对象导致的。当我们启动LeakActivity的时候,handler会把Runnable对象封装成一个Message,然后 post 这个Message进MessageQueue,等待60秒后执行。我们结束LeakActivity时,并没有取消掉MessageQueue里的Message,所以Message里的Runnable会一直等到1分钟结束后执行里面的run方法,在这过程中MessageQueue会一直持有Message的对象引用,然而Runnable是封装在这个Message的,所以他们之间的引用关系就像这样:MessageQueue->Message->Runnable->Handler->Activity。所以这就导致当前activity与MessageQueue一直有关联,导致LeakActivity的对象不能被gc回收,从而导致内存泄露。(关于Handler、Message、MessageQueue、Looper之间的工作原理可以去看下源码)

所以要避免上面的内存泄露,我们可以这样做,在activity的onDestroy方法中干掉handler的所有callback和message:

@Override
protected void onDestroy() {
    super.onDestroy();
    handler.removeCallbacksAndMessages(null);
}

这样就ok了。

在activity中开启的线程也是一样,如果activity结束了而线程还在跑,一样会导致activity内存泄露,因为"非静态内部类对象都会持有一个外部类对象的引用",你创建的线程就是activity中的一个内部类,持有activity对象的引用,当activity结束了,但线程还在跑,就会导致activity内存泄露。

上面的例子是很容易看出是否有内存泄露,那么接下来的例子就没那么容易看出来了,而且开发中使用的频率是很高的。

  • 内存泄露案例2

LeakActivity1

public class LeakActivity1 extends AppCompatActivity {

    private TestManager testManager = TestManager.getInstance();
    private MyListener listener=new MyListener() {
        @Override
        public void doSomeThing() {}
    };

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        testManager.registerListener(listener);
    }

}

TestManager

public class TestManager {

    private TestManager(){}
    private static final TestManager INSTANCE = new TestManager();
    private MyListener listener;

    public static TestManager getInstance() {
        return INSTANCE;
    }

    public void registerListener(MyListener listener) {
        this.listener = listener;
    }
    public void unregisterListener() {
        listener = null;
    }
}

interface MyListener {
    void doSomeThing();
}

运行LeakActivity1后出现了内存泄露,如下图:

device-2016-02-26-183138.png

由上图我们可以看出,LeakActivity1还是内存泄露了。我按照上面标注的顺序说下,第1个中的leaks是内存泄露的意思,代表是LeakActivity1的对象instance内存泄露了,2中references是引用的意思,导致内存泄露是由LeakActivity1中的一个实现了MyListener的匿名类导致的,这里的引用就是指这个实现类的引用,3中的reference是指TestManager类中的listener这个引用,4中指出了最终导致内存泄露的根本源头为TestManger类中的INSTANCE。

上面的代码中,我们定义了个TestManager,并且使用单例模式,然后在activity实现MyListener接口,再通过testManager.registerListener(listener);注册个回调。我们开发中很经常这样干。但是我们这里的是单例,如下图,当程序执行LeakActivity1时,读到private TestManager testManager=TestManager.getInstance(); 这句代码时,首先会在栈内存中开辟内存存储testManager变量,然后读到TestManager.getInstance();时候,会加载TestManager类,因为TestManager中有static对象,static跟类的生命周期是一样的,类一加载,static就加载了,类一被销毁,static才会跟着销毁(static是存在方法区),这时候jvm会在方法区中存储变量INSTANCE,然后在堆内存开辟空间存放INSTANCE对象,然后把地址值付给INSTANCE变量,使INSTANCE变量就指向这个对象(类似c语言的指针),activity类也是这样的一种执行关系。

内存中的加载情况.png

因为这是一个单例,当app进程被干掉的时候,堆内存中的INSTANCE对象才会被释放,所以INSTANCE对象的生命周期是很长的,LeakActivity1中,listener持有当前activity的对象,然后testManager.registerListener(listener);执行完,TestManager中的listener就持有activity中listener的对象,而TestManager中的INSTANCE是static的,生命周期长,activity销毁的时候INSTANCE依然还在,INSTANCE还在,那么TestManager类中的全局变量也还是存在的,所以TestManager中的listener变量还在,还一直持有LeakActivity1中的listener对象引用,所以最终是INSTANCE导致LeakActivity1内存泄露。

所以,要解决这个问题,可以这样做,在activity的onDestroy方法中注销注册的listener:

@Override
protected void onDestroy() {
    testManager.unregisterListener();
    super.onDestroy();
}

这样做后TestManager中的listener不再持有LeakActivity1中的listener对象引用,所以LeakActivity1被销毁后listener对象也可被回收了。
最终,问题又解决了,当然你也可以直接把INSTANCE置null。

  • 总结
1、小心使用static
2、线程生命周期要跟activity同步
3、小心使用第三方jar包(我开发中就遇到过jar包中持有activity对象导致的内存泄露)
4、网络请求也是线程操作的,也应该与activity生命周期同步,在onDestroy的时候cancle掉请求
5、尽量使用application代替activity和context:  Context context = activity.getApplication();这样就使得context不是指向Activity了,指向全局的application,这样就没内存泄露可说了。
等等......

不单单是activity会出现内存泄漏的,其他的类对象也可能会泄漏,对象回收不了,那么类中的其他变量值也会在,如果不处理,量一多,还是挺可怕的。今天就写到这了,公司现在就剩我一个人了,该回去吃饭了。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 160,999评论 4 368
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 68,102评论 1 302
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 110,709评论 0 250
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 44,439评论 0 217
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,846评论 3 294
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,881评论 1 224
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 32,062评论 2 317
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,783评论 0 205
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,517评论 1 248
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,762评论 2 253
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,241评论 1 264
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,568评论 3 260
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 33,236评论 3 241
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,145评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,941评论 0 201
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,965评论 2 283
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,802评论 2 275

推荐阅读更多精彩内容

  • 前言 对于内存泄漏,我想大家在开发中肯定都遇到过,只不过内存泄漏对我们来说并不是可见的,因为它是在堆中活动,而要想...
    EsonJack阅读 859评论 1 3
  • 内存泄露指的是该释放的对象没有释放,一直被某个或某些实例特持有却不再被使用导致GC不能回收。首先,我们先看看Jav...
    PeOS阅读 654评论 0 2
  • 我的博客:http://xuyushi.github.io原文地址 [TOC] 内存泄露 内存泄露的定义:当某些对...
    接地气的二呆阅读 1,252评论 2 23
  • 参考内存泄露从入门到精通三部曲之基础知识篇Android 内存泄漏总结Android内存泄漏研究Android内存...
    合肥黑阅读 403评论 0 3
  • 注意Activity的泄漏 通常来说,Activity的泄漏是内存泄漏里面最严重的问题,它占用的内存多,影响面广,...
    738bc070cd74阅读 562评论 0 2