Android 内存泄漏与分析方法

内存泄漏也称作“存储渗漏”,用动态存储分配函数动态开辟的空间,在使用完毕后未释放,结果导致一直占据该内存单元。直到程序结束。内存泄露并非指内存在物理上的消失,二是引用程序分配某段内存后,由于设计错误,导致在释放该段内存之前就失去了对该段内存的控制,从而造成了内存的浪费。
内存泄漏会因为可用内存减少导致计算机的性能下降,最糟糕的情况是软件崩溃或设备停止工作。

常见的泄漏

这里主要罗列了常见的几种泄漏类型。

静态属性

在所有泄漏类型中,这是最常见的一种。

下面这段代码,单例类 StaticFieldHolder 持有一个 staticField 属性。

public class StaticFieldHolder {

    private static StaticFieldHolder sInstance;
    private        Object            staticField;

    public static StaticFieldHolder getInstance() {
        if (sInstance == null) {
            sInstance = new StaticFieldHolder();
        }
        return sInstance;
    }

    public void setStaticField(Object staticField) {
        this.staticField = staticField;
    }

}

如果将 Activity 中的 View 传入,只要 StaticFieldHolder 持有这个对象,即便 Activity 已经 finish,也无法回收。

public class StaticLeakActivity extends AppCompatActivity {

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_test);
        TextView textView = (TextView) findViewById(R.id.test_text_view);

        StaticFieldHolder.getInstance().setStaticField(textView);
    }
}

所以,对静态对象赋值前需要考虑设计是否合理,如果确实需要赋值,则需要在退出时将引用去掉。

@Override
protected void onDestroy() {
    super.onDestroy();
    StaticFieldHolder.getInstance().setStaticField(null);
}

Handler 与 Inner Class

在 Java 中非静态的匿名类都保存了一个所关联的类的引用,因此可以直接调用外部类的方法。如果在使用时不够小心,将可能导致 Activity 无法 GC 造成大量的内存泄漏。
建议:使用内部类之前先考虑能否使用静态内部类。

下面定义一个下载管理类,通过 addTask() 添加下载任务,下载监听回调都保存在 listeners 数组中。

public class DownloadManager {

    private final static DownloadManager instance = new DownloadManager();
    public List<DownloadListener> listeners = new ArrayList<>();

    private DownloadManager() {
    }

    public static DownloadManager getInstance() {
        return instance;
    }

    public void addTask(Object task, DownloadListener listener) {
        listeners.add(listener);
        //do something
    }
}

当在一个对象中创建了 DownloadListener 的匿名内部类,并调用 addTask 方法,将导致泄漏。

public class InnerClassLeakActivity extends AppCompatActivity {

    @Override
    public void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        DownloadManager.getInstance().addTask("Task", new DownloadListener() {
            @Override
            public void onCompleted() {
                //do something
            }
        });
    }
}

解决该种泄漏的方法有两种:

  • 在数组中仅保存匿名内部类的弱引用。修改 DownloadManager 中的 listeners 属性类型。
public List<WeakReference<DownloadListener>>listeners=new ArrayList<>();
  • 第二种是使用静态的匿名内部类,静态内部类不会保存关联类的引用,所以在Activity实例配置发生变化的时候不会造成泄漏。但静态匿名内部类需要考虑使用场景,并不是全都适用。

Cursor

数据库连接是以申请和归还的方式分配的。一个连接的生命周期从申请到关闭,由于关闭连接的操作无法由数据库主动调用,因此需要由申请方在使用后关闭。数据库连接数并不是无限的,当连接数达到一定数目(Android中通常为数百个)必然会出现无法查询数据库的情况。

在 Android 中数据库是 SQLite,操作数据库的是 Cursor。下面例子使用 Cursor 查询手机通讯录:

public class DatabaseLeakActivity extends AppCompatActivity {

    String[] getContacts() {
        Uri contactUri = ContactsContract.Contacts.CONTENT_URI;
        String[] columns = {ContactsContract.Contacts.DISPLAY_NAME};
        ContentResolver resolver = getContentResolver();
        Cursor cursor = resolver.query(contactUri, columns, null, null, null);
        int nameIndex = cursor.getColumnIndex(ContactsContract.Contacts.DISPLAY_NAME);
        List<String> result = new ArrayList<>();
        cursor.moveToFirst();
        while (!cursor.isAfterLast()) {
            result.add(cursor.getString(nameIndex));
            cursor.moveToNext();
        }
        return result.toArray(new String[result.size()]);
    }

}

代码中 getContacts() 方法在查询到通讯录后,没有调用 cursor.close() 方法,将导致数据库连接泄漏。正确的做法是在 getContacts 函数返回前关闭数据库连接,代码修改如下:

