Android 内存泄漏全解

5,869 阅读20分钟
原文链接: duanmo.me

Java 中的内存分配

主要是分三块:

  • 静态储存区:编译时就分配好,在程序整个运行期间都存在。它主要存放静态数据和常量。
  • 栈区:当方法执行时,会在栈区内存中创建方法体内部的局部变量,方法结束后自动释放内存
  • 堆区:通常存放 new 出来的对象。由 Java 垃圾回收器回收。

栈与堆的区别

栈内存用来存放局部变量和函数参数等。它是先进后出的队列,进出一一对应,不产生碎片,运行效率稳定高。当超过变量的作用域后,该变量也就无效了,分配给它的内存空间也将被释放掉,该内存空间可以被重新使用。

堆内存用于存放对象实例。在堆中分配的内存,将由Java垃圾回收器来自动管理。在堆内存中频繁的 new / delete 会造成大量内存碎片,使程序效率降低。

对于非静态变量的储存位置,我们可以粗暴的认为:

  • 局部变量位于栈中(其中引用变量指向的对象实体存在于堆中)。
  • 成员变量位于堆中。因为它们属于类,该类最终被new成对象,并作为一个整体储存在堆中。

四种引用类型的介绍

GC释放对象的根本原则是该对象不再被引用(强引用)。那么什么是强引用呢?

强引用(Strong Reference)

我们平常用的最多的就是强引用,如下:

Person person = new Person();

JVM 宁可抛出 OOM ,也不会让 GC 回收具有强引用的对象。强引用不使用时,可以通过 obj = null 来显式的设置该对象的所有引用为 null,这样就可以回收该对象了。至于什么时候回收,取决于 GC 的算法,这里不做深究(其实我也不会)。

软引用(Soft Reference)

创建软引用:

SoftReference softRef = new SoftReference<>(str);

如果一个对象只具有软引用,那么在内存空间足够时,垃圾回收器就不会回收它;如果内存空间不足了,就会回收这些对象的内存。只要垃圾回收器没有回收它,该对象就可以被使用。

软引用曾经常被用来作图片缓存,然而谷歌现在推荐用 LruCache 替代,因为 LRU 更高效。

In the past, a popular memory cache implementation was a SoftReference or WeakReference bitmap cache, however this is not recommended. Starting from Android 2.3 (API Level 9) the garbage collector is more aggressive with collecting soft/weak references which makes them fairly ineffective. In addition, prior to Android 3.0 (API Level 11), the backing data of a bitmap was stored in native memory which is not released in a predictable manner, potentially causing an application to briefly exceed its memory limits and crash. > 原文

大致意思是:因为在 Android 2.3 以后,GC 会很频繁,导致释放软引用的频率也很高,这样会降低它的使用效率。并且 3.0 以前 Bitmap 是存放在 Native Memory 中,它的释放是不受 GC 控制的,所以使用软引用缓存 Bitmap 可能会造成 OOM。

弱引用(Weak Reference)

创建弱引用:

WeakReference weakRef = new WeakReference<>(str);

与软引用的区别在于:只具有弱引用的对象拥有更短暂的生命周期。因为在 GC 时,一旦发现了只具有弱引用的对象,不管当前内存空间足够与否,都会回收它的内存。不过,由于垃圾回收器是一个优先级很低的线程,因此不一定会很快发现那些只具有弱引用的对象- -。

虚引用(PhantomReference)

(好像并没有什么用)

顾名思义,就是形同虚设,与其他几种引用都不同,虚引用并不会决定对象的生命周期,也无法通过虚引用获得对象实例。虚引用必须和引用队列(ReferenceQueue)联合使用。当垃圾回收器准备回收一个对象时,如果发现它还有虚引用,就会在回收对象的内存之前,把这个虚引用加入到与之关联的引用队列中。程序可以通过判断引用队列中是否存在该对象的虚引用,来了解这个对象是否将要被回收。

Android的垃圾回收机制简介

Android 系统里面有一个 Generational Heap Memory 模型,系统会根据内存中不同的内存数据类型分别执行不同的 GC 操作。

该模型分为三个区:

  • Young Generation
    • eden
    • Survivor Space
  • Old Generation
  • Permanent Generation

Young Generation

大多数 new 出来的对象都放到 eden 区,当 eden 区填满时,执行 Minor GC(轻量级GC),然后存活下来的对象被转移到 Survivor 区(有 S0,S1 两个)。 Minor GC 也会检查 Survivor 区的对象,并把它们转移到另一个 Survivor 区,这样就总会有一个 Survivor 区是空的。

Old Generation

存放长期存活下来的对象(经过多次 Minor GC 后仍然存活下来的对象) Old Generation 区满了以后,执行 Major GC(大型 GC)。

在Android 2.2 之前,执行 GC 时,应用的线程会被暂停,2.3 开始添加了并发垃圾回收机制。

Permanent Generation

存放方法区。一般存放:

  • 要加载的类的信息
  • 静态变量
  • final常量
  • 属性、方法信息

60 FPS

这里简单的介绍一下帧率的概念,以便于理解为什么大量的 GC 容易引起卡顿。

App 开发时,一般追求界面的帧率达到60 FPS(60 帧/秒),那这个 FPS 是什么概念呢?

  • 10-12 FPS 时可以感受到动画的效果;
  • 24 FPS,可以感受到平滑连贯的动画效果,电影常用帧率(不追求 60 FPS 是节省成本);
  • 60 FPS,达到最流畅的效果,对于更高的FPS,大脑已经难以察觉区别。

Android 每隔 16 ms发出 VSYNC 信号,触发对 UI 的渲染(即每 16 ms绘制一帧),如果整个过程保持在 16 ms以内,那么就会达到 60 FPS 的流畅画面。超过了 16 ms就会造成卡顿。那么如果在 UI 渲染时发生了大量 GC,或者 GC 耗时太长,那么就可能导致绘制过程超过 16 ms从而造成卡顿(FPS 下降、掉帧等),而我们大脑对于掉帧的情况十分敏锐,因此如果没有做好内存管理,将会给用户带来非常不好的体验。

再介绍一下内存抖动的概念,本文后面可能会用到这个概念。

内存抖动:短时间内大量 new 对象,达到 Young Generation 的阈值后触发GC,导致刚 new 出来的对象又被回收。此现象会影响帧率,造成卡顿。

内存抖动在 Android 提供的 Memory Monitor 中大概表现为这样:

内存抖动

Android中常见的内存泄露及解决方案

集合类泄露

如果某个集合是全局性的变量(比如 static 修饰),集合内直接存放一些占用大量内存的对象(而不是通过弱引用存放),那么随着集合 size 的增大,会导致内存占用不断上升,而在 Activity 等销毁时,集合中的这些对象无法被回收,导致内存泄露。比如我们喜欢通过静态 HashMap 做一些缓存之类的事,这种情况要小心,集合内对象建议采用弱引用的方式存取,并考虑在不需要的时候手动释放。

单例造成的内存泄露

单例的静态特性导致其生命周期同应用一样长。

有时创建单例时如果我们需要Context对象,如果传入的是Application的Context那么不会有问题。如果传入的是Activity的Context对象,那么当Activity生命周期结束时,该Activity的引用依然被单例持有,所以不会被回收,而单例的生命周期又是跟应用一样长,所以这就造成了内存泄露。

解决办法一:在创建单例的构造中不直接用传进来的context,而是通过这个context获取Application的Context。代码如下:

public class AppManager {
  private static AppManager instance;
  private Context context;
  private AppManager(Context context) {
     this.context = context.getApplicationContext();// 使用Application 的context
  }
  public static AppManager getInstance(Context context) {
     if (instance != null) {
          instance = new AppManager(context);
     }
     return instance;
  }
}

第二种解决方案:

在构造单例时不需要传入 context,直接在我们的 Application 中写一个静态方法,方法内通过 getApplicationContext 返回 context,然后在单例中直接调用这个静态方法获取 context。

嗯原理上是一样的。

非静态内部类造成的内存泄露

在 Java 中,非静态内部类(包括匿名内部类,比如 Handler, Runnable匿名内部类最容易导致内存泄露)会持有外部类对象的强引用(如 Activity),而静态的内部类则不会引用外部类对象。

非静态内部类或匿名类因为持有外部类的引用,所以可以访问外部类的资源属性成员变量等;静态内部类不行。

因为普通内部类或匿名类依赖外部类,所以必须先创建外部类,再创建普通内部类或匿名类;而静态内部类随时都可以在其他外部类中创建。

以上其实是 Java 基础 0.0

在 Android 中最典型的非静态内部类造成的内存泄露非 Handler 莫属。有些同学写 Handler 时经常这样写:

Handler 内存泄露 看见没,Android 都提示你了这里可能会发生内存泄露!

这里可能有同学不明白为什么,这里创建出来的匿名对象Handler会间接的持有外部类实例Activity的引用,而Handler可能会因为要处理耗时操作导致存活周期超过Activity(handler被main Looper持有),或者Message还未被处理(Message会持有handler的引用),此时就会导致Activity无法被及时回收,造成内存泄露

身为准程序员怎能容忍这一坨黄色的警告?哥教你正确的 Handler 应该怎么写:

public class MainActivity extends Activity {
  StaticHandler mHandler = new StaticHandler(this);

  static class StaticHandler extends Handler {
    WeakReference activityReference;

    StaticHandler(MainActivity activity) {
      activityReference = new WeakReference<>(activity);
    }

    @Override
    public void handleMessage(Message msg) {
      MainActivity activity = activityReference.get();
      if (activity != null) {
        activity.getTextView().setText("测试");
      }
    }
  }
}

如上,通过弱引用持有 Activity 对象,在 Activity 中提供 get 方法让 Handler 可以访问它的成员变量。

通过静态内部类+弱引用解决了。不过这样虽然避免了Activity泄漏,不过Looper线程的消息队列中还是可能会有待处理的消息,所以我们在Activity的Destroy时或者Stop时应该移除消息队列中的消息。

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

另一种常见的 Thread 线程对象导致的内存泄露,原因也是这样的。线程 Thread 对象的 run 任务未执行完之前,它是不会被释放的,而我们经常在 Activity 中 new 一个线程来执行耗时任务,通常也都是通过匿名内部类的方法构造线程对象,因此非常容易导致 Activity 无法及时释放。

WebView 的泄漏

Android 中的 WebView 存在很大的兼容性问题,有些 WebView 甚至存在内存泄露的问题。所以通常根治这个问题的办法是为 WebView 开启另外一个进程,通过 AIDL 与主进程进行通信, WebView 所在的进程可以根据业务的需要选择合适的时机进行销毁,从而达到内存的完整释放。

AlertDialog 造成的内存泄露

new AlertDialog.Builder(this)
    .setPositiveButton("Baguette", new DialogInterface.OnClickListener() {
    @Override public void onClick(DialogInterface dialog, int which) {
      MyActivity.this.makeBread();
    }
  })
    .show();

DialogInterface.OnClickListener 的匿名实现类持有了 MainActivity 的引用;

而在 AlertDialog 的实现中,OnClickListener 类将被包装在一个 Message 对象中(具体可以看 AlertController 类的 setButton 方法),而且这个 Message 会在其内部被复制一份(AlertController 类的 mButtonHandler 中可以看到),两份 Message 中只有一个被 recycle,另一个(OnClickListener 的成员变量引用的 Message 对象)将会泄露!

解决办法:

  • Android 5.0 以上不存在此问题;
  • Message 对象的泄漏无法避免,但是如果仅仅是一个空的 Message 对象,将被放入对象池作为后用,是没有问题的;
  • 让 DialogInterface.OnClickListener 对象不持有外部类的强引用,如用 static 类实现;
  • 在 Activity 退出前 dismiss dialog!

Drawable 引起的内存泄露

Android 在 4.0 以后已经解决了这个问题。这里可以跳过。

当我们屏幕旋转时,默认会销毁掉当前的 Activity,然后创建一个新的 Activity 并保持之前的状态。在这个过程中,Android 系统会重新加载程序的UI视图和资源。假设我们有一个程序用到了一个很大的 Bitmap 图像,我们不想每次屏幕旋转时都重新加载这个 Bitmap 对象,最简单的办法就是将这个 Bitmap 对象使用 static 修饰。

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);
}

但是上面的方法在屏幕旋转时有可能引起内存泄露,因为,当一个 Drawable 绑定到了 View 上,实际上这个 View 对象就会成为这个 Drawable 的一个 callback 成员变量,上面的例子中静态的 sBackground 持有 TextView 对象的引用,而 TextView 持有 Activity 的引用。当屏幕旋转时,Activity 无法被销毁,这样就产生了内存泄露问题。

