Android 内存泄露分析

Android OOM/MemoryLeak

各位读者可能都有拿的出手的github或者APP实战项目,但是会使用现成的XX开源组件并不代表你的基础就很好。本文将带你补习Android基础 -- Android中内存泄露实例,分享给大家。

1. 基础

在阅读本文前,请了解如下基础

本文属于java语言上的分析,不涉及到GC,虚拟机,native底层细节的实现。

2. 什么是内存泄漏

  • 当你不再需要某个实例后,但是这个对象却仍然被引用,防止被垃圾回收(Prevent from being bargage collected)。这个情况就叫做内存泄露(Memory Leak)。
  • 内存泄漏潜在危害非常大,比如无意泄漏了一个Drawable,它可能只有几百K的占用,但是由于它一般会引用View,就意味着同时泄漏了View,Context,Activity 以及 Activity中的resource,这个内存的泄漏就非常可观了。而且Android设备作为嵌入式设备,内存非常有限,泄漏后的卡顿或者崩溃也非常影响用户体验。

3. 常见内存泄露与解决方法

Activity中防止内存的关键只有一个:及时回收没有使用的项目。

3.1. 需要手动关闭的对象没有关闭

3.1.1. try/catch/finally中网络文件等流的手动关闭

  • HTTP
  • File
  • ContendProvider
  • Bitmap
  • Uri
  • Socket

这些都是java基础啦,就不一一介绍了。我们可以用RxJava进行封装,让它变成可观察的流;在Go语言中,可以使用Defer这样的方法来减少迷之缩进;在okhttp中,使用了引用计数的技术对流进行管理

3.1.2. onDestroy() 或者 onPause()中未及时关闭对象

泄露实例:

  • 线程泄漏:当你执行耗时任务,在onDestroy()的时候考虑调用Thread.close(),如果对线程的控制不够强的话,可以使用RxJava自动建立线程池进行控制,并在生命周期结束时取消订阅;
  • Handler泄露:当退出activity时,要注意所在Handler消息队列中的Message是否全部处理完成,可以考虑removeCallbacksAndMessages(null)手动关闭
  • 广播泄露:手动注册广播时,记住退出的时候要unregisterReceiver()
  • 第三方SDK/开源框架泄露:ShareSDK, JPush等第三方SDK需要按照文档控制生命周期,它们有时候要求你继承它们丑陋的activity,其实也为了帮你控制生命周期
  • 各种callBack/Listener的泄露,要及时设置为Null,特别是static的callback
  • EventBus等观察者模式的框架需要手动解除注册
  • 某些Service也要及时关闭,比如图片上传,当上传成功后,要stopself()
  • Webview需要手动调用WebView.onPause()以及WebView.destory()

比如常见的ButterKnife

  @Override public void onDestroyView() {
    super.onDestroyView();
    ButterKnife.reset(this);
  }

再比如ShareSDK(此垃圾再也不用)

protected void onDestroy() {
        ShareSDK.stopSDK(this);
        super.onDestroy();
    }

使用开源的框架(比如帮你写好的图片下载队列,REST解析等)可能会帮助你快速的解决这个问题,但是知其然并知其所以然,也要了解它们的生命周期

3.2. Static的使用

3.2.1 static class/method/variable 的区别,你真的懂了吗?

(1). Static inner class 与 non static inner class 的区别

static inner class 即静态内部类,它只会出现在类的内部,在某个类中写一个静态内部类其实同你在IDE里新建一个.java 文件是完全一样的。

以下为它们的对比

class对比 static inner class non-static inner class
与外部class引用关系 如果没有传入参数,就无引用关系 自动获得强引用(implicit reference)
被调用时需要外部实例 不需要(比如Bulider类) 需要
能否调用外部class中的变量与方法 不能
生命周期 自主的生命周期 依赖于外部类,甚至比外部类更长

可以看到,在生命周期中,埋下了内存泄漏的隐患,如果它的生命周期比activity更长,那么可能会发生泄露,更可怕的是,有可能会产生难以预防的空指针问题。

这个泄露的例子,详见内存管理(2)的文章。

(2). static inner method

静态内部方法,也就是虚函数:可以被直接调用,而不用去依赖它所在的类,比如你需要随机数,只用调用Math.random()即可,而不用实例化Math这个对象。在工具类(Utils)中,建议用static修饰方法。static方法的调用不会泄露内存。