String[] getContacts() {
    Uri contactUri = ContactsContract.Contacts.CONTENT_URI;
    String[] columns = {ContactsContract.Contacts.DISPLAY_NAME};
    ContentResolver resolver = getContentResolver();
    Cursor cursor = resolver.query(contactUri, columns, null, null, null);
    int nameIndex = cursor.getColumnIndex(ContactsContract.Contacts.DISPLAY_NAME);
    List<String> result = new ArrayList<>();
    cursor.moveToFirst();
    while (!cursor.isAfterLast()) {
        result.add(cursor.getString(nameIndex));
        cursor.moveToNext();
    }
    cursor.close();
    return result.toArray(new String[result.size()]);
}

Thread

在 Activity 的生命周期中,一个长时间运行的任务也可能造成内存泄露。我们先来看下面这段代码:

public class ThreadLeakActivity extends AppCompatActivity {

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        exampleOne();
    }

    private void exampleOne() {
        new Thread() {
            @Override
            public void run() {
                while (true) {
                    SystemClock.sleep(1000);
                }
            }
        }.start();
    }
}

Activity 使用匿名内部类的方式创建了一个心跳线程,当 Activity 被关闭后,由于匿名内部类包含了 Activity 的引用,导致 Activity 无法回收。这种情况下,可以考虑使用静态的类名内部类,创建的对象不会包含 Activity 的引用。另外,也可以考虑在 Activity 关闭后结束此线程。

public class ThreadFixLeakActivity extends AppCompatActivity {
    private MyThread mThread;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        mThread = new MyThread();
        mThread.start();
    }

    private static class MyThread extends Thread {
        private boolean mRunning = false;

        @Override
        public void run() {
            mRunning = true;
            while (mRunning) {
                SystemClock.sleep(1000);
            }
        }

        public void close() {
            mRunning = false;
        }
    }

    @Override
    protected void onDestroy() {
        super.onDestroy();
        mThread.close();
    }
}

Bitmap

对于很多图像处理,大的 Bitmap 对象可能直接导致软件崩溃。目前 Android 设备的 RAM 差距比较大,很多低端配置的256MB RAM 或512MB RAM 由于运行了太多的后台任务或酷炫主题,导致了处理一些高像素的图片,比如500万或800万像素的照片很容易崩溃。通过以下方法可以再一定程度上降低泄漏的可能:
- 通过减少工作区域可以有效的降低RAM使用。
由于内存中Bitmap是以是DIB(设备无关位图)方式存储,所以ARGB的图片占用内存为 4 * height * width,比如500万像素的图片,占用内存就是 500 x 4 = 2000万字节就是19MB左右。同时 Java VM 的异常处理机制和绘图方法可能在内部产生副本,追踪消耗的运行内存是十分庞大的,对于图片打开时就进行压缩可以使用 android.graphics.BitmapFactory 的相关方法来处理。另外,Android API 也提供了工具类可以直接使用 ThumbnailUtils

  • 及时的显示执行Bitmap的recycle方法,以及是当时可以调用Runtime的gc方法,提示虚拟机尽快释放掉内存。
  • 使用优秀的第三方图形加载库,ImageLoader、Fresco 等等。

Drawable

Android 把可绘制的对象抽象为 Drawable,不同的图形图像资源就代表着不同的 Drawable 类型。在平时开发中我们经常会用到。Drawable 将如何引起内存泄露?先看一下下面的代码:

public class DrawableLeakActivity extends AppCompatActivity {

    private static Drawable sBackground;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        TextView label = new TextView(this);
        label.setText("Leaks are bad");
        if (sBackground == null) {
            sBackground = getResources().getDrawable(R.drawable.ic_launcher);
        }
        label.setBackgroundDrawable(sBackground);
        setContentView(label);
    }
}

表面上看似乎没有问题。我们看一下 View 内提供 setBackgroundDrawable(Drawable background) 的源码:

@Deprecated
public void setBackgroundDrawable(Drawable background) {
    if (background == mBackground) {
        return;
    }

    ...

    if (mBackground != null) {
        mBackground.setCallback(null);
        unscheduleDrawable(mBackground);
    }

    if (background != null) {

        ...

        background.setCallback(this);// view 被 Drawable 对象引用

        ...

    }
    ...
}

假设 Activity 发生的转屏,Activity 将做一次 UI 重建。这时候就泄漏了第一次屏幕旋转之前创建的第一个 Activity。当一个 Drawable 被绑定到一个 View时,这个 View 就被设定成这个 Drawable 的 Callback,这意味着 Drawable 拥有了对这个 TextView 的引用。而 TextView 又拥有对 activity 的引用,造成 Activity 无法回收,造成泄漏。

