Android學習系列(32)–App調試記憶體泄露之Cursor篇

來源:互聯網
上載者:User

    最近在工作中處理了一些記憶體泄露的問題,在這個過程中我尤其發現了一些基本的問題反而忽略導致記憶體泄露,比如靜態變數,cursor關閉,線程,定時器,反註冊,bitmap等等,我稍微統計並總結了一下,當然了,這些問題這麼說起來比較籠統,接下來我會根據問題,把一些執行個體代碼貼出來,一步一步分析,在具體的情境下,用行之有效方法,找出泄露的根本原因,並給出解決方案。
    現在,就從cursor關閉的問題開始把,誰都知道cursor要關閉,但是往往相反,人們卻常常忘記關閉,因為真正的應用情境可能並非理想化的簡單。
1. 理想化的cursor關閉

// Sample CodeCursor 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 calledE/StrictMode(639): at dalvik.system.CloseGuard.open(CloseGuard.java:184)... ...

  在Android4.0 JellyBean中Google修正了這個泄露問題,相關代碼如下:

    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,Google在早期的版本裡面都有一些這樣的代碼,更何況不注意的我們呢,實際上網上很多使用AsyncQueryHandler舉例中都犯了這個錯誤,看完這篇文章後,以後再也不怕AsyncQueryHandler的cursor泄露了,還說不定能解決很多你現在應用的後台strictmode的cursor not close異常問題。

7. 小結
    雖然我覺得還有很多cursor未關閉的情況沒有說到,但是根本問題都是及時正確的關閉cursor。
    記憶體泄露cursor篇是我工作經驗上的一個總結,專門捋清楚後對我自己對大家覺得都很有協助,讓複雜的問題本質化,簡單化!

相關文章

聯繫我們

該頁面正文內容均來源於網絡整理,並不代表阿里雲官方的觀點,該頁面所提到的產品和服務也與阿里云無關,如果該頁面內容對您造成了困擾,歡迎寫郵件給我們,收到郵件我們將在5個工作日內處理。

如果您發現本社區中有涉嫌抄襲的內容,歡迎發送郵件至: info-contact@alibabacloud.com 進行舉報並提供相關證據,工作人員會在 5 個工作天內聯絡您,一經查實,本站將立刻刪除涉嫌侵權內容。

A Free Trial That Lets You Build Big!

Start building with 50+ products and up to 12 months usage for Elastic Compute Service

  • Sales Support

    1 on 1 presale consultation

  • After-Sales Support

    24/7 Technical Support 6 Free Tickets per Quarter Faster Response

  • Alibaba Cloud offers highly flexible support services tailored to meet your exact needs.