首页 > 系统 > Android > 正文

Android App调试内存泄露之Cursor篇

2019-10-24 21:11:31
字体:
来源:转载
供稿:网友
最近在工作中处理了一些内存泄露的问题,在这个过程中我尤其发现了一些基本的问题反而忽略导致内存泄露
 
最近在工作中处理了一些内存泄露的问题,在这个过程中我尤其发现了一些基本的问题反而忽略导致内存泄露,比如静态变量,cursor关闭,线程,定时器,反注册,bitmap等等,我稍微统计并总结了一下,当然了,这些问题这么说起来比较笼统,接下来我会根据问题,把一些实例代码贴出来,一步一步分析,在具体的场景下,用行之有效的方法,找出泄露的根本原因,并给出解决方案。 
现在,就从cursor关闭的问题开始把,谁都知道cursor要关闭,但是往往相反,人们却常常忘记关闭,因为真正的应用场景可能并非理想化的简单。 
1.理想化的cursor关闭 
复制代码代码如下:

// Sample Code 
Cursor cursor = db.query(); 
List<String> list = convertToList(cursor); 
cursor.close(); 

这是最简单的cursor使用场景,如果这里的cursor没有关闭,我想可能会引起万千口水,一片骂声。 
但是实际场景可能并非如此,这里的cursor可能不会关闭,至少有以下两种可能。 
2. Cursor未关闭的可能 
(1). cursor.close()之前发生异常。 
(2). cursor需要继续使用,不能马上关闭,后面忘记关闭了。 

3. Cursor.close()之前发生异常 
这个很容易理解,应该也是初学者最开始碰到的常见问题,举例如下: 
复制代码代码如下:

try { 
Cursor c = queryCursor(); 
int a = c.getInt(1); 
...... 
// 如果出错,后面的cursor.close()将不会执行 
...... 
c.close(); 
} catch (Exception e) { 

正确写法应该是: 
复制代码代码如下:

Cursor c; 
try { 
c = queryCursor(); 
int a = c.getInt(1); 
...... 
// 如果出错,后面的cursor.close()将不会执行 
//c.close(); 
} catch (Exception e) { 
} finally{ 
if (c != null) { 
c.close(); 

}  

很简单,但是需要时刻谨记。 
4. Cursor需要继续使用,不能马上关闭 
有没有这种情况?怎么办? 
答案是有,CursorAdapter就是一个典型的例子。 
CursorAdapter示例如下: 
复制代码代码如下:

mCursor = getContentResolver().query(CONTENT_URI, PROJECTION, 
null, null, null); 
mAdapter = new MyCursorAdapter(this, R.layout.list_item, mCursor); 
setListAdapter(mAdapter); 
// 这里就不能关闭执行mCursor.close(), 
// 否则list中将会无数据 

5. 这样的Cursor应该什么时候关闭呢? 
这是个可以说好回答也可以说不好回答的问题,那就是在Cursor不再使用的时候关闭掉。 
比如说, 
上面的查询,如果每次进入或者resume的时候会重新查询执行。 
一般来说,也是这种需求,很少我看不到界面的时候还在不停地显示查询结果吧,如果真的有,不予讨论,记得最终关掉就OK了。 
这个时候,我们一般可以在onStop()方法里面把cursor关掉。 
复制代码代码如下:

@Override 
protected void onStop() { 
super.onStop(); 
// mCursorAdapter会释放之前的cursor,相当于关闭了cursor 
mCursorAdapter.changeCursor(null); 

我专门附上CursorAdapter的changeCursor()方法源码,让大家看的更清楚,免得不放心changeCursor(null)方法: 
复制代码代码如下:

/** 
* Change the underlying cursor to a new cursor. If there is an existing cursor it will be 
* closed. 

* @param cursor The new cursor to be used 
*/ 
public void changeCursor(Cursor cursor) { 
Cursor old = swapCursor(cursor); 
if (old != null) { 
old.close(); 



/** 
* Swap in a new Cursor, returning the old Cursor. Unlike 
* {@link #changeCursor(Cursor)}, the returned old Cursor is <em>not</em> 
* closed. 

* @param newCursor The new cursor to be used. 
* @return Returns the previously set Cursor, or null if there wasa not one. 
* If the given new Cursor is the same instance is the previously set 
* Cursor, null is also returned. 
*/ 
public Cursor swapCursor(Cursor newCursor) { 
if (newCursor == mCursor) { 
return null; 

Cursor oldCursor = mCursor; 
if (oldCursor != null) { 
if (mChangeObserver != null) oldCursor.unregisterContentObserver(mChangeObserver); 
if (mDataSetObserver != null) oldCursor.unregisterDataSetObserver(mDataSetObserver); 

mCursor = newCursor; 
if (newCursor != null) { 
if (mChangeObserver != null) newCursor.registerContentObserver(mChangeObserver); 
if (mDataSetObserver != null) newCursor.registerDataSetObserver(mDataSetObserver); 
mRowIDColumn = newCursor.getColumnIndexOrThrow("_id"); 
mDataValid = true; 
// notify the observers about the new cursor 
notifyDataSetChanged(); 
} else { 
mRowIDColumn = -1; 
mDataValid = false; 
// notify the observers about the lack of a data set 
notifyDataSetInvalidated(); 

return oldCursor; 

6.实战AsyncQueryHandler中Cursor的关闭问题 
AsyncQueryHandler是一个很经典很典型的分析Cursor的例子,不仅一阵见血,能举一反三,而且非常常见,为以后避免。 
AsyncQueryHandler文档参考地址: 
http://developer.android.com/reference/android/content/AsyncQueryHandler.html 
下面这段代码是Android2.3系统中Mms信息主页面ConversationList源码的一部分,大家看看Cursor正确关闭了吗? 
复制代码代码如下:

private final class ThreadListQueryHandler extends AsyncQueryHandler { 
public ThreadListQueryHandler(ContentResolver contentResolver) { 
super(contentResolver); 


@Override 
protected void onQueryComplete(int token, Object cookie, Cursor cursor) { 
switch (token) { 
case THREAD_LIST_QUERY_TOKEN: 
mListAdapter.changeCursor(cursor); 
setTitle(mTitle); 
... ... 
break; 

case HAVE_LOCKED_MESSAGES_TOKEN: 
long threadId = (Long)cookie; 
confirmDeleteThreadDialog(new DeleteThreadListener(threadId, mQueryHandler, 
ConversationList.this), threadId == -1, 
cursor != null && cursor.getCount() > 0, 
ConversationList.this); 
break; 

default: 
Log.e(TAG, "onQueryComplete called with unknown token " + token); 



复制代码代码如下:

@Override 
protected void onStop() { 
super.onStop(); 

mListAdapter.changeCursor(null); 

大家觉得有问题吗? 
主要是两点: 
(1). THREAD_LIST_QUERY_TOKEN分支的Cursor正确关闭了吗? 
(2). HAVE_LOCKED_MESSAGES_TOKEN分支的Cursor正确关闭了吗? 
根据前面的一条条分析,答案是: 
(1). THREAD_LIST_QUERY_TOKEN分支的Cursor被传递到了mListAdapter了,而mListAdapter在onStop里面使用changeCursor(null),当用户离开当前Activity,这个Cursor被正确释放了,不会泄露。 
(2). HAVE_LOCKED_MESSAGES_TOKEN分支的Cursor(就是参数cursor),只是作为一个判断的一个条件,被使用后不再使用,但是也没有关掉,所以cursor泄露,在StrictMode监视下只要跑到这个地方都会抛出这个错误: 

E/StrictMode(639): A resource was acquired at attached stack trace but never released. See java.io.Closeable for information on avoiding resource leaks. 
E/StrictMode(639): java.lang.Throwable: Explicit termination method 'close' not called 
E/StrictMode(639): at dalvik.system.CloseGuard.open(CloseGuard.java:184) 
... ... 

在Android.0 JellyBean中谷歌修正了这个泄露问题,相关代码如下: 
复制代码代码如下:

private final class ThreadListQueryHandler extends ConversationQueryHandler { 
public ThreadListQueryHandler(ContentResolver contentResolver) { 
super(contentResolver); 


@Override 
protected void onQueryComplete(int token, Object cookie, Cursor cursor) { 
switch (token) { 
case THREAD_LIST_QUERY_TOKEN: 
mListAdapter.changeCursor(cursor); 

... ... 

break; 

case UNREAD_THREADS_QUERY_TOKEN: 
// 新增的UNREAD_THREADS_QUERY_TOKEN分子和HAVE_LOCKED_MESSAGES_TOKEN分支也是类似的情况,cursor在jellybean中被及时关闭了 
int count = 0; 
if (cursor != null) { 
count = cursor.getCount(); 
cursor.close(); 

mUnreadConvCount.setText(count > 0 ? Integer.toString(count) : null); 
break; 

case HAVE_LOCKED_MESSAGES_TOKEN: 
@SuppressWarnings("unchecked") 
Collection<Long> threadIds = (Collection<Long>)cookie; 
confirmDeleteThreadDialog(new DeleteThreadListener(threadIds, mQueryHandler, 
ConversationList.this), threadIds, 
cursor != null && cursor.getCount() > 0, 
ConversationList.this); 
// HAVE_LOCKED_MESSAGES_TOKEN分支中的cursor在jellybean中被及时关闭了 
if (cursor != null) { 
cursor.close(); 

break; 

default: 
Log.e(TAG, "onQueryComplete called with unknown token " + token); 



复制代码代码如下:

@Override 
protected void onStop() { 
super.onStop(); 
mListAdapter.changeCursor(null); 

是不是小看了AsyncQueryHandler,谷歌在早期的版本里面都有一些这样的代码,更何况不注意的我们呢,实际上网上很多使用AsyncQueryHandler举例中都犯了这个错误,看完这篇文章后,以后再也不怕AsyncQueryHandler的cursor泄露了,还说不定能解决很多你现在应用的后台strictmode的cursor not close异常问题。 

7.小结 
虽然我觉得还有很多cursor未关闭的情况没有说到,但是根本问题都是及时正确的关闭cursor。 
内存泄露cursor篇是我工作经验上的一个总结,专门捋清楚后对我自己对大家觉得都很有帮助,让复杂的问题本质化,简单化! 

发表评论 共有条评论
用户名: 密码:
验证码: 匿名发表