为了避免在使用 setBackgroundDrawable 是造成泄漏,可以考虑:
- 使用 setBackground(Drawable drawable) 方法,setBackgroundDrawable 方法已经在高版本中被标注为过期的方法,应该避免使用。
- 在 Activity 被销毁的时候,将存储的 drawable 的 callbacks 置空。

Context

Context 是开发中使用最多的一个类,也是最容易造成泄漏的类。一般可能会碰到两种 Context:Activity 和 Application,通常我们都将前者作为需要传入到类或者方法里。导致 Activity 在其预期的作用于外被长期持有而无法回收。以下两个简单的方法可以避免 Context 相关的内存泄漏:
- 避免将 Context 带出它本身的作用域
- 使用Application上下文

Fragment

待补充

泄漏检测

GC 日志信息

每一次 GC 发生,在 Debug Build 在 Logcat 中会打印 GC 的信息,格式如下:

D/dalvikvm: <GC_Reason> <Amount_freed>, <Heap_stats>, <External_memory_stats>, <Pause_time>

<GC_Reason> 即 GC 原因,类型包括:
- GC_CONCURRENT
当堆中对象数量达到一定时触发的垃圾收集
- GC_FOR_MALLOC
在内存已满的情况下分配内存,此时系统会暂停程序并回收内存
- GC_EXTERNAL_ALLOC
出现在API 10及以下,为外部分配内存(native memory or NIO buffer)所造成的垃圾回收,高版本全部分配在Dalvik Heap中。
- GC_HPROF_DUMP_HEAP
创建FPFOR文件来分析Heap时所造成的垃圾收集
- GC_EXPLICIT
对垃圾收集的显式调用(System.gc)

<Amount_freed> 表示此次垃圾回收的内存大小

<Heap_stats> 表示空闲内存百分比,被分配的对象数量/堆的总大小

<External_memory_stats> 表示 API 10 及以上的外部分配内存,已分配内存/导致垃圾回收的界限

<Pause_time> 堆越大,暂停时间越长。Concurrent 类型的提供两个暂停时间,一个在回收开始,一个在回收快要结束的时候。

例如:

D/dalvikvm( 9050): GC_CONCURRENT freed 2049K, 65% free 3571K/
9991K, external 4703K/5261K, paused 2ms+2ms

IDEA Memory Monitor

Memory Monitor 是IntelliJ IDEA提供的设备内存监控插件。通过它可以帮助我们了解软件内存使用情况,对性能优化有指导作用。界面显示如下:

android之内存泄漏 android内存泄漏分析_Android

界面上显示的内容包括:
- 当前设备(可选)
- 监控进程(可选)
- 内存消耗堆积面积图
- 当前可用内存值
- 已分配内存值

通过分析内存堆积面积图,可以知道内存分配与回收的趋势。通过比较某个(某一系列)操作前后的内存大小,可以粗略判断是否有内存泄漏的情况。

Leak Canary

Square 组织开发的 Android 与 Java 平台的内存泄漏检测第三方库。在代码中使用 LeakCanary 添加内存监控,软件运行过程中,如果检测到内存泄漏,LeakCanary 将在通知栏显示泄漏信息,并能够精确到被泄漏的对象。

android之内存泄漏 android内存泄漏分析_android之内存泄漏_02

开始使用

在 build.gradle 中加入引用:

dependencies {
   debugCompile 'com.squareup.leakcanary:leakcanary-android:1.3'
   releaseCompile 'com.squareup.leakcanary:leakcanary-android-no-op:1.3'
}

在 Application 中:

public class ExampleApplication extends Application {

  @Override public void onCreate() {
    super.onCreate();
    LeakCanary.install(this);
  }
}

这样,就万事俱备了! 在 Debug Build 中,如果检测到某个 Activity 有内存泄露,LeakCanary 就是自动地显示一个通知。

使用进阶

使用 RefWatcher 监控那些本该被回收的对象。

RefWatcher refWatcher = {...};
// 监控
refWatcher.watch(schrodingerCat);

LeakCanary.install() 会返回一个预定义的 RefWatcher,同时也会启用一个 ActivityRefWatcher,用于自动监控调用 Activity.onDestroy() 之后泄漏的 activity。

public class ExampleApplication extends Application {

  public static RefWatcher getRefWatcher(Context context) {
    ExampleApplication application = (ExampleApplication) context.getApplicationContext();
    return application.refWatcher;
  }

  private RefWatcher refWatcher;

  @Override public void onCreate() {
    super.onCreate();
    refWatcher = LeakCanary.install(this);
  }
}

使用 RefWatcher 监控 Fragment:

public abstract class BaseFragment extends Fragment {

  @Override public void onDestroy() {
    super.onDestroy();
    RefWatcher refWatcher = ExampleApplication.getRefWatcher(getActivity());
    refWatcher.watch(this);
  }
}

