您的位置:首页 > 移动开发 > Android开发

Android 内存泄漏与分析方法

2015-05-19 15:31 281 查看

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提供的设备内存监控插件。通过它可以帮助我们了解软件内存使用情况,对性能优化有指导作用。界面显示如下:



界面上显示的内容包括:

- 当前设备(可选)

- 监控进程(可选)

- 内存消耗堆积面积图

- 当前可用内存值

- 已分配内存值

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

Leak Canary

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


如何复制 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


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



自定义

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 监测应用进程使用内存情况。



步骤如下:

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 会自动解析并生成报告,界面如下:



界面中各个视图的作用可参见 官网介绍。具体的分析方法也可通过官方网站和客户端的帮助文档进行学习。

附录

相关资料:

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
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: