Android檢測Cursor洩漏的理以及使用方法

NO IMAGE
1 Star2 Stars3 Stars4 Stars5 Stars 給文章打分!
Loading...

簡介
本文介紹如何在 Android 檢測 Cursor 洩漏的原理以及使用方法,還指出幾種常見的出錯示例。有一些洩漏在程式碼中難以察覺,但程式長時間執行後必然會出現異常。同時該方法同樣適合於其他需要檢測資源洩露的情況。

最近發現某蔬菜手機連線程式在查詢媒體儲存(MediaProvider)資料庫時出現嚴重 Cursor 洩漏現象,執行一段時間後會導致系統中所有使用到該資料庫的程式無法使用。另外在工作中也常發現有些應用有 Cursor 洩漏現象,由於需要長時間執行才會出現異常,所以有的此類 bug 很長時間都沒被發現。

但是一旦 Cursor 洩漏累計到一定數目(通常為數百個)必然會出現無法查詢資料庫的情況,只有等資料庫服務所在程序死掉重啟才能恢復正常。通常的出錯資訊如下,指出某 pid 的程式開啟了 866 個 Cursor 沒有關閉,導致了 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 檢測原理
在 Cursor 物件被 JVM 回收執行到 finalize() 方法的時候,檢測 close() 方法有沒有被呼叫,此辦法在 ContentResolver 裡面也得到應用。簡化後的示例程式碼如下:
複製程式碼 程式碼如下:
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();
}
}
}

然後查詢的時候,把 TestCursor 作為查詢結果返回給 APP:
1 return new TestCursor(cursor); // cursor 是普通查詢得到的結果,例如從 ContentProvider.query()
該方法同樣適合於所有需要檢測顯式釋放資源方法沒有被呼叫的情形,是一種通用方法。但在 finalize() 方法裡檢測需要注意
優點:準確。因為該資源在 Cursor 物件被回收時仍沒被釋放,肯定是發生了資源洩露。
缺點:依賴於 finalize() 方法,也就依賴於 JVM 的垃圾回收策略。例如某 APP 現在有 10 個 Cursor 物件洩露,並且這 10 個物件已經不再被任何引用指向處於可回收狀態,但是 JVM 可能並不會馬上回收(時間不可預測),如果你現在檢查不能夠發現問題。另外,在某些情況下就算物件被回收 finalize() 可能也不會執行,也就是不能保證檢測出所有問題。關於 finalize() 更多資訊可以參考《Effective Java 2nd Edition》的 Item 7: Avoid Finalizers

2. 使用方法
對於 APP 開發人員
從 GINGERBREAD 開始 Android 就提供了 StrictMode 工具協助開發人員檢查是否不小心地做了一些不該有的操作。使用方法是在 Activity 裡面設定 StrictMode,下面的例子是開啟了檢查洩漏的 SQLite 物件以及 Closeable 物件(普通 Cursor/FileInputStream 等)的功能,發現有違規情況則記錄 log 並使程式強行退出。
複製程式碼 程式碼如下:
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();
}
}

對於 framework 開發人員
如果是通過 ContentProvider 提供資料庫資料,在 ContentResolver 裡面已有 CloseGuard 類實行類似檢測,但需要自行開啟(上例也是開啟 CloseGuard):
1 CloseGuard.setEnabled(true);更值得推薦的辦法是按照本文第一節中的檢測原理,在 ContentResolver 內部類 CursorWrapperInner 裡面加入。其他需要檢測類似於資源洩漏的,同樣可以使用該檢測原理。

3. 容易出錯的地方
忘記呼叫 close() 這種低階錯誤沒什麼好說的,這種應該也佔不小的比例。下面說說不太明顯的例子。
提前返回
有時候粗心會犯這種錯誤,在 close() 呼叫之前就 return 了,特別是函式比較大邏輯比較複雜時更容易犯錯。這種情況可以通過把 close() 放在 finally 程式碼塊解決
複製程式碼 程式碼如下:
private void method() {
Cursor cursor = query(); // 假設 query() 是一個查詢資料庫返回 Cursor 結果的函式
if (flag == false) { // !!提前返回
return;
}
cursor.close();
}

類的成員變數
假設類裡面有一個在類全域性有效的成員變數,在方法 A 獲取了查詢結果,後面在其他地方又獲取了一次查詢結果,那麼第二次查詢的時候就應該先把前面一個 Cursor 物件關閉。
複製程式碼 程式碼如下:
public class TestCursor {
private Cursor mCursor;
private void methodA() {
mCursor = query();
}
private void methodB() {
// !!必須先關閉上一個 cursor 物件
mCursor = query();
}
}

注意:曾經遇到過有人對 mCursor 感到疑惑,明明是同一個變數為什麼還需要先關閉?首先 mCursor 是一個 Cursor 物件的引用,在 methodA 時 mCursor 指向了 query() 返回的一個 Cursor 物件 1;在 methodB() 時它又指向了返回的另外一個 Cursor 物件 2。在指向 Cursor 物件 2 之前必須先關閉 Cursor 物件 1,否則就出現了 Cursor 物件 1 在 finalize() 之前沒有呼叫 close() 的情況。
異常處理
開啟和關閉 Cursor 之間的程式碼出現 exception,導致沒有跑到關閉的地方:
複製程式碼 程式碼如下:
try {
Cursor cursor = query();
// 中間省略某些出現異常的程式碼
cursor.close();
} catch (Exception e) {
// !!出現異常沒跑到 cursor.close()
}

這種情況應該把 close() 放到 finally 程式碼塊裡面:
複製程式碼 程式碼如下:
Cursor cursor = null;
try {
cursor = query();
// 中間省略某些出現異常的程式碼
} catch (Exception e) {
// 出現異常
} finally {
if (cursor != null)
cursor.close();
}
 
4. 總結思考
在 finalize() 裡面檢測是可行的,且基本可以滿足需要。針對 finalize() 執行時間不確定以及可能不執行的問題,可以通過記錄目前開啟沒關閉的 Cursor 數量來部分解決,超過一定數目發出警告,兩種手段相結合。

還有沒有其他檢測辦法呢?有,在 Cursor 構造方法以及 close() 方法新增 log,執行一段時間後檢查 log 看哪個地方沒有關閉。簡化程式碼如下:
複製程式碼 程式碼如下:
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.”);
}
}

檢查時看某個 hashCode() 的 Cursor 有沒有呼叫過 close() 方法,沒有的話說明資源有洩露。這種方法優點是同樣準確,且更可靠。缺點是需要檢查大量 log,且開啟/關閉的地方可能相距較遠,如果不寫個小指令碼分析人工看的話會比較痛苦;另外必須 APP 完全退出後才能檢查,因為後臺執行時某些 Cursor 還在正常使用。

您可能感興趣的文章:

android CursorLoader用法介紹Android開發筆記之:深入理解Cursor相關的效能問題Android App除錯記憶體洩露之Cursor篇android在非同步任務中關閉Cursor的程式碼方法

相關文章

Android 開發 最新文章