该问题主要产生在 4.0 以前,因为在 2.3.7 及以下版本 Drawable 的 setCallback 方法的实现是直接赋值,而从 4.0.1 开始,setCallback 采用了弱引用处理这个问题,避免了内存泄露问题。

资源未关闭造成的内存泄露

  • BroadcastReceiver,ContentObserver 之类的没有解除注册啊;
  • Cursor,Stream 之类的没有 close 啊;
  • 无限循环的动画在 Activity 退出前没有停止啊;
  • 一些其他的该 release 的没有 release,该 recycle 的没有 recycle…等等。

总结

我们不难发现,大多数问题都是 static 造成的!

  • 在使用 static 时一定要小心,关注该 static 变量持有的引用情况。在必要情况下使用弱引用的方式来持有一些引用。
  • 在使用非静态内部类时也要注意,毕竟它们持有外部类的引用。(高端一点的使用 RxJava 的同学在 subscribe 时也要注意 unSubscribe 哦)。
  • 注意在生命周期结束时释放资源。
  • 使用属性动画时,不用的时候请停止(尤其是循环播放的动画),不然会产生内存泄露(Activity 无法释放)(View 动画不会)。

几种内存检测工具的介绍

  • Memory Monitor
  • Allocation Tracker
  • Heap Viewer
  • LeakCanary

Memory Monitor

位于 Android Monitor 中(别告诉我你不知道!),该工具可以:

  • 方便的显示内存使用和 GC 情况;
  • 快速定位卡顿是否和 GC 有关;
  • 快速定位 Crash 是否和内存占用过高有关;
  • 快速定位潜在的内存泄露问题(内存占用一直在增长);
  • 但是不能准确的定位问题。

Allocation Tracker

该工具用途:

  • 可以定位代码中分配的对象类型、大小、时间、线程、堆栈等信息;
  • 可以定位内存抖动问题;
  • 配合 Heap Viewer 定位内存泄露问题(可以找出来泄露的对象是在哪创建的等等)。

使用方法: 在 Memory Monitor 中有个 Start Allocation Tracking 按钮即可开始跟踪 在点击停止跟踪后会显示统计结果。

Heap Viewer

该工具用于

  • 显示内存快照信息;
  • 每次 GC 后收集一次信息;
  • 查找内存泄露的利器。

使用方法: 在 Memory Monitor 中有个 Dump Java Heap 按钮,点击即可,在统计报告左上角选按 package 分类。配合 Memory Monitor 的 initiate GC(执行 GC)按钮,可检测内存泄露等情况。

LeakCanary

重要的事情说三遍:

for(int i = 0 ; i < 3 ; i++){
  System.out.println("检测内存泄露的神器!");
}

LeakCanary 具体使用不再赘述,自行 Google。

OOM 主要元凶:大胖子 Bitmap

首先强调一点,加载图片属于耗时操作请放到非 UI 线程进行!

Android 中加载图片时一般是按每像素占 4 byte 来处理的,拿计算器算一下可以发现,如果原封不动的加载一张图片是非常占内存的!因此非常容易 OOM。

网上流行一种说法,是说图片解码后是存放在 Native Memory 中,图片用完要调用 Bitmap.recycle() 方法来回收内存,实际上谷歌不推荐这样做了,因为这样非常容易引发一些别的问题,并且在 Android 3.0 (API 11)以后,解码后的数据已经调整为储存在 Dalvik heap 中,Dalvik 会自动回收内存。详情见Managing Bitmap Memory

那如何优化 Bitmap 从而避免 OOM 或者内存抖动呢?这里提供两种思路:使用对象池和缩放 Bitmap。

使用对象池

在启动时预先申请一块内存给对象池使用。加载图片时根据特定算法,从对象池中找到要淘汰的 Bitmap 对象,将其内存腾出来给新图片用,这样每次加载图片也不用去向 JVM 申请内存,也避免了启动 GC 来腾出内存,可以有效防止内存抖动,提升加载效率。

还有另一种复用 Bitmap 的方式:

在 Android 中可以让 BitmapOption 的 inBitmap 属性指向当前某个已创建的 Bitmap 对象,后续在解码时传入这个 option 就可以复用这个 Bitmap 对象的内存空间(要求两者像素格式必须一样,例如都是 ARGB8888。也可以按像素格式创建不同的对象来复用)。

注意,这个 inBitmap 参数在 API 11-18 时,后续要解码的图片大小必须和当前这个 Bitmap 一模一样,才能复用,否则后面的图片就无法复用了。在 API 19 以后就没这个限制了,只要后续 Bitmap 大小小于等于要复用的 Bitmap 即可。

对 Bitmap 进行缩放

Android 提供了如下几种方法来缩放 Bitmap:

  • createScaledBitmap() 传入指定宽高即可,该方法缺陷是需要传入一个已经加载完毕的 Bitmap 图片。。。都加载完了还要你干嘛?
  • inSampleSize。该值只能是 2 的倍数或者 1。原理是解码时根据这个值,如果是 1,就记录每一个像素的值。如果值为 2,Android 就从每 4 个像素中取出两个像素记录下来。
  • 如果我们需要缩放的倍数不是 2 的倍数,即 inSampleSize 满足不了需求时,可以考虑设置 BitmapOption 的 inScaled 为 true,同时设置 inDensity 和 inTargetDensity 属性,这样就可以指定想要的 Bitmap 为原来的任意分之一大小了。该算法很复杂,如果原图较大,那么缩放加载时可能会耗时较长。可以和 inSampleSize 结合使用,用 inSampleSize 缩放,减小大小后,再用这个方法缩放 。

Bitmap

内存优化

我们主要从减少内存使用的角度来考虑这个问题:

  • 使用更轻量的数据结构,如用 SpareArray 代替 HashMap;
  • 多使用 Google 提供的工具类,他们往往比 Java 的工具类更轻量,优化更好;
  • 避免在 onDraw 方法中创建对象,因为该方法会被频繁调用;
  • 使用对象池,范例:Message;
  • 使用 LRUCache 进行缓存;
  • Bitmap 优化。内存复用,压缩 inSampleSize inBitmap;
  • 用 StringBuilder 进行拼接;
  • 使用枚举时用 Android 提供的注解,不用 Java 的枚举。使用 Java 枚举会导致增加的 dex 文件大小是定义为常量的 10 多倍,占用内存也远大于常量。不信你 Dump 一下看看就知道了!Android 提供了注解来优化枚举,使用方法如下:

enum

  • 避免自动拆装箱。基本数据类型的包装类占用内存较大,如果不是特别需要包装类的话,就不要用包装类(可以从 Allocation Tracker 看到,Integer 要占 16 字节,而 int 只占 4 个字节)。这种情况主要出现在 HashMap 等集合中,这些集合的泛型规定了只能用包装类,所以 get,put 等操作均会涉及到自动拆装箱,在操作频繁时可能会出现性能问题。

包装类

我们可以用 Android 中提供的一些容器来替代 Java 中的一些容器,比如:SparseBoolMap,SparseIntMap,SparseLongMap,LongSparseMap 等,以上这几个容器的 key 都是集合名中的那个基本类型,value 都是 Object。他们都是用 ArrayMap 实现的,ArrayMap 会对 key 占用空间进行压缩,并且可以通过普通 for 循环进行遍历( keyAt(i), valueAt(i) )这样遍历效率高于迭代器遍历(foreach 底层就是迭代器)。在容器 size 小于 1000 或者嵌套 map 的情况下,适合用 Sparse 系列替换 HashMap。

HashMap 的数据结构如下:

HashMap 可以看到,HaspMap 创建时直接申请了一些空间来存 key 的 hash 值,在 key 数量没有占满这些空间时,就很浪费。我们再来看看 ArrayMap 的数据结构 :

ArrayMap

可以看到,不存在空间浪费。

一个简单的内存优化练习

由大头鬼老师提供,项目地址:MemoryBugs,要求运用各种工具查找潜在的内存性能问题。建议各位自己先 clone 下来练习(老油条们就不用啦),再继续往下看,和我的对比一下。我做的也不是标准答案,如有遗漏或不对的地方,欢迎指正。

先装到模拟器上,启动,当点击按钮跳转到 ActivityB 时,LeakCanary 弹出通知,告诉我们 Activity 发生了内存泄露 。

