如何检测 Android Cursor 泄漏

Posted

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了如何检测 Android Cursor 泄漏相关的知识,希望对你有一定的参考价值。

简介:
本文介绍如何在 android 检测 Cursor 泄漏的原理以及使用方法,还指出几种常见的出错示例。有一些泄漏在代码中难以察觉,但程序长时间运行后必然会出现异常。同时该方法同样适合于其他需要检测资源泄露的情况。

最近发现某蔬菜手机连接程序在查询媒体存储(MediaProvider)数据库时出现严重 Cursor 泄漏现象,运行一段时间后会导致系统中所有使用到该数据库的程序无法使用。另外在工作中也常发现有些应用有 Cursor 泄漏现象,由于需要长时间运行才会出现异常,所以有的此类 bug 很长时间都没被发现。
但是一旦 Cursor 泄漏累计到一定数目(通常为数百个)必然会出现无法查询数据库的情况,只有等数据库服务所在进程死掉重启才能恢复正常。通常的出错信息如下,指出某 pid 的程序打开了 866 个 Cursor 没有关闭,导致了 exception:
3634 3644 E JavaBinder: *** Uncaught remote exception! (Exceptions are not yet supported across processes.)
3634 3644 E JavaBinder: android.database.CursorWindowAllocationException: Cursor window allocation of 2048 kb failed. # Open Cursors=866 (# cursors opened by pid 1565=866)
3634 3644 E JavaBinder: at android.database.CursorWindow.(CursorWindow.java:104)
3634 3644 E JavaBinder: at android.database.AbstractWindowedCursor.clearOrCreateWindow(AbstractWindowedCursor.java:198)
3634 3644 E JavaBinder: at android.database.sqlite.SQLiteCursor.fillWindow(SQLiteCursor.java:147)
3634 3644 E JavaBinder: at android.database.sqlite.SQLiteCursor.getCount(SQLiteCursor.java:141)
3634 3644 E JavaBinder: at android.database.CursorToBulkCursorAdaptor.getBulkCursorDescriptor(CursorToBulkCursorAdaptor.java:143)
3634 3644 E JavaBinder: at android.content.ContentProviderNative.onTransact(ContentProviderNative.java:118)
3634 3644 E JavaBinder: at android.os.Binder.execTransact(Binder.java:367)
3634 3644 E JavaBinder: at dalvik.system.NativeStart.run(Native Method)

1. Cursor 检测原理
在 Cursor 对象被 JVM 回收运行到 finalize() 方法的时候,检测 close() 方法有没有被调用,此办法在 ContentResolver 里面也得到应用。简化后的示例代码如下:
1 import android.database.Cursor;
2 import android.database.CursorWrapper;
3 import android.util.Log;
4
5 public class TestCursor extends CursorWrapper
6 private static final String TAG = "TestCursor";
7 private boolean mIsClosed = false;
8 private Throwable mTrace;
9
10 public TestCursor(Cursor c)
11 super(c);
12 mTrace = new Throwable("Explicit termination method \'close()\' not called");
13
14
15 @Override
16 public void close()
17 mIsClosed = true;
18
19
20 @Override
21 public void finalize() throws Throwable
22 try
23 if (mIsClosed != true)
24 Log.e(TAG, "Cursor leaks", mTrace);
25
26 finally
27 super.finalize();
28
29
30

然后查询的时候,把 TestCursor 作为查询结果返回给 APP:
1 return new TestCursor(cursor); // cursor 是普通查询得到的结果,例如从 ContentProvider.query()