如何复制 leak trace

在 Logcat 中,可以看到类似这样的 leak trace:

In com.example.leakcanary:1.0:1 com.example.leakcanary.MainActivity has leaked:

* GC ROOT thread java.lang.Thread.<Java Local> (named 'AsyncTask #1')
* references com.example.leakcanary.MainActivity$3.this$0 (anonymous class extends android.os.AsyncTask)
* leaks com.example.leakcanary.MainActivity instance

* Reference Key: e71f3bf5-d786-4145-8539-584afaecad1d
* Device: Genymotion generic Google Nexus 6 - 5.1.0 - API 22 - 1440x2560 vbox86p
* Android Version: 5.1 API: 22
* Durations: watch=5086ms, gc=110ms, heap dump=435ms, analysis=2086ms

甚至可以通过分享按钮把这些东西分享出去。

android之内存泄漏 android内存泄漏分析_ide_03

自定义

UI 样式

DisplayLeakActivity 有一个默认的图标和标签,只要在 APP 资源中,替换以下资源就可。

res/
  drawable-hdpi/
    __leak_canary_icon.png
  drawable-mdpi/
    __leak_canary_icon.png
  drawable-xhdpi/
    __leak_canary_icon.png
  drawable-xxhdpi/
    __leak_canary_icon.png
  drawable-xxxhdpi/
    __leak_canary_icon.png
<?xml version="1.0" encoding="utf-8"?>
<resources>
  <string name="__leak_canary_display_activity_label">MyLeaks</string>
</resources>
保存 leak trace

在 APP 的目录中,DisplayLeakActivity 保存了 7 个 dump 文件和 leak trace。可以在 APP 中定义 R.integer.__leak_canary_max_stored_leaks 来覆盖类库的默认值。

<?xml version="1.0" encoding="utf-8"?>
<resources>
  <integer name="__leak_canary_max_stored_leaks">20</integer>
</resources>

此外,还支持将trace上传到服务器。

DDMS Heap

Android SDK Tools 中的 DDMS 也提供内存监测工具 Heap,可以使用 Heap 监测应用进程使用内存情况。

android之内存泄漏 android内存泄漏分析_Android_04

步骤如下:
1. 打开 DDMS,确认 Devices 视图、Heap 视图都已经打开
2. 启动模拟器或连接上手机(处于 USB 调试模式),在 DDMS 的 Devices 视图中看到设备和部分进程信息
3. 点击选中要监测的进程,比如 com.nd.hy.android.memoryleak.sample 进程
4. 点击 Update Heap 图标
5. 点击 Heap 视图中的 Cause GC 按钮
6. 查看 Heap 视图中的内存使用详细情况 [如上图所示]

Heap 视图中有一个 Type 叫做 data object,即数据对象,也就是程序中大量存在的类类型的对象。在 data objet 一行中有一列是 Total Size,其值就是当前进程中所有 Java 数据对象的内存总量,一般情况下,这个值的大小决定了是否会有内存泄漏。可以这样判断:
1. 不断的操作当前应用,同时注意观察 data objectTotal Size 值。
2. 正常情况下 Total Size 值都会稳定在一个有限的范围内,也就是说由于程序中的的代码良好,没有造成对象不被垃圾回收的情况,所以说虽然我们不断的操作会不断的生成很多对象,而在虚拟机不断的进行GC的过程中,这些对象都被回收了,内存占用量会会落到一个稳定的水平。
3. 如果代码中存在没有释放对象引用的情况,则 data objectTotal Size 值在每次GC后不会有明显的回落,随着操作次数的增多 Total Size 的值会越来越大。

MAT(Memory Analyzer Tool)

如果使用 DDMS 确实发现了我们的程序中存在内存泄漏,那又如何定位到具体出现问题的代码片段,最终找到问题所在呢?MAT 正好可以满足这个需求。MAT是一个Eclipse插件,同时也有单独的RCP客户端

使用 MAT 进行内存分析需要几个步骤:
- 生成 .hprof 文件
可以使用 DDMS Heap,在 Devices 视图中点击 Dump HPROF file 生成 .hprof 文件。或者使用 android.os.Debug.dumpHprofData(String fileName) 方法在代码中保存文件。

  • 打开 MAT,选择 File -> Open Heap Dump,导入 .hprof 文件
    如果出现 Unknown HPROF Version (JAVA PROFILE 1.0.3) 的异常是由于 .hprof 文件格式与标准的 Java hprof 文件格式标准不一样,根本原因是两者的虚拟机不一致导致的。只需要使用SDK中自带的转换工具即可,执行命令:
hprof-conv 源文件 目标文件
  • 导入成功后,MAT 会自动解析并生成报告,界面如下:

android之内存泄漏 android内存泄漏分析_android之内存泄漏_05