leak 很明显,是 TextView 持有 MainActivity 引用造成的,最简单的解决办法就是取消 sTextView 的 static 关键字,那如果非要采用静态的呢?

我们再用 Dump Heap 查看一下,在 MainActivity 界面,Dump :

dump1

可以看到 ActivityB 数量是 0,MainActivity 数量 1 接下来我们启动 ActivityB,然后点击 initiate GC 将 MainActivity 回收 。

dump2

可以看到 MainActivity 仍然没有被回收。那么该怎么解决呢?一种方式是在 onDestroy 中手动释放:

@Override
protected void onDestroy() {
  super.onDestroy();
  sTextView = null;
}

还可以用过弱引用的方式持有 TextView。

sTextView = new WeakReference<>((TextView) findViewById(R.id.tv_text)).get();

解决后在 ActivityB 界面我们 initial GC 一下,发现 MainActivity 被回收了 。

dump3

这里 Handler 可能也会造成内存泄露,我们在打开 ActivityB 时,尽快 initial GC(考验手速的时刻!),然后 LeakCanary 就弹出了通知:

leak2

显然这里泄露了,我们改写 Handler 为静态内部类就 OK。

private MyHandler mHandler = new MyHandler(this);

public static class MyHandler extends Handler {
  private WeakReference  mWeakReference;

  public MyHandler(MainActivity activity) {
    mWeakReference = new WeakReference<>(activity);
  }

  @Override
  public void handleMessage(Message msg) {
    super.handleMessage(msg);
  }
}

然后我们再点击 STARTALLOCATION 按钮出现了,观察 Memory Monitor,发现出现了内存抖动。 使用 Allocation Tracking 工具重复一遍,统计结果如图 :

track

发现 MainActivity 里有 20001 个对象,基本一半是 Rect 一半是 StringBuilder。。 这里可以把重复创建对象的操作提取出来,用 StringBuffer 替代 String。

mStringBuffer = new StringBuffer("-------: ");
for (int i = 0; i < 10000; i++) {
  if (mRect == null) {          //避免创建大量的 mRect
     mRect = new Rect(0, 0, 100, 100);
  } else {
    mRect.right = 100;
    mRect.bottom = 100;
  }
  mStringBuffer.append(mRect.width()); //避免创建大量的String
  System.out.println(mStringBuffer);
  mStringBuffer.delete(9, mStringBuffer.length());
}

虽然创建的对象是减少了。但是在疯狂的点击按钮的时候,内存还是飙升,仔细看了 startAllocationLargeNumbersOfObjects() 发现,是该方法第一行弹出 Toast 的问题:

Toast 源码:

public static Toast makeText(Context context, CharSequence text, @Duration int duration) {
  Toast result = new Toast(context);

  LayoutInflater inflate = (LayoutInflater)
            context.getSystemService(Context.LAYOUT_INFLATER_SERVICE);
  View v = inflate.inflate(com.android.internal.R.layout.transient_notification, null);
  TextView tv = (TextView)v.findViewById(com.android.internal.R.id.message);
  tv.setText(text);

  result.mNextView = v;
  result.mDuration = duration;

  return result;
}

很明显,通过这个源码发现,Toast.makeText 会返回一个新的 Toast 对象。疯狂的点击按钮,也会使得这个对象创建很多。因为在其他地方也会使用 Toast,所以可以在成员变量位置声明 Toast,然后在 onCreate 里创建其对象,在这个方法里直接调用 Toast 对象 setText 并 show 即可,这里就不贴代码了。

MainActivity 类我大概就知道这些,接下来看看 MyView 类:

@Override
protected void onDraw(Canvas canvas) {
  super.onDraw(canvas);

  RectF rect = new RectF(0, 0, 100, 100);
  Paint paint = new Paint();
  paint.setColor(Color.RED);
  paint.setStrokeWidth(4);
  canvas.drawArc(rect, 0, 180, true, paint);
}

可以看到,在 onDraw 方法中创建了对象,我们把创建对象的操作提取到构造中即可。

参考资料

Managing Bitmap Memory

Android Performance Patterns

Cache Bitmap

Android性能优化之内存篇

Android 内存泄漏总结

Android内存优化之OOM

内存泄露从入门到精通三部曲之基础知识篇

内存泄露从入门到精通三部曲之常见原因与用户实践