android优化(一)---App调试内存泄露之Cursor篇

android优化(一)---App调试内存泄露之Cursor篇 


内存泄露分析,从cursor关闭的问题开始把,谁都知道cursor要关闭,但是往往相反,人们却常常忘记关闭,因为真正的应用场景可能并非理想化的简单。

1. 理想化的cursor关闭

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

   但是cursor可能不会关闭,至少有以下两种可能。

2. Cursor未关闭的可能
     (1). cursor.close()之前发生异常。
     (2). cursor需要继续使用,不能马上关闭,后面忘记关闭了。

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

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

  正确写法应该是:

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

4. Cursor需要继续使用,不能马上关闭
    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关掉(同时意味着你可能需要在onResume()或者onStart()重新查询一下)。

@Override
protectedvoid 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) {
        returnnull;
    }
    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();
    }
    returnoldCursor;
}

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

private final class ThreadListQueryHandlerextends AsyncQueryHandler {
    publicThreadListQueryHandler(ContentResolver contentResolver) {
        super(contentResolver);
    }
 
    @Override
    protectedvoid onQueryComplete(inttoken, Object cookie, Cursor cursor) {
        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,"onQueryComplete called with unknown token "+ token);
        }
    }
}
 
@Override
protectedvoid 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.Closeablefor 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)
... ...

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

private final class ThreadListQueryHandlerextends ConversationQueryHandler {
    publicThreadListQueryHandler(ContentResolver contentResolver) {
        super(contentResolver);
    }
 
    @Override
    protectedvoid onQueryComplete(inttoken, Object cookie, Cursor cursor) {
        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,"onQueryComplete called with unknown token "+ token);
        }
    }
}
 
 
@Override
protectedvoid onStop() {
    super.onStop();
    mListAdapter.changeCursor(null);
}

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

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


  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值