该方法同样适合于所有需要检测显式释放资源方法没有被调用的情形,是一种通用方法。但在 finalize() 方法里检测需要注意
优点:准确。因为该资源在 Cursor 对象被回收时仍没被释放,肯定是发生了资源泄露。
缺点:依赖于 finalize() 方法,也就依赖于 JVM 的垃圾回收策略。例如某 APP 现在有 10 个 Cursor 对象泄露,并且这 10 个对象已经不再被任何引用指向处于可回收状态,但是 JVM 可能并不会马上回收(时间不可预测),如果你现在检查不能够发现问题。另外,在某些情况下就算对象被回收 finalize() 可能也不会执行,也就是不能保证检测出所有问题。关于 finalize() 更多信息可以参考《Effective Java 2nd Edition》的 Item 7: Avoid Finalizers
2. 使用方法
对于 APP 开发人员
从 GINGERBREAD 开始 Android 就提供了 StrictMode 工具协助开发人员检查是否不小心地做了一些不该有的操作。使用方法是在 Activity 里面设置 StrictMode,下面的例子是打开了检查泄漏的 SQLite 对象以及 Closeable 对象(普通 Cursor/FileInputStream 等)的功能,发现有违规情况则记录 log 并使程序强行退出。
1 import android.os.StrictMode;
2
3 public class TestActivity extends Activity
4 private static final boolean DEVELOPER_MODE = true;
5 public void onCreate()
6 if (DEVELOPER_MODE)
7 StrictMode.setVMPolicy(new StrictMode.VMPolicy.Builder()
8 .detectLeakedSqlLiteObjects()
9 .detectLeakedClosableObjects()
10 .penaltyLog()
11 .penaltyDeath()
12 .build());
13
14 super.onCreate();
15
16

对于 framework 开发人员
如果是通过 ContentProvider 提供数据库数据,在 ContentResolver 里面已有 CloseGuard 类实行类似检测,但需要自行打开(上例也是打开 CloseGuard):
1 CloseGuard.setEnabled(true);

更值得推荐的办法是按照本文第一节中的检测原理,在 ContentResolver 内部类 CursorWrapperInner 里面加入。其他需要检测类似于资源泄漏的,同样可以使用该检测原理。
3. 容易出错的地方
忘记调用 close() 这种低级错误没什么好说的,这种应该也占不小的比例。下面说说不太明显的例子。
提前返回
有时候粗心会犯这种错误,在 close() 调用之前就 return 了,特别是函数比较大逻辑比较复杂时更容易犯错。这种情况可以通过把 close() 放在 finally 代码块解决
1 private void method()
2 Cursor cursor = query(); // 假设 query() 是一个查询数据库返回 Cursor 结果的函数
3 if (flag == false) // !!提前返回
4 return;
5
6 cursor.close();
7

类的成员变量
假设类里面有一个在类全局有效的成员变量,在方法 A 获取了查询结果,后面在其他地方又获取了一次查询结果,那么第二次查询的时候就应该先把前面一个 Cursor 对象关闭。
1 public class TestCursor
2 private Cursor mCursor;
3
4 private void methodA()
5 mCursor = query();
6
7
8 private void methodB()
9 // !!必须先关闭上一个 cursor 对象
10 mCursor = query();
11
12

注意:曾经遇到过有人对 mCursor 感到疑惑,明明是同一个变量为什么还需要先关闭?首先 mCursor 是一个 Cursor 对象的引用,在 methodA 时 mCursor 指向了 query() 返回的一个 Cursor 对象 1;在 methodB() 时它又指向了返回的另外一个 Cursor 对象 2。在指向 Cursor 对象 2 之前必须先关闭 Cursor 对象 1,否则就出现了 Cursor 对象 1 在 finalize() 之前没有调用 close() 的情况。
异常处理
打开和关闭 Cursor 之间的代码出现 exception,导致没有跑到关闭的地方:
1 try
2 Cursor cursor = query();
3 // 中间省略某些出现异常的代码
4 cursor.close();
5 catch (Exception e)
6 // !!出现异常没跑到 cursor.close()
7

这种情况应该把 close() 放到 finally 代码块里面:
1 Cursor cursor = null;
2 try
3 cursor = query();
4 // 中间省略某些出现异常的代码
5 catch (Exception e)
6 // 出现异常
7 finally
8 if (cursor != null)
9 cursor.close();
10
参考技术A 有一些泄漏在代码中难以察觉,但程序长时间运行后必然会出现异常。同时该方法同样适合于其他需要检测资源泄露的情况。
最近发现某蔬菜手机连接程序在查询媒体存储(MediaProvider)数据库时出现严重 Cursor
泄漏现象,运行一段时间后会导致系统中所有使用到该数据库的程序无法使用。另外在工作中也常发现有些应用有 Cursor
泄漏现象,由于需要长时间运行才会出现异常,所以有的此类 bug 很长时间都没被发现。但是一旦 Cursor
泄漏累计到一定数目(通常为数百个)必然会出现无法查询数据库的情况,只有等数据库服务所在进程死掉重启才能恢复正常。通常的出错信息如下,指出某
pid 的程序打开了 866 个 Cursor 没有关闭,导致了 exception:3634 3644 E JavaBinder: ***
Uncaught remote exception! (Exceptions are not yet supported across
processes.) 3634 3644 E JavaBinder:
android.database.CursorWindowAllocationException: Cursor window
allocation of 2048 kb failed. # Open Cursors=866 (# cursors opened by
pid 1565=866) 3634 3644 E JavaBinder: at
android.database.CursorWindow.(CursorWindow.java:104) 3634 3644 E
JavaBinder: at
android.database.AbstractWindowedCursor.clearOrCreateWindow(AbstractWindowedCursor.java:198)
3634 3644 E JavaBinder: at
android.database.sqlite.SQLiteCursor.fillWindow(SQLiteCursor.java:147)
3634 3644 E JavaBinder: at
android.database.sqlite.SQLiteCursor.getCount(SQLiteCursor.java:141)
3634 3644 E JavaBinder: at
android.database.CursorToBulkCursorAdaptor.getBulkCursorDescriptor(CursorToBulkCursorAdaptor.java:143)
3634 3644 E JavaBinder: at
android.content.ContentProviderNative.onTransact(ContentProviderNative.java:118)
3634 3644 E JavaBinder: at
android.os.Binder.execTransact(Binder.java:367) 3634 3644 E JavaBinder:
at dalvik.system.NativeStart.run(Native Method) 1. Cursor 检测原理在Cursor
对象被 JVM 回收运行到 finalize() 方法的时候,检测 close() 方法有没有被调用,此办法在 ContentResolver
里面也得到应用。简化后的示例代码如下: 1import android.database.Cursor; 2import
android.database.CursorWrapper; 3import android.util.Log; 4 5publicclass
TestCursor extends CursorWrapper 6privatestaticfinal String TAG =
"TestCursor"; 7privateboolean mIsClosed = false; 8private Throwable
mTrace; 910public TestCursor(Cursor c) 11super(c); 12 mTrace = new
Throwable("Explicit termination method 'close()' not called"); 13 1415
@Override 16publicvoid close() 17 mIsClosed = true; 18 1920
@Override 21publicvoid finalize() throws Throwable 22try 23if
(mIsClosed != true) 24 Log.e(TAG, "Cursor leaks", mTrace); 25 26
finally 27super.finalize(); 28 29 30 然后查询的时候,把 TestCursor
作为查询结果返回给 APP:1returnnew TestCursor(cursor); // cursor 是普通查询得到的结果,例如从
ContentProvider.query() 该方法同样适合于所有需要检测显式释放资源方法没有被调用的情形,是一种通用方法。但在
finalize() 方法里检测需要注意优点:准确。因为该资源在 Cursor 对象被回收时仍没被释放,肯定是发生了资源泄露。缺点:依赖于
finalize() 方法,也就依赖于 JVM 的垃圾回收策略。例如某 APP 现在有 10 个 Cursor 对象泄露,并且这 10
个对象已经不再被任何引用指向处于可回收状态,但是 JVM
可能并不会马上回收(时间不可预测),如果你现在检查不能够发现问题。另外,在某些情况下就算对象被回收 finalize()
可能也不会执行,也就是不能保证检测出所有问题。关于 finalize() 更多信息可以参考《Effective Java 2nd
Edition》的 Item 7: Avoid Finalizers2. 使用方法对于APP 开发人员从GINGERBREAD 开始
Android 就提供了 StrictMode 工具协助开发人员检查是否不小心地做了一些不该有的操作。使用方法是在 Activity 里面设置
StrictMode,下面的例子是打开了检查泄漏的 SQLite 对象以及 Closeable 对象(普通
Cursor/FileInputStream 等)的功能,发现有违规情况则记录 log 并使程序强行退出。 1import
android.os.StrictMode; 2 3publicclass TestActivity extends Activity
4privatestaticfinalboolean DEVELOPER_MODE = true; 5publicvoid onCreate()
6if (DEVELOPER_MODE) 7 StrictMode.setVMPolicy(new
StrictMode.VMPolicy.Builder() 8 .detectLeakedSqlLiteObjects() 9
.detectLeakedClosableObjects() 10 .penaltyLog() 11 .penaltyDeath() 12
.build()); 13 14super.onCreate(); 15 16 对于framework 开发人员如果是通过
ContentProvider 提供数据库数据,在 ContentResolver 里面已有 CloseGuard
类实行类似检测,但需要自行打开(上例也是打开 CloseGuard):1
CloseGuard.setEnabled(true);更值得推荐的办法是按照本文第一节中的检测原理,在 ContentResolver 内部类
CursorWrapperInner 里面加入。其他需要检测类似于资源泄漏的,同样可以使用该检测原理。3. 容易出错的地方忘记调用
close() 这种低级错误没什么好说的,这种应该也占不小的比例。下面说说不太明显的例子。提前返回有时候粗心会犯这种错误,在 close()
调用之前就 return 了,特别是函数比较大逻辑比较复杂时更容易犯错。这种情况可以通过把 close() 放在 finally
代码块解决1privatevoid method() 2 Cursor cursor = query(); // 假设query()
是一个查询数据库返回 Cursor 结果的函数3if (flag == false) //!!提前返回4return; 5 6
cursor.close(); 7 类的成员变量假设类里面有一个在类全局有效的成员变量,在方法 A
获取了查询结果,后面在其他地方又获取了一次查询结果,那么第二次查询的时候就应该先把前面一个 Cursor 对象关闭。 1publicclass
TestCursor 2private Cursor mCursor; 3 4privatevoid methodA() 5
mCursor = query(); 6 7 8privatevoid methodB() 9//!!必须先关闭上一个 cursor
对象10 mCursor = query(); 11 12 注意:曾经遇到过有人对 mCursor
感到疑惑,明明是同一个变量为什么还需要先关闭?首先 mCursor 是一个 Cursor 对象的引用,在 methodA 时 mCursor
指向了 query() 返回的一个 Cursor 对象 1;在 methodB() 时它又指向了返回的另外一个 Cursor 对象 2。在指向
Cursor 对象 2 之前必须先关闭 Cursor 对象 1,否则就出现了 Cursor 对象 1 在 finalize() 之前没有调用
close() 的情况。异常处理打开和关闭 Cursor 之间的代码出现 exception,导致没有跑到关闭的地方:1try 2
Cursor cursor = query(); 3// 中间省略某些出现异常的代码4 cursor.close(); 5 catch
(Exception e) 6//!!出现异常没跑到 cursor.close()7 这种情况应该把 close() 放到 finally
代码块里面: 1 Cursor cursor = null; 2try 3 cursor = query(); 4//
中间省略某些出现异常的代码 5 catch (Exception e) 6// 出现异常 7 finally 8if
(cursor != null) 9 cursor.close(); 10 4. 总结思考在finalize()
里面检测是可行的,且基本可以满足需要。针对 finalize() 执行时间不确定以及可能不执行的问题,可以通过记录目前打开没关闭的 Cursor
数量来部分解决,超过一定数目发出警告,两种手段相结合。还有没有其他检测办法呢?有,在 Cursor 构造方法以及 close() 方法添加
log,运行一段时间后检查 log 看哪个地方没有关闭。简化代码如下: 1import android.database.Cursor;
2import android.database.CursorWrapper; 3import android.util.Log; 4
5publicclass TestCursor extends CursorWrapper 6privatestaticfinal
String TAG = "TestCursor"; 7private Throwable mTrace; 8 9public
TestCursor(Cursor c) 10super(c); 11 mTrace = new Throwable("cusor
opened here"); 12 Log.d(TAG, "Cursor " + this.hashCode() + " opened,
stacktrace is: ", mTrace); 13 1415 @Override 16publicvoid close() 17
mIsClosed = true; 18 Log.d(TAG, "Cursor " + this.hashCode() + "
closed."); 19 20 检查时看某个 hashCode() 的 Cursor 有没有调用过 close()
方法,没有的话说明资源有泄露。这种方法优点是同样准确,且更可靠。
参考技术B Cursor 检测原理
在 Cursor 对象被 JVM 回收运行到 finalize() 方法的时候,检测 close() 方法有没有被调用,此办法在 ContentResolver 里面也得到应用。简化后的示例代码如下:

1 import android.database.Cursor;
2 import android.database.CursorWrapper;
3 import android.util.Log;
4
5 public class TestCursor extends CursorWrapper
6 private static final String TAG = "TestCursor";
7 private boolean mIsClosed = false;
8 private Throwable mTrace;
9
10 public TestCursor(Cursor c)
11 super(c);
12 mTrace = new Throwable("Explicit termination method 'close()' not called");
13
14
15 @Override
16 public void close()
17 mIsClosed = true;
18
19
20 @Override
21 public void finalize() throws Throwable
22 try
23 if (mIsClosed != true)
24 Log.e(TAG, "Cursor leaks", mTrace);
25
26 finally
27 super.finalize();
28
29
30

然后查询的时候,把 TestCursor 作为查询结果返回给 APP:
1 return new TestCursor(cursor); // cursor 是普通查询得到的结果,例如从 ContentProvider.query()

该方法同样适合于所有需要检测显式释放资源方法没有被调用的情形,是一种通用方法。但在 finalize() 方法里检测需要注意
优点:准确。因为该资源在 Cursor 对象被回收时仍没被释放,肯定是发生了资源泄露。
缺点:依赖于 finalize() 方法,也就依赖于 JVM 的垃圾回收策略。例如某 APP 现在有 10 个 Cursor 对象泄露,并且这 10 个对象已经不再被任何引用指向处于可回收状态,但是 JVM 可能并不会马上回收(时间不可预测),如果你现在检查不能够发现问题。另外,在某些情况下就算对象被回收 finalize() 可能也不会执行,也就是不能保证检测出所有问题。关于 finalize() 更多信息可以参考《Effective Java 2nd Edition》的 Item 7: Avoid Finalizers。

如何检测内存泄漏

我遇到了一些与android中的MemoryOutOfBounds异常相关的问题。我发现这背后有两个原因

(1)创建的线程是活的,不会在任何地方被破坏

(2)内存泄漏。

我们可以在eclipse中检测线程信息。但是当我执行我的应用程序时,如何知道我的代码内存泄漏的哪一行。 MAT工作方式不同。 MAT是静态的。是否有任何插件或任何方式来了解日食中的内存泄漏?

谢谢迪帕克

答案

这可能很有用。

What Android tools and methods work best to find memory/resource leaks?

另一答案

您不需要内存泄漏来获取OutOfMemoryError,只需在您的应用中使用太多内存就会导致它。

另一答案

您可以查看以下有关如何避免内存泄漏的链接:

以上是关于如何检测 Android Cursor 泄漏的主要内容,如果未能解决你的问题,请参考以下文章

Android内存泄漏检测工具:LeakCanary

Android内存泄漏检测工具:LeakCanary

Android内存泄漏检测工具:LeakCanary

Android内存泄漏检测工具:LeakCanary

如何定位和解决Android的内存溢出问题(大总

android 内存泄漏检测工具 LeakCanary 泄漏金丝雀