(3). static inner variable

慎重使用静态变量,静态变量是被分配给当前的Class的,而不是一个独立的实例,当ClassLoader停止加载这个Class时,它才会回收。在Android中,需要手动置空才会卸掉ClassLoader,才能出现GC。

static 变量称为静态变量或者类变量,它由类的所有实例共享。
Classes are only unloaded if all classes associated with a ClassLoader can be garbage collected, which is rare but will not be impossible in Android.
高效的场景:“全局常量”,“单例”与“远程接口”。

这段谷歌博客上的著名代码演示了一次内存泄露的,当你旋转屏幕后,Drawable就会泄露。

private static Drawable sBackground;

@Override
protected void onCreate(Bundle state) {
  super.onCreate(state);

  TextView label = new TextView(this);
  label.setText("Leaks are bad");

  if (sBackground == null) {
    sBackground = getDrawable(R.drawable.large_bitmap);
  }
  label.setBackgroundDrawable(sBackground);

  setContentView(label);
}

注意,我在实际试验(CM12)中,没有GC时,导出的堆是有泄露的,而手动GC后,是不会发生内存泄露的,希望各位自己做实验,发一下反馈(评论已经有了相关反馈)。

[图片上传中...(image-3862fe-1556528446518-0)]

总的来说,还是少用,就算可能是新的设备支持更加先进的GC,但是还是要注意控制内存。

3.2.2. 使用内部匿名类要注意什么?

匿名内部类实际上就是non-static inner class,比如某些初学者经常一个new Handler就写出来了,它对外部类有一个强引用。建议单独写出来这个类并继承,并加入static修饰。

3.2.4. 单例模式(Singleton)是不是内存泄漏?

在单例模式中,只有一个对象被产生,看起来一直占用了内存,但是这个不意味就是浪费了内存,内存本来就是用来装东西的,只要这个对象一直被高效的利用就不能叫做泄露。但是也不要偷懒,一个劲的全整成了单例,越多的单例会让内存占用过多,放在Application中初始化的内容也越多,意味着APP打开白屏的时间会更久,而且软件维护起来也变得复杂。

  • 好的例子:GlobalContext,SmsReceiver动态注册,EventBus

3.2.5. 为什么大神喜欢用static final来修饰常数?

static由于是所有实例共享的,说到共享一定要加锁,万一某个实例更改它后,其它的实例也会受到影响,所以加入final作为永久只读锁以防止常数被修改。

全局变量生命周期是classloader,有坑。你的activity在finish后变量并不会改变。
这个在面试中经常遇到,问你经过多次计算后,static的值是多少。比如在Android中有个坑,最常见的就是把一个sharedpreference赋值给一个static变量,然后又把sharedpreference改变后,再次调用这个static变量,就发现变量并没有改变,这个在debug中很难发现。

3.2.6. 顺便说下final吧

  • final 变量:是只读的;
  • final 方法:是不能继承或者重写的。
  • final 引用:引用不能修改,但是对象本身的属性可以修改;
  • final class:不可继承;
final MyObject o = new MyObject();
o.setValue("foo"); // Works just fine
o = new MyObject(); // Doesn't work.
  • 虚拟机并不会知道你的变量是否是final的,所以final与内存泄露无关。
  • final不会让代码速度更快

3.3. Bitmap的使用

  • 使用前注意配置Bitmap的Config,比如长宽,参数(565, 8888),格式;
  • 使用中注意缓存;
  • 使用后注意recycle以清理native层的内存。

2.3以后的bitmap不需要手动recycle了,内存已经在java层了。同时,Bitmap还有别人做好的轮子,比如PhotoView,Picasso,就可以方便的解决OOM问题。

3.4. 多线程

线程泄露可能是最严重的泄露问题了,第一它可能与Handler一样,转一转手机内存就没了,第二是当回调的时候,它极可能弹出NullPointException

个人在实际使用的一个失败实例

上传图片时退出Activity,等到图片完成后,Toast就会抛出空指针异常。

//retrofit 1.9 bad sample 
RestAdapter adapter = new RestAdapter.Builder().setEndpoint(HeadlineService.END_POINT)
            .setLogLevel(RestAdapter.LogLevel.FULL)
            .build();
