欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页  >  移动技术

Android App调试内存泄露之Cursor篇

程序员文章站 2023-12-15 20:40:46
最近在工作中处理了一些内存泄露的问题,在这个过程中我尤其发现了一些基本的问题反而忽略导致内存泄露,比如静态变量,cursor关闭,线程,定时器,反注册,bitmap等等,我...
最近在工作中处理了一些内存泄露的问题,在这个过程中我尤其发现了一些基本的问题反而忽略导致内存泄露,比如静态变量,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篇是我工作经验上的一个总结,专门捋清楚后对我自己对大家觉得都很有帮助,让复杂的问题本质化,简单化!

上一篇:

下一篇: