Android檢測(cè)Cursor泄漏的原理以及使用方法
本文介紹如何在 Android 檢測(cè) Cursor 泄漏的原理以及使用方法,還指出幾種常見(jiàn)的出錯(cuò)示例。有一些泄漏在代碼中難以察覺(jué),但程序長(zhǎng)時(shí)間運(yùn)行后必然會(huì)出現(xiàn)異常。同時(shí)該方法同樣適合于其他需要檢測(cè)資源泄露的情況。
最近發(fā)現(xiàn)某蔬菜手機(jī)連接程序在查詢媒體存儲(chǔ)(MediaProvider)數(shù)據(jù)庫(kù)時(shí)出現(xiàn)嚴(yán)重 Cursor 泄漏現(xiàn)象,運(yùn)行一段時(shí)間后會(huì)導(dǎo)致系統(tǒng)中所有使用到該數(shù)據(jù)庫(kù)的程序無(wú)法使用。另外在工作中也常發(fā)現(xiàn)有些應(yīng)用有 Cursor 泄漏現(xiàn)象,由于需要長(zhǎng)時(shí)間運(yùn)行才會(huì)出現(xiàn)異常,所以有的此類 bug 很長(zhǎng)時(shí)間都沒(méi)被發(fā)現(xiàn)。
但是一旦 Cursor 泄漏累計(jì)到一定數(shù)目(通常為數(shù)百個(gè))必然會(huì)出現(xiàn)無(wú)法查詢數(shù)據(jù)庫(kù)的情況,只有等數(shù)據(jù)庫(kù)服務(wù)所在進(jìn)程死掉重啟才能恢復(fù)正常。通常的出錯(cuò)信息如下,指出某 pid 的程序打開(kāi)了 866 個(gè) Cursor 沒(méi)有關(guān)閉,導(dǎo)致了 exception:
3634 3644 E JavaBinder: *** Uncaught remote exception! (Exceptions are not yet supported across processes.)
3634 3644 E JavaBinder: android.database.CursorWindowAllocationException: Cursor window allocation of 2048 kb failed. # Open Cursors=866 (# cursors opened by pid 1565=866)
3634 3644 E JavaBinder: at android.database.CursorWindow.(CursorWindow.java:104)
3634 3644 E JavaBinder: at android.database.AbstractWindowedCursor.clearOrCreateWindow(AbstractWindowedCursor.java:198)
3634 3644 E JavaBinder: at android.database.sqlite.SQLiteCursor.fillWindow(SQLiteCursor.java:147)
3634 3644 E JavaBinder: at android.database.sqlite.SQLiteCursor.getCount(SQLiteCursor.java:141)
3634 3644 E JavaBinder: at android.database.CursorToBulkCursorAdaptor.getBulkCursorDescriptor(CursorToBulkCursorAdaptor.java:143)
3634 3644 E JavaBinder: at android.content.ContentProviderNative.onTransact(ContentProviderNative.java:118)
3634 3644 E JavaBinder: at android.os.Binder.execTransact(Binder.java:367)
3634 3644 E JavaBinder: at dalvik.system.NativeStart.run(Native Method)
1. Cursor 檢測(cè)原理
在 Cursor 對(duì)象被 JVM 回收運(yùn)行到 finalize() 方法的時(shí)候,檢測(cè) close() 方法有沒(méi)有被調(diào)用,此辦法在 ContentResolver 里面也得到應(yīng)用。簡(jiǎn)化后的示例代碼如下:
import android.database.Cursor;
import android.database.CursorWrapper;
import android.util.Log;
public class TestCursor extends CursorWrapper {
private static final String TAG = "TestCursor";
private boolean mIsClosed = false;
private Throwable mTrace;
public TestCursor(Cursor c) {
super(c);
mTrace = new Throwable("Explicit termination method 'close()' not called");
}
@Override
public void close() {
mIsClosed = true;
}
@Override
public void finalize() throws Throwable {
try {
if (mIsClosed != true) {
Log.e(TAG, "Cursor leaks", mTrace);
}
} finally {
super.finalize();
}
}
}
然后查詢的時(shí)候,把 TestCursor 作為查詢結(jié)果返回給 APP:
1 return new TestCursor(cursor); // cursor 是普通查詢得到的結(jié)果,例如從 ContentProvider.query()
該方法同樣適合于所有需要檢測(cè)顯式釋放資源方法沒(méi)有被調(diào)用的情形,是一種通用方法。但在 finalize() 方法里檢測(cè)需要注意
優(yōu)點(diǎn):準(zhǔn)確。因?yàn)樵撡Y源在 Cursor 對(duì)象被回收時(shí)仍沒(méi)被釋放,肯定是發(fā)生了資源泄露。
缺點(diǎn):依賴于 finalize() 方法,也就依賴于 JVM 的垃圾回收策略。例如某 APP 現(xiàn)在有 10 個(gè) Cursor 對(duì)象泄露,并且這 10 個(gè)對(duì)象已經(jīng)不再被任何引用指向處于可回收狀態(tài),但是 JVM 可能并不會(huì)馬上回收(時(shí)間不可預(yù)測(cè)),如果你現(xiàn)在檢查不能夠發(fā)現(xiàn)問(wèn)題。另外,在某些情況下就算對(duì)象被回收 finalize() 可能也不會(huì)執(zhí)行,也就是不能保證檢測(cè)出所有問(wèn)題。關(guān)于 finalize() 更多信息可以參考《Effective Java 2nd Edition》的 Item 7: Avoid Finalizers
2. 使用方法
對(duì)于 APP 開(kāi)發(fā)人員
從 GINGERBREAD 開(kāi)始 Android 就提供了 StrictMode 工具協(xié)助開(kāi)發(fā)人員檢查是否不小心地做了一些不該有的操作。使用方法是在 Activity 里面設(shè)置 StrictMode,下面的例子是打開(kāi)了檢查泄漏的 SQLite 對(duì)象以及 Closeable 對(duì)象(普通 Cursor/FileInputStream 等)的功能,發(fā)現(xiàn)有違規(guī)情況則記錄 log 并使程序強(qiáng)行退出。
import android.os.StrictMode;
public class TestActivity extends Activity {
private static final boolean DEVELOPER_MODE = true;
public void onCreate() {
if (DEVELOPER_MODE) {
StrictMode.setVMPolicy(new StrictMode.VMPolicy.Builder()
.detectLeakedSqlLiteObjects()
.detectLeakedClosableObjects()
.penaltyLog()
.penaltyDeath()
.build());
}
super.onCreate();
}
}
對(duì)于 framework 開(kāi)發(fā)人員
如果是通過(guò) ContentProvider 提供數(shù)據(jù)庫(kù)數(shù)據(jù),在 ContentResolver 里面已有 CloseGuard 類實(shí)行類似檢測(cè),但需要自行打開(kāi)(上例也是打開(kāi) CloseGuard):
1 CloseGuard.setEnabled(true);更值得推薦的辦法是按照本文第一節(jié)中的檢測(cè)原理,在 ContentResolver 內(nèi)部類 CursorWrapperInner 里面加入。其他需要檢測(cè)類似于資源泄漏的,同樣可以使用該檢測(cè)原理。
3. 容易出錯(cuò)的地方
忘記調(diào)用 close() 這種低級(jí)錯(cuò)誤沒(méi)什么好說(shuō)的,這種應(yīng)該也占不小的比例。下面說(shuō)說(shuō)不太明顯的例子。
提前返回
有時(shí)候粗心會(huì)犯這種錯(cuò)誤,在 close() 調(diào)用之前就 return 了,特別是函數(shù)比較大邏輯比較復(fù)雜時(shí)更容易犯錯(cuò)。這種情況可以通過(guò)把 close() 放在 finally 代碼塊解決
private void method() {
Cursor cursor = query(); // 假設(shè) query() 是一個(gè)查詢數(shù)據(jù)庫(kù)返回 Cursor 結(jié)果的函數(shù)
if (flag == false) { // ??!提前返回
return;
}
cursor.close();
}
類的成員變量
假設(shè)類里面有一個(gè)在類全局有效的成員變量,在方法 A 獲取了查詢結(jié)果,后面在其他地方又獲取了一次查詢結(jié)果,那么第二次查詢的時(shí)候就應(yīng)該先把前面一個(gè) Cursor 對(duì)象關(guān)閉。
public class TestCursor {
private Cursor mCursor;
private void methodA() {
mCursor = query();
}
private void methodB() {
// ?。”仨毾汝P(guān)閉上一個(gè) cursor 對(duì)象
mCursor = query();
}
}
注意:曾經(jīng)遇到過(guò)有人對(duì) mCursor 感到疑惑,明明是同一個(gè)變量為什么還需要先關(guān)閉?首先 mCursor 是一個(gè) Cursor 對(duì)象的引用,在 methodA 時(shí) mCursor 指向了 query() 返回的一個(gè) Cursor 對(duì)象 1;在 methodB() 時(shí)它又指向了返回的另外一個(gè) Cursor 對(duì)象 2。在指向 Cursor 對(duì)象 2 之前必須先關(guān)閉 Cursor 對(duì)象 1,否則就出現(xiàn)了 Cursor 對(duì)象 1 在 finalize() 之前沒(méi)有調(diào)用 close() 的情況。
異常處理
打開(kāi)和關(guān)閉 Cursor 之間的代碼出現(xiàn) exception,導(dǎo)致沒(méi)有跑到關(guān)閉的地方:
try {
Cursor cursor = query();
// 中間省略某些出現(xiàn)異常的代碼
cursor.close();
} catch (Exception e) {
// ?。〕霈F(xiàn)異常沒(méi)跑到 cursor.close()
}
這種情況應(yīng)該把 close() 放到 finally 代碼塊里面:
Cursor cursor = null;
try {
cursor = query();
// 中間省略某些出現(xiàn)異常的代碼
} catch (Exception e) {
// 出現(xiàn)異常
} finally {
if (cursor != null)
cursor.close();
}
4. 總結(jié)思考
在 finalize() 里面檢測(cè)是可行的,且基本可以滿足需要。針對(duì) finalize() 執(zhí)行時(shí)間不確定以及可能不執(zhí)行的問(wèn)題,可以通過(guò)記錄目前打開(kāi)沒(méi)關(guān)閉的 Cursor 數(shù)量來(lái)部分解決,超過(guò)一定數(shù)目發(fā)出警告,兩種手段相結(jié)合。
還有沒(méi)有其他檢測(cè)辦法呢?有,在 Cursor 構(gòu)造方法以及 close() 方法添加 log,運(yùn)行一段時(shí)間后檢查 log 看哪個(gè)地方?jīng)]有關(guān)閉。簡(jiǎn)化代碼如下:
import android.database.Cursor;
import android.database.CursorWrapper;
import android.util.Log;
public class TestCursor extends CursorWrapper {
private static final String TAG = "TestCursor";
private Throwable mTrace;
public TestCursor(Cursor c) {
super(c);
mTrace = new Throwable("cusor opened here");
Log.d(TAG, "Cursor " + this.hashCode() + " opened, stacktrace is: ", mTrace);
}
@Override
public void close() {
mIsClosed = true;
Log.d(TAG, "Cursor " + this.hashCode() + " closed.");
}
}
檢查時(shí)看某個(gè) hashCode() 的 Cursor 有沒(méi)有調(diào)用過(guò) close() 方法,沒(méi)有的話說(shuō)明資源有泄露。這種方法優(yōu)點(diǎn)是同樣準(zhǔn)確,且更可靠。缺點(diǎn)是需要檢查大量 log,且打開(kāi)/關(guān)閉的地方可能相距較遠(yuǎn),如果不寫(xiě)個(gè)小腳本分析人工看的話會(huì)比較痛苦;另外必須 APP 完全退出后才能檢查,因?yàn)楹笈_(tái)運(yùn)行時(shí)某些 Cursor 還在正常使用。
相關(guān)文章
Android?Studio中如何修改APP圖標(biāo)和APP名稱
這篇文章主要介紹了Android?Studio中如何修改APP圖標(biāo)和APP名稱,具有很好的參考價(jià)值,希望對(duì)大家有所幫助。如有錯(cuò)誤或未考慮完全的地方,望不吝賜教2022-11-11
Android?雙屏異顯自適應(yīng)Dialog的實(shí)現(xiàn)
Android 多屏互聯(lián)的時(shí)代,必然會(huì)出現(xiàn)多屏連接的問(wèn)題,本文主要介紹了Android?雙屏異顯自適應(yīng)Dialog的實(shí)現(xiàn),具有一定的參考價(jià)值,感興趣的可以了解一下2023-12-12
View觸發(fā)機(jī)制API實(shí)現(xiàn)GestureDetector OverScroller詳解
這篇文章主要為大家介紹了View觸發(fā)機(jī)制API實(shí)現(xiàn)GestureDetector OverScroller詳解,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進(jìn)步,早日升職加薪2022-11-11
Android EditText限制輸入字符的方法總結(jié)
這篇文章主要介紹了 Android EditText限制輸入字符的方法總結(jié)的相關(guān)資料,這里提供了五種方法來(lái)實(shí)現(xiàn)并進(jìn)行比較,需要的朋友可以參考下2017-07-07
Android通過(guò)代碼控制ListView上下滾動(dòng)的方法
今天小編就為大家分享一篇關(guān)于Android通過(guò)代碼控制ListView上下滾動(dòng)的方法,小編覺(jué)得內(nèi)容挺不錯(cuò)的,現(xiàn)在分享給大家,具有很好的參考價(jià)值,需要的朋友一起跟隨小編來(lái)看看吧2018-12-12
Android 開(kāi)發(fā)隨手筆記之使用攝像頭拍照
在Android中,使用攝像頭拍照一般有兩種方法, 一種是調(diào)用系統(tǒng)自帶的Camera,另一種是自己寫(xiě)一個(gè)攝像的界面,本篇文章給大家介紹android開(kāi)發(fā)隨手筆記之使用攝像頭拍照,感興趣的朋友一起學(xué)習(xí)吧2015-11-11
Android自動(dòng)化獲取卡頓信息的實(shí)現(xiàn)方法
自動(dòng)化獲取卡頓信息就像給App裝 “行車(chē)記錄儀” —— 實(shí)時(shí)記錄主線程的“駕駛狀態(tài)”,一旦發(fā)現(xiàn)“急剎車(chē)”(卡頓),立刻保存現(xiàn)場(chǎng)(堆棧),事后回看錄像(日志)精準(zhǔn)定位問(wèn)題,本文給大家介紹了Android自動(dòng)化獲取卡頓信息的實(shí)現(xiàn)方法,需要的朋友可以參考下2025-02-02
Android PhoneWindowManager監(jiān)聽(tīng)屏幕右側(cè)向左滑動(dòng)實(shí)現(xiàn)返回功能
這篇文章主要介紹了Android PhoneWindowManager監(jiān)聽(tīng)屏幕右側(cè)向左滑動(dòng)實(shí)現(xiàn)返回功能,本文通過(guò)實(shí)例代碼給大家介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或工作具有一定的參考借鑒價(jià)值,需要的朋友可以參考下2020-04-04
Android中FloatingActionButton的顯示與隱藏示例
本篇文章主要介紹了Android中FloatingActionButton的顯示與隱藏示例,非常具有實(shí)用價(jià)值,需要的朋友可以參考下2017-10-10