adapter.create(ImageService.class)
    .updateImage(new TypedFile("image/*", file), new TypedString(nickname),
        new TypedString(Build.MODEL), new TypedString(avatar),
        new Callback<UploadResult>() {
          @Override public void success(UploadResult uploadResult, Response response) {
            if (uploadResult.getStatus() == 1) {
              Log.d(TAG, "upload successfully!");
              Toast.makeText(getActivity(), "上传成功!", Toast.LENGTH_SHORT)
                  .show();
            } else {
              Log.e(TAG, "upload failed!");
              Toast.makeText(getActivity(), "上传失败!", Toast.LENGTH_SHORT)
                  .show();
            }
            bmp.recycle();
          }

          @Override public void failure(RetrofitError error) {
            bmp.recycle();
          }
        });

我是使用Retrofit框架进行上传的,retrofit内部自己维护它的线程与生命周期,当我退出Activity时,Retrofit内部的网络线程并没有停止;当图片上传成功回调的时候,却发现window已经没了,这样就会抛出异常。

解决方法:在Activity中使用耗时任务本来就不合适,使用Service可以更好的控制回调问题。

3.5. Context与ApplicationContext

class Context ApplicationContext
生命周期 非常长,几乎就是单例
适用场景 Activity中需要UI/素材资源的地方 数据库,包管理,偏好设置,以及Picasso/Retrofit/ShareSDK/Webview等单例框架

Context的生命周期是一个Activiy,而ApplicationContext的生命周期是整个程序。我们最要注意的就是Context的内存泄露。

在Activiy的UI中要使用Context,而在其他的地方比如数据库、网络、系统服务的需要频繁调用Context的情况时,要使用ApplicationContext,以防止内存泄露。

其他的小技巧

以下为各类小问题,就不多介绍了,我会尽量写全所有的泄露。

Listview的item泄露

这个是入门问题了,加入ViewHolder可以减少findViewById的时间,或者使用RecyclerView,来解决“滑动很卡”的问题。这个实质也是一个单例。

StringBuilder

尽量使用StringBuilder,而不用String来累加字符串.

StringBuffer其实是给StringBuilder加了同步锁;其实你使用Log.d(TAG,"xx" + "yy")这类写法后,编译器生成的代码已经自动帮你变成StringBuilder了

多用基本类型

使用int而不用Integer,较少的对象花销。在Android中使用sparseArrayMap取代HashMap就是把key变成了int,而一定程度上减小了内存占用。

当然这个也是相对的,比如RxJava为了提高代码可读性使用了大量的包装,性能损失相比项目整体管理是可以接受的

Native代码不受GC控制

Native层面的代码不受到GC控制,又是长篇大论了,取决于C的水平。Malloc的内存一定要free,free后记得把指针置空,还有new 与delete的区别,一笔带过吧。当然,我们可以用Go语言进行NDK开发,那么又是另一套GC方案了。

使用弱引用

使用弱引用可以防止一定程度的无意引用造成的泄露,比如在Handler中使用弱引用作为参数,当销毁的时候就有可能不会发生泄露。

弱引用随时可能为null,使用前需要判断是否为空。
很久前给Fir.im的SDK提过一个建议,没想到人家开发团队马上就提供了弱引用的支持,赞一个!

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

推荐阅读更多精彩内容

  • 很久以前就谋划着做个学习笔记,但由于懒癌晚期,一直都没能提起干劲儿开始它,但这都快要过年了,不能让这个问题留着过年...
    Will_Change阅读 844评论 0 0
  • Android 内存泄漏总结 内存管理的目的就是让我们在开发中怎么有效的避免我们的应用出现内存泄漏的问题。内存泄漏...
    _痞子阅读 1,622评论 0 8
  • 内存管理的目的就是让我们在开发中怎么有效的避免我们的应用出现内存泄漏的问题。内存泄漏大家都不陌生了,简单粗俗的讲,...
    宇宙只有巴掌大阅读 2,360评论 0 12
  • Android 内存泄漏总结 内存管理的目的就是让我们在开发中怎么有效的避免我们的应用出现内存泄漏的问题。内存泄漏...
    apkcore阅读 1,216评论 2 7
  • 我希望通过这篇文章能够把Android内存相关的基础和大部分内存相关问题如:溢出、泄漏、图片等等产生的都讲解清楚,...
    Cactus_b245阅读 7,080评论 6 82