内存泄漏也称作“存储渗漏”,用动态存储分配函数动态开辟的空间,在使用完毕后未释放,结果导致一直占据该内存单元。直到程序结束。内存泄露并非指内存在物理上的消失,二是引用程序分配某段内存后,由于设计错误,导致在释放该段内存之前就失去了对该段内存的控制,从而造成了内存的浪费。
内存泄漏会因为可用内存减少导致计算机的性能下降,最糟糕的情况是软件崩溃或设备停止工作。
这里主要罗列了常见的几种泄漏类型。
在所有泄漏类型中,这是最常见的一种。
下面这段代码,单例类 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);
}
在 Java 中非静态的匿名类都保存了一个所关联的类的引用,因此可以直接调用外部类的方法。如果在使用时不够小心,将可能导致 Activity 无法 GC 造成大量的内存泄漏。
建议:使用内部类之前先考虑能否使用静态内部类。
下面定义一个下载管理类,通过 addTask()
添加下载任务,下载监听回调都保存在 listeners
数组中。
public class DownloadManager {
private final static DownloadManager instance = new DownloadManager();
public List 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>listeners=new ArrayList<>();
数据库连接是以申请和归还的方式分配的。一个连接的生命周期从申请到关闭,由于关闭连接的操作无法由数据库主动调用,因此需要由申请方在使用后关闭。数据库连接数并不是无限的,当连接数达到一定数目(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 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 result = new ArrayList<>();
cursor.moveToFirst();
while (!cursor.isAfterLast()) {
result.add(cursor.getString(nameIndex));
cursor.moveToNext();
}
cursor.close();
return result.toArray(new String[result.size()]);
}
在 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 对象可能直接导致软件崩溃。目前 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。
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:Activity 和 Application,通常我们都将前者作为需要传入到类或者方法里。导致 Activity 在其预期的作用于外被长期持有而无法回收。以下两个简单的方法可以避免 Context 相关的内存泄漏:
- 避免将 Context 带出它本身的作用域
- 使用Application上下文
待补充
每一次 GC 发生,在 Debug Build 在 Logcat 中会打印 GC 的信息,格式如下:
D/dalvikvm: <GC_Reason> <Amount_freed>, <Heap_stats>, <External_memory_stats>, <Pause_time>
即 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)
表示此次垃圾回收的内存大小
表示空闲内存百分比,被分配的对象数量/堆的总大小
表示 API 10 及以上的外部分配内存,已分配内存/导致垃圾回收的界限
堆越大,暂停时间越长。Concurrent 类型的提供两个暂停时间,一个在回收开始,一个在回收快要结束的时候。
例如:
D/dalvikvm( 9050): GC_CONCURRENT freed 2049K, 65% free 3571K/
9991K, external 4703K/5261K, paused 2ms+2ms
Memory Monitor 是IntelliJ IDEA提供的设备内存监控插件。通过它可以帮助我们了解软件内存使用情况,对性能优化有指导作用。界面显示如下:
界面上显示的内容包括:
- 当前设备(可选)
- 监控进程(可选)
- 内存消耗堆积面积图
- 当前可用内存值
- 已分配内存值
通过分析内存堆积面积图,可以知道内存分配与回收的趋势。通过比较某个(某一系列)操作前后的内存大小,可以粗略判断是否有内存泄漏的情况。
Square 组织开发的 Android 与 Java 平台的内存泄漏检测第三方库。在代码中使用 LeakCanary 添加内存监控,软件运行过程中,如果检测到内存泄漏,LeakCanary 将在通知栏显示泄漏信息,并能够精确到被泄漏的对象。
在 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);
}
}
在 Logcat 中,可以看到类似这样的 leak trace:
In com.example.leakcanary:1.0:1 com.example.leakcanary.MainActivity has leaked:
* GC ROOT thread java.lang.Thread. (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
甚至可以通过分享按钮把这些东西分享出去。
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
<resources>
<string name="__leak_canary_display_activity_label">MyLeaksstring>
resources>
在 APP 的目录中,DisplayLeakActivity 保存了 7 个 dump 文件和 leak trace。可以在 APP 中定义 R.integer.__leak_canary_max_stored_leaks 来覆盖类库的默认值。
<resources>
<integer name="__leak_canary_max_stored_leaks">20integer>
resources>
此外,还支持将trace上传到服务器。
Android SDK Tools 中的 DDMS 也提供内存监测工具 Heap,可以使用 Heap 监测应用进程使用内存情况。
步骤如下:
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 object 的 Total Size 值。
2. 正常情况下 Total Size 值都会稳定在一个有限的范围内,也就是说由于程序中的的代码良好,没有造成对象不被垃圾回收的情况,所以说虽然我们不断的操作会不断的生成很多对象,而在虚拟机不断的进行GC的过程中,这些对象都被回收了,内存占用量会会落到一个稳定的水平。
3. 如果代码中存在没有释放对象引用的情况,则 data object 的 Total Size 值在每次GC后不会有明显的回落,随着操作次数的增多 Total Size 的值会越来越大。
如果使用 DDMS 确实发现了我们的程序中存在内存泄漏,那又如何定位到具体出现问题的代码片段,最终找到问题所在呢?MAT 正好可以满足这个需求。MAT是一个Eclipse插件,同时也有单独的RCP客户端,下载地址、介绍及详细使用教程请参见 官方网站。
使用 MAT 进行内存分析需要几个步骤:
- 生成 .hprof 文件
可以使用 DDMS Heap,在 Devices 视图中点击 Dump HPROF file 生成 .hprof 文件。或者使用 android.os.Debug.dumpHprofData(String fileName)
方法在代码中保存文件。
Unknown HPROF Version (JAVA PROFILE 1.0.3)
的异常是由于 .hprof 文件格式与标准的 Java hprof 文件格式标准不一样,根本原因是两者的虚拟机不一致导致的。只需要使用SDK中自带的转换工具即可,执行命令:hprof-conv 源文件 目标文件
界面中各个视图的作用可参见 官网介绍。具体的分析方法也可通过官方网站和客户端的帮助文档进行学习。
相关资料:
Memory Manager for Android APPs: http://dubroy.com/memory_management_for_android_apps.pdf
Managing Your App’s Memory: https://developer.android.com/training/articles/memory.html
Android Memory Leaks Or Different Ways to Leak: http://evendanan.net/2013/02/Android-Memory-Leaks-OR-Different-Ways-to-Leak
Activitys, Threads, & Memory Leaks: http://www.androiddesignpatterns.com/2013/04/activitys-threads-memory-leaks.html
Leak Canary: https://github.com/square/leakcanary