如何檢測 Android Cursor 泄漏


簡介

本文介紹如何在 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 里面也得到應用。簡化后的示例代碼如下:

 1 import android.database.Cursor;
 2 import android.database.CursorWrapper;
 3 import android.util.Log;
 4 
 5 public class TestCursor extends CursorWrapper {
 6     private static final String TAG = "TestCursor";
 7     private boolean mIsClosed = false;
 8     private Throwable mTrace;
 9     
10     public TestCursor(Cursor c) {
11         super(c);
12         mTrace = new Throwable("Explicit termination method 'close()' not called");
13     }
14     
15     @Override
16     public void close() {
17         mIsClosed = true;
18     }
19     
20     @Override
21     public void finalize() throws Throwable {
22         try {
23             if (mIsClosed != true) {
24                 Log.e(TAG, "Cursor leaks", mTrace);
25             }
26         } finally {
27             super.finalize();
28         }
29     }
30 }

然后查詢的時候,把 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 並使程序強行退出。

 1 import android.os.StrictMode;
 2 
 3 public class TestActivity extends Activity {
 4     private static final boolean DEVELOPER_MODE = true;
 5     public void onCreate() {
 6         if (DEVELOPER_MODE) {
 7             StrictMode.setVMPolicy(new StrictMode.VMPolicy.Builder()
 8                     .detectLeakedSqlLiteObjects()
 9                     .detectLeakedClosableObjects()
10                     .penaltyLog()
11                     .penaltyDeath()
12                     .build());
13         }
14         super.onCreate();
15     }
16 }

 

對於 framework 開發人員

如果是通過 ContentProvider 提供數據庫數據,在 ContentResolver 里面已有 CloseGuard 類實行類似檢測,但需要自行打開(上例也是打開 CloseGuard):

1 CloseGuard.setEnabled(true);

更值得推薦的辦法是按照本文第一節中的檢測原理,在 ContentResolver 內部類 CursorWrapperInner 里面加入。其他需要檢測類似於資源泄漏的,同樣可以使用該檢測原理。

3. 容易出錯的地方

忘記調用 close() 這種低級錯誤沒什么好說的,這種應該也占不小的比例。下面說說不太明顯的例子。

提前返回

有時候粗心會犯這種錯誤,在 close() 調用之前就 return 了,特別是函數比較大邏輯比較復雜時更容易犯錯。這種情況可以通過把 close() 放在 finally 代碼塊解決

1 private void method() {
2     Cursor cursor = query(); // 假設 query() 是一個查詢數據庫返回 Cursor 結果的函數
3     if (flag == false) {  // !!提前返回
4         return;
5     }
6     cursor.close();
7 }

 

類的成員變量

假設類里面有一個在類全局有效的成員變量,在方法 A 獲取了查詢結果,后面在其他地方又獲取了一次查詢結果,那么第二次查詢的時候就應該先把前面一個 Cursor 對象關閉。

 1 public class TestCursor {
 2     private Cursor mCursor;
 3 
 4     private void methodA() {
 5         mCursor = query();
 6     }
 7 
 8     private void methodB() {
 9         // !!必須先關閉上一個 cursor 對象
10         mCursor = query();
11     }
12 }

注意:曾經遇到過有人對 mCursor 感到疑惑,明明是同一個變量為什么還需要先關閉?首先 mCursor 是一個 Cursor 對象的引用,在 methodA 時 mCursor 指向了 query() 返回的一個 Cursor 對象 1;在 methodB() 時它又指向了返回的另外一個 Cursor 對象 2。在指向 Cursor 對象 2 之前必須先關閉 Cursor 對象 1,否則就出現了 Cursor 對象 1 在 finalize() 之前沒有調用 close() 的情況。

異常處理

打開和關閉 Cursor 之間的代碼出現 exception,導致沒有跑到關閉的地方:

1 try {
2     Cursor cursor = query();
3     // 中間省略某些出現異常的代碼
4     cursor.close();
5 } catch (Exception e) {
6     // !!出現異常沒跑到 cursor.close()
7 }

這種情況應該把 close() 放到 finally 代碼塊里面:

 1 Cursor cursor = null;
 2 try {
 3     cursor = query();
 4     // 中間省略某些出現異常的代碼
 5 } catch (Exception e) {
 6     // 出現異常
 7 } finally {
 8     if (cursor != null)
 9         cursor.close();
10 }

4. 總結思考

在 finalize() 里面檢測是可行的,且基本可以滿足需要。針對 finalize() 執行時間不確定以及可能不執行的問題,可以通過記錄目前打開沒關閉的 Cursor 數量來部分解決,超過一定數目發出警告,兩種手段相結合。

還有沒有其他檢測辦法呢?有,在 Cursor 構造方法以及 close() 方法添加 log,運行一段時間后檢查 log 看哪個地方沒有關閉。簡化代碼如下:

 1 import android.database.Cursor;
 2 import android.database.CursorWrapper;
 3 import android.util.Log;
 4 
 5 public class TestCursor extends CursorWrapper {
 6     private static final String TAG = "TestCursor";
 7     private Throwable mTrace;
 8     
 9     public TestCursor(Cursor c) {
10         super(c);
11         mTrace = new Throwable("cusor opened here");
12         Log.d(TAG, "Cursor " + this.hashCode() + " opened, stacktrace is: ", mTrace);
13     }
14     
15     @Override
16     public void close() {
17         mIsClosed = true;
18         Log.d(TAG, "Cursor " + this.hashCode() + " closed.");
19     }
20 }

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

轉載請注明出處:http://www.cnblogs.com/imouto/archive/2013/01/14/how-to-detect-leaked-cursor.html

本文外部鏡像:http://oteku.blogspot.com/2013/01/how-to-detect-android-cursor-leak-cn.html


免責聲明!

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



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