Android學習系列(32)--App調試內存泄露之Cursor篇


    最近在工作中處理了一些內存泄露的問題,在這個過程中我尤其發現了一些基本的問題反而忽略導致內存泄露,比如靜態變量,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關掉(同時意味着你可能需要在onResume()或者onStart()重新查詢一下)。

    @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)
... ...

  在Android4.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篇是我工作經驗上的一個總結,專門捋清楚后對我自己對大家覺得都很有幫助,讓復雜的問題本質化,簡單化!


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM