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 object 的 Total Size 值。
2. 正常情况下 Total Size 值都会稳定在一个有限的范围内,也就是说由于程序中的的代码良好,没有造成对象不被垃圾回收的情况,所以说虽然我们不断的操作会不断的生成很多对象,而在虚拟机不断的进行GC的过程中,这些对象都被回收了,内存占用量会会落到一个稳定的水平。
3. 如果代码中存在没有释放对象引用的情况,则 data object 的 Total 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
相关文章推荐
- [Android内存分析] 非UI线程使用View.post()方法一处潜在的内存泄漏
- 【腾讯优测干货分享】Android内存泄漏的简单检查与分析方法
- 【腾讯优测干货分享】Android内存泄漏的简单检查与分析方法
- Android内存泄漏的简单检查与分析方法
- Android内存泄漏的简单检查与分析方法
- Android内存泄漏的简单检查与分析方法 ---待完善
- Android 内存泄漏的简单检查与分析方法
- Android内存泄漏的简单检查与分析方法
- Android 内存泄漏分析与解决方法
- Android中常见的内存泄漏分析及应对方法
- Android popupWindow弹出窗体实现方法分析
- Android ROM开发(二)——ROM架构以及Updater-Script脚本分析,常见的Status错误解决的方法
- AndroidStudio + MAT 内存泄漏分析
- Android中的内存泄漏情况分析
- 有关Android Handler内存泄漏分析及解决办法
- Android开发之使用SQLite存储数据的方法分析
- Android的死机、重启问题分析方法
- Android中View绘制流程以及invalidate()等相关方法分析
- 1.13Android 学习+进度之十三-删除冗余代码+注释+分析设计方法
- Android 5.1版本以上WebView内存泄漏问题及快速解决方法