簡介:java
本文介紹如何在 Android 檢測 Cursor 泄漏的原理以及使用方法,還指出幾種常見的出錯示例。有一些泄漏在代碼中難以察覺,但程序長時間運行後必然會出現異常。同時該方法一樣適合於其餘須要檢測資源泄露的狀況。android
最近發現某蔬菜手機鏈接程序在查詢媒體存儲(MediaProvider)數據庫時出現嚴重 Cursor 泄漏現象,運行一段時間後會致使系統中全部使用到該數據庫的程序沒法使用。另外在工做中也常發現有些應用有 Cursor 泄漏現象,因爲須要長時間運行纔會出現異常,因此有的此類 bug 很長時間都沒被發現。sql
可是一旦 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) |
在 Cursor 對象被 JVM 回收運行到 finalize() 方法的時候,檢測 close() 方法有沒有被調用,此辦法在 ContentResolver 裏面也獲得應用。簡化後的示例代碼以下:app
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:ide
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 Finalizersui
從 GINGERBREAD 開始 Android 就提供了 StrictMode 工具協助開發人員檢查是否不當心地作了一些不應有的操做。使用方法是在 Activity 裏面設置 StrictMode,下面的例子是打開了檢查泄漏的 SQLite 對象以及 Closeable 對象(普通 Cursor/FileInputStream 等)的功能,發現有違規狀況則記錄 log 並使程序強行退出。this
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 }
若是是經過 ContentProvider 提供數據庫數據,在 ContentResolver 裏面已有 CloseGuard 類實行相似檢測,但須要自行打開(上例也是打開 CloseGuard):
1 CloseGuard.setEnabled(true);
更值得推薦的辦法是按照本文第一節中的檢測原理,在 ContentResolver 內部類 CursorWrapperInner 裏面加入。其餘須要檢測相似於資源泄漏的,一樣可使用該檢測原理。
忘記調用 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 }
在 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 還在正常使用。