Android App調(diào)試內(nèi)存泄露之Cursor篇
更新時(shí)間:2012年11月19日 12:13:07 作者:
最近在工作中處理了一些內(nèi)存泄露的問題,在這個(gè)過程中我尤其發(fā)現(xiàn)了一些基本的問題反而忽略導(dǎo)致內(nèi)存泄露
最近在工作中處理了一些內(nèi)存泄露的問題,在這個(gè)過程中我尤其發(fā)現(xiàn)了一些基本的問題反而忽略導(dǎo)致內(nèi)存泄露,比如靜態(tài)變量,cursor關(guān)閉,線程,定時(shí)器,反注冊(cè),bitmap等等,我稍微統(tǒng)計(jì)并總結(jié)了一下,當(dāng)然了,這些問題這么說起來比較籠統(tǒng),接下來我會(huì)根據(jù)問題,把一些實(shí)例代碼貼出來,一步一步分析,在具體的場(chǎng)景下,用行之有效的方法,找出泄露的根本原因,并給出解決方案。
現(xiàn)在,就從cursor關(guān)閉的問題開始把,誰都知道cursor要關(guān)閉,但是往往相反,人們卻常常忘記關(guān)閉,因?yàn)檎嬲膽?yīng)用場(chǎng)景可能并非理想化的簡(jiǎn)單。
1.理想化的cursor關(guān)閉
// Sample Code
Cursor cursor = db.query();
List<String> list = convertToList(cursor);
cursor.close();
這是最簡(jiǎn)單的cursor使用場(chǎng)景,如果這里的cursor沒有關(guān)閉,我想可能會(huì)引起萬千口水,一片罵聲。
但是實(shí)際場(chǎng)景可能并非如此,這里的cursor可能不會(huì)關(guān)閉,至少有以下兩種可能。
2. Cursor未關(guān)閉的可能
(1). cursor.close()之前發(fā)生異常。
(2). cursor需要繼續(xù)使用,不能馬上關(guān)閉,后面忘記關(guān)閉了。
3. Cursor.close()之前發(fā)生異常
這個(gè)很容易理解,應(yīng)該也是初學(xué)者最開始碰到的常見問題,舉例如下:
try {
Cursor c = queryCursor();
int a = c.getInt(1);
......
// 如果出錯(cuò),后面的cursor.close()將不會(huì)執(zhí)行
......
c.close();
} catch (Exception e) {
}
正確寫法應(yīng)該是:
Cursor c;
try {
c = queryCursor();
int a = c.getInt(1);
......
// 如果出錯(cuò),后面的cursor.close()將不會(huì)執(zhí)行
//c.close();
} catch (Exception e) {
} finally{
if (c != null) {
c.close();
}
}
很簡(jiǎn)單,但是需要時(shí)刻謹(jǐn)記。
4. Cursor需要繼續(xù)使用,不能馬上關(guān)閉
有沒有這種情況?怎么辦?
答案是有,CursorAdapter就是一個(gè)典型的例子。
CursorAdapter示例如下:
mCursor = getContentResolver().query(CONTENT_URI, PROJECTION,
null, null, null);
mAdapter = new MyCursorAdapter(this, R.layout.list_item, mCursor);
setListAdapter(mAdapter);
// 這里就不能關(guān)閉執(zhí)行mCursor.close(),
// 否則list中將會(huì)無數(shù)據(jù)
5. 這樣的Cursor應(yīng)該什么時(shí)候關(guān)閉呢?
這是個(gè)可以說好回答也可以說不好回答的問題,那就是在Cursor不再使用的時(shí)候關(guān)閉掉。
比如說,
上面的查詢,如果每次進(jìn)入或者resume的時(shí)候會(huì)重新查詢執(zhí)行。
一般來說,也是這種需求,很少我看不到界面的時(shí)候還在不停地顯示查詢結(jié)果吧,如果真的有,不予討論,記得最終關(guān)掉就OK了。
這個(gè)時(shí)候,我們一般可以在onStop()方法里面把cursor關(guān)掉。
@Override
protected void onStop() {
super.onStop();
// mCursorAdapter會(huì)釋放之前的cursor,相當(dāng)于關(guān)閉了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.實(shí)戰(zhàn)AsyncQueryHandler中Cursor的關(guān)閉問題
AsyncQueryHandler是一個(gè)很經(jīng)典很典型的分析Cursor的例子,不僅一陣見血,能舉一反三,而且非常常見,為以后避免。
AsyncQueryHandler文檔參考地址:
http://developer.android.com/reference/android/content/AsyncQueryHandler.html
下面這段代碼是Android2.3系統(tǒng)中Mms信息主頁面ConversationList源碼的一部分,大家看看Cursor正確關(guān)閉了嗎?
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);
}
大家覺得有問題嗎?
主要是兩點(diǎn):
(1). THREAD_LIST_QUERY_TOKEN分支的Cursor正確關(guān)閉了嗎?
(2). HAVE_LOCKED_MESSAGES_TOKEN分支的Cursor正確關(guān)閉了嗎?
根據(jù)前面的一條條分析,答案是:
(1). THREAD_LIST_QUERY_TOKEN分支的Cursor被傳遞到了mListAdapter了,而mListAdapter在onStop里面使用changeCursor(null),當(dāng)用戶離開當(dāng)前Activity,這個(gè)Cursor被正確釋放了,不會(huì)泄露。
(2). HAVE_LOCKED_MESSAGES_TOKEN分支的Cursor(就是參數(shù)cursor),只是作為一個(gè)判斷的一個(gè)條件,被使用后不再使用,但是也沒有關(guān)掉,所以cursor泄露,在StrictMode監(jiān)視下只要跑到這個(gè)地方都會(huì)拋出這個(gè)錯(cuò)誤:
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中谷歌修正了這個(gè)泄露問題,相關(guān)代碼如下:
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中被及時(shí)關(guān)閉了
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中被及時(shí)關(guān)閉了
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,谷歌在早期的版本里面都有一些這樣的代碼,更何況不注意的我們呢,實(shí)際上網(wǎng)上很多使用AsyncQueryHandler舉例中都犯了這個(gè)錯(cuò)誤,看完這篇文章后,以后再也不怕AsyncQueryHandler的cursor泄露了,還說不定能解決很多你現(xiàn)在應(yīng)用的后臺(tái)strictmode的cursor not close異常問題。
7.小結(jié)
雖然我覺得還有很多cursor未關(guān)閉的情況沒有說到,但是根本問題都是及時(shí)正確的關(guān)閉cursor。
內(nèi)存泄露cursor篇是我工作經(jīng)驗(yàn)上的一個(gè)總結(jié),專門捋清楚后對(duì)我自己對(duì)大家覺得都很有幫助,讓復(fù)雜的問題本質(zhì)化,簡(jiǎn)單化!
現(xiàn)在,就從cursor關(guān)閉的問題開始把,誰都知道cursor要關(guān)閉,但是往往相反,人們卻常常忘記關(guān)閉,因?yàn)檎嬲膽?yīng)用場(chǎng)景可能并非理想化的簡(jiǎn)單。
1.理想化的cursor關(guān)閉
復(fù)制代碼 代碼如下:
// Sample Code
Cursor cursor = db.query();
List<String> list = convertToList(cursor);
cursor.close();
這是最簡(jiǎn)單的cursor使用場(chǎng)景,如果這里的cursor沒有關(guān)閉,我想可能會(huì)引起萬千口水,一片罵聲。
但是實(shí)際場(chǎng)景可能并非如此,這里的cursor可能不會(huì)關(guān)閉,至少有以下兩種可能。
2. Cursor未關(guān)閉的可能
(1). cursor.close()之前發(fā)生異常。
(2). cursor需要繼續(xù)使用,不能馬上關(guān)閉,后面忘記關(guān)閉了。
3. Cursor.close()之前發(fā)生異常
這個(gè)很容易理解,應(yīng)該也是初學(xué)者最開始碰到的常見問題,舉例如下:
復(fù)制代碼 代碼如下:
try {
Cursor c = queryCursor();
int a = c.getInt(1);
......
// 如果出錯(cuò),后面的cursor.close()將不會(huì)執(zhí)行
......
c.close();
} catch (Exception e) {
}
正確寫法應(yīng)該是:
復(fù)制代碼 代碼如下:
Cursor c;
try {
c = queryCursor();
int a = c.getInt(1);
......
// 如果出錯(cuò),后面的cursor.close()將不會(huì)執(zhí)行
//c.close();
} catch (Exception e) {
} finally{
if (c != null) {
c.close();
}
}
很簡(jiǎn)單,但是需要時(shí)刻謹(jǐn)記。
4. Cursor需要繼續(xù)使用,不能馬上關(guān)閉
有沒有這種情況?怎么辦?
答案是有,CursorAdapter就是一個(gè)典型的例子。
CursorAdapter示例如下:
復(fù)制代碼 代碼如下:
mCursor = getContentResolver().query(CONTENT_URI, PROJECTION,
null, null, null);
mAdapter = new MyCursorAdapter(this, R.layout.list_item, mCursor);
setListAdapter(mAdapter);
// 這里就不能關(guān)閉執(zhí)行mCursor.close(),
// 否則list中將會(huì)無數(shù)據(jù)
5. 這樣的Cursor應(yīng)該什么時(shí)候關(guān)閉呢?
這是個(gè)可以說好回答也可以說不好回答的問題,那就是在Cursor不再使用的時(shí)候關(guān)閉掉。
比如說,
上面的查詢,如果每次進(jìn)入或者resume的時(shí)候會(huì)重新查詢執(zhí)行。
一般來說,也是這種需求,很少我看不到界面的時(shí)候還在不停地顯示查詢結(jié)果吧,如果真的有,不予討論,記得最終關(guān)掉就OK了。
這個(gè)時(shí)候,我們一般可以在onStop()方法里面把cursor關(guān)掉。
復(fù)制代碼 代碼如下:
@Override
protected void onStop() {
super.onStop();
// mCursorAdapter會(huì)釋放之前的cursor,相當(dāng)于關(guān)閉了cursor
mCursorAdapter.changeCursor(null);
}
我專門附上CursorAdapter的changeCursor()方法源碼,讓大家看的更清楚,免得不放心changeCursor(null)方法:
復(fù)制代碼 代碼如下:
/**
* 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.實(shí)戰(zhàn)AsyncQueryHandler中Cursor的關(guān)閉問題
AsyncQueryHandler是一個(gè)很經(jīng)典很典型的分析Cursor的例子,不僅一陣見血,能舉一反三,而且非常常見,為以后避免。
AsyncQueryHandler文檔參考地址:
http://developer.android.com/reference/android/content/AsyncQueryHandler.html
下面這段代碼是Android2.3系統(tǒng)中Mms信息主頁面ConversationList源碼的一部分,大家看看Cursor正確關(guān)閉了嗎?
復(fù)制代碼 代碼如下:
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);
}
}
}
復(fù)制代碼 代碼如下:
@Override
protected void onStop() {
super.onStop();
mListAdapter.changeCursor(null);
}
大家覺得有問題嗎?
主要是兩點(diǎn):
(1). THREAD_LIST_QUERY_TOKEN分支的Cursor正確關(guān)閉了嗎?
(2). HAVE_LOCKED_MESSAGES_TOKEN分支的Cursor正確關(guān)閉了嗎?
根據(jù)前面的一條條分析,答案是:
(1). THREAD_LIST_QUERY_TOKEN分支的Cursor被傳遞到了mListAdapter了,而mListAdapter在onStop里面使用changeCursor(null),當(dāng)用戶離開當(dāng)前Activity,這個(gè)Cursor被正確釋放了,不會(huì)泄露。
(2). HAVE_LOCKED_MESSAGES_TOKEN分支的Cursor(就是參數(shù)cursor),只是作為一個(gè)判斷的一個(gè)條件,被使用后不再使用,但是也沒有關(guān)掉,所以cursor泄露,在StrictMode監(jiān)視下只要跑到這個(gè)地方都會(huì)拋出這個(gè)錯(cuò)誤:
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中谷歌修正了這個(gè)泄露問題,相關(guān)代碼如下:
復(fù)制代碼 代碼如下:
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中被及時(shí)關(guān)閉了
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中被及時(shí)關(guān)閉了
if (cursor != null) {
cursor.close();
}
break;
default:
Log.e(TAG, "onQueryComplete called with unknown token " + token);
}
}
}
復(fù)制代碼 代碼如下:
@Override
protected void onStop() {
super.onStop();
mListAdapter.changeCursor(null);
}
是不是小看了AsyncQueryHandler,谷歌在早期的版本里面都有一些這樣的代碼,更何況不注意的我們呢,實(shí)際上網(wǎng)上很多使用AsyncQueryHandler舉例中都犯了這個(gè)錯(cuò)誤,看完這篇文章后,以后再也不怕AsyncQueryHandler的cursor泄露了,還說不定能解決很多你現(xiàn)在應(yīng)用的后臺(tái)strictmode的cursor not close異常問題。
7.小結(jié)
雖然我覺得還有很多cursor未關(guān)閉的情況沒有說到,但是根本問題都是及時(shí)正確的關(guān)閉cursor。
內(nèi)存泄露cursor篇是我工作經(jīng)驗(yàn)上的一個(gè)總結(jié),專門捋清楚后對(duì)我自己對(duì)大家覺得都很有幫助,讓復(fù)雜的問題本質(zhì)化,簡(jiǎn)單化!
您可能感興趣的文章:
- Android 優(yōu)化Handler防止內(nèi)存泄露
- 解決Android使用Handler造成內(nèi)存泄露問題
- 使用Android Studio檢測(cè)內(nèi)存泄露(LeakCanary)
- Android 消息機(jī)制以及handler的內(nèi)存泄露
- 避免 Android中Context引起的內(nèi)存泄露
- Android 中Handler引起的內(nèi)存泄露
- Android垃圾回收機(jī)制解決內(nèi)存泄露問題
- Android中Handler引起的內(nèi)存泄露問題解決辦法
- Android編程中避免內(nèi)存泄露的方法總結(jié)
- 分析Android常見的內(nèi)存泄露和解決方案
相關(guān)文章
Android 5秒學(xué)會(huì)使用手勢(shì)解鎖功能
本文講述的是一個(gè)手勢(shì)解鎖的庫(kù),可以定制顯示隱藏宮格點(diǎn)、路徑、并且?guī)в行【艑m格顯示圖,和震動(dòng)!讓你學(xué)會(huì)使用這個(gè)簡(jiǎn)單,高效的庫(kù),好了,具體內(nèi)容詳情大家通過本文學(xué)習(xí)吧2017-12-12Android仿微信雷達(dá)輻射搜索好友(邏輯清晰實(shí)現(xiàn)簡(jiǎn)單)
仿微信雷達(dá)掃描,仿安卓微信、云播雷達(dá)掃描動(dòng)畫效果點(diǎn)擊中間的黑色圓圈開始掃描動(dòng)畫,再次點(diǎn)擊復(fù)位,需要這種效果的朋友可以自己下載看一下2016-02-02Android 使用viewpager實(shí)現(xiàn)無限循環(huán)(定時(shí)+手動(dòng))
這篇文章主要介紹了Android 使用viewpager實(shí)現(xiàn)無限循環(huán)(定時(shí)+手動(dòng))的相關(guān)資料,需要的朋友可以參考下2015-11-11Android動(dòng)畫之小球擬合動(dòng)畫實(shí)例
這篇文章主要介紹了Android動(dòng)畫之小球擬合動(dòng)畫實(shí)例的相關(guān)資料,需要的朋友可以參考下2017-07-07詳解RxJava2 Retrofit2 網(wǎng)絡(luò)框架簡(jiǎn)潔輕便封裝
本篇文章主要介紹了詳解RxJava2 Retrofit2 網(wǎng)絡(luò)框架簡(jiǎn)潔輕便封裝,小編覺得挺不錯(cuò)的,現(xiàn)在分享給大家,也給大家做個(gè)參考。一起跟隨小編過來看看吧2017-12-12Android基于TextView實(shí)現(xiàn)跑馬燈效果
這篇文章主要為大家詳細(xì)介紹了Android基于TextView實(shí)現(xiàn)跑馬燈效果的相關(guān)資料,具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下2017-03-03