zl程序教程

您现在的位置是:首页 >  硬件

当前栏目

AndroidApp调试内存泄露之Cursor篇

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

//SampleCode
Cursorcursor=db.query();
List<String>list=convertToList(cursor);
cursor.close();

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

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

try{
Cursorc=queryCursor();
inta=c.getInt(1);
......
//如果出错,后面的cursor.close()将不会执行
......
c.close();
}catch(Exceptione){
}

正确写法应该是:
复制代码代码如下:
Cursorc;
try{
c=queryCursor();
inta=c.getInt(1);
......
//如果出错,后面的cursor.close()将不会执行
//c.close();
}catch(Exceptione){
}finally{
if(c!=null){
c.close();
}
} 

很简单,但是需要时刻谨记。
4.Cursor需要继续使用,不能马上关闭
有没有这种情况?怎么办?
答案是有,CursorAdapter就是一个典型的例子。
CursorAdapter示例如下:
复制代码代码如下:
mCursor=getContentResolver().query(CONTENT_URI,PROJECTION,
null,null,null);
mAdapter=newMyCursorAdapter(this,R.layout.list_item,mCursor);
setListAdapter(mAdapter);
//这里就不能关闭执行mCursor.close(),
//否则list中将会无数据

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

我专门附上CursorAdapter的changeCursor()方法源码,让大家看的更清楚,免得不放心changeCursor(null)方法:
复制代码代码如下:
/**
*Changetheunderlyingcursortoanewcursor.Ifthereisanexistingcursoritwillbe
*closed.
*
*@paramcursorThenewcursortobeused
*/
publicvoidchangeCursor(Cursorcursor){
Cursorold=swapCursor(cursor);
if(old!=null){
old.close();
}
}

/**
*SwapinanewCursor,returningtheoldCursor.Unlike
*{@link#changeCursor(Cursor)},thereturnedoldCursoris<em>not</em>
*closed.
*
*@paramnewCursorThenewcursortobeused.
*@returnReturnsthepreviouslysetCursor,ornulliftherewasanotone.
*IfthegivennewCursoristhesameinstanceisthepreviouslyset
*Cursor,nullisalsoreturned.
*/
publicCursorswapCursor(CursornewCursor){
if(newCursor==mCursor){
returnnull;
}
CursoroldCursor=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;
//notifytheobserversaboutthenewcursor
notifyDataSetChanged();
}else{
mRowIDColumn=-1;
mDataValid=false;
//notifytheobserversaboutthelackofadataset
notifyDataSetInvalidated();
}
returnoldCursor;
}

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

@Override
protectedvoidonQueryComplete(inttoken,Objectcookie,Cursorcursor){
switch(token){
caseTHREAD_LIST_QUERY_TOKEN:
mListAdapter.changeCursor(cursor);
setTitle(mTitle);
......
break;

caseHAVE_LOCKED_MESSAGES_TOKEN:
longthreadId=(Long)cookie;
confirmDeleteThreadDialog(newDeleteThreadListener(threadId,mQueryHandler,
ConversationList.this),threadId==-1,
cursor!=null&&cursor.getCount()>0,
ConversationList.this);
break;

default:
Log.e(TAG,"onQueryCompletecalledwithunknowntoken"+token);
}
}
}

复制代码代码如下:
@Override
protectedvoidonStop(){
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):Aresourcewasacquiredatattachedstacktracebutneverreleased.Seejava.io.Closeableforinformationonavoidingresourceleaks.
E/StrictMode(639):java.lang.Throwable:Explicitterminationmethod"close"notcalled
E/StrictMode(639):atdalvik.system.CloseGuard.open(CloseGuard.java:184)
......

在Android.0JellyBean中谷歌修正了这个泄露问题,相关代码如下:
复制代码代码如下:
privatefinalclassThreadListQueryHandlerextendsConversationQueryHandler{
publicThreadListQueryHandler(ContentResolvercontentResolver){
super(contentResolver);
}

@Override
protectedvoidonQueryComplete(inttoken,Objectcookie,Cursorcursor){
switch(token){
caseTHREAD_LIST_QUERY_TOKEN:
mListAdapter.changeCursor(cursor);

......

break;

caseUNREAD_THREADS_QUERY_TOKEN:
//新增的UNREAD_THREADS_QUERY_TOKEN分子和HAVE_LOCKED_MESSAGES_TOKEN分支也是类似的情况,cursor在jellybean中被及时关闭了
intcount=0;
if(cursor!=null){
count=cursor.getCount();
cursor.close();
}
mUnreadConvCount.setText(count>0?Integer.toString(count):null);
break;

caseHAVE_LOCKED_MESSAGES_TOKEN:
@SuppressWarnings("unchecked")
Collection<Long>threadIds=(Collection<Long>)cookie;
confirmDeleteThreadDialog(newDeleteThreadListener(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,"onQueryCompletecalledwithunknowntoken"+token);
}
}
}

复制代码代码如下:
@Override
protectedvoidonStop(){
super.onStop();
mListAdapter.changeCursor(null);
}

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

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