2013年11月19日星期二

How to detect Android Cursor leak

Summary:This post tells the theory of detecting SQLite database Cursor leak of your Android app, alongside some common mistake examples. Some leaks are hardly noticable in the code until memory errors happen. This sort of method can be applied to other kinds of resources leak as well.

Cursor leak means that you have opened a Cursor object, which is usually associated with a portion of memory, but fail to close it before you lost referece to it. If it do happens and is repeated several hundreds of times, you would finally be unable to query SQLite database, and exception like this would appear. The log says 866 Cursors are opened and memory allocation of 2MB is failed.


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. How to detect leak

Check whether the close() method is called before the Cursor object is garbage collected by the Java virtual machine. If you dive into the code of ContentResolver, you can see this approach is being used. The simpified code is:

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();
        }
    }
}

Then return wrapped TestCursor object to app side as query result:

return new TestCursor(cursor);

Some notes on using finalize():

  • Benifit: Accurate. If close() is not called before the Cursor object being garbage collected by the virtual machine, it's definitely a leak.
  • Drawback: Relies on finalizers, which means it affected by the garbage collector's strategy. For instance, an app leaks 10 Cursor objects, then you check the logs may not be able to see your warning message. Because the objects are likely to be collected some time later, however you do not know the exact time.

Note: Regarding finalizers, please refer to Effective Java 2nd Edition Item 7: Avoid Finalizers

2. How to use

Since GINGERBREAD, Android begin to provide StrictMode tool to help developers to detect things that maight be done by accident. Example code to detect SQLite or Closeable objects leak by enabling StrictMode, then let app crash when a violation is detected:

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();
    }
}

Note: In ContentResolver.java there is a CloseGuard, which can be enabled to detect Cursor leaks. If StrictMode is turn on, then CloseGuard will take effect.

You can also implement one by yourself, just wrap your Cursor like the example of TestCursor.

3. Common mistake examples

First of all, check your code to make sure you do not forget to call close().

Early return

Sometimes you may return before close() is called, especially when your method contains large chunk of code, or an exception occurs in the middle of execution. So putting cleanup code in a finally block is always a good pratice. Here is the bad practice:

private void method() {
    Cursor cursor = query();
    if (flag == false) {  // WRONG: return before close()
        return;
    }
    cursor.close();
}
Good practice should be like this:
private void method() {
    Cursor cursor = null;
    try {
        cursor = query();
    } finally {
        if (cursor != null)
            cursor.close();  // RIGHT: ensure resource is always recovered
    }
}

Member variable

If there is a Cursor member variable, you may forget to call close() and then point it to another object, which would leak the previous cursor object. Here is the wrong example:

public class TestCursor {
    private Cursor mCursor;

    private void methodA() {
        mCursor = query();
    }

    private void methodB() {
        // WRONG: close it before pointing to another Cursor object
        mCursor = query();
    }
}

Note: Someone may get confused about objects and references. In the above example, mCursor is a reference pointing a Cursor object. In methodB() mCursor points to another Cursor object, but there is no reference points to the object created in methodA(), which means you have no chance to call close() on it in the future. That is a resources leak. So you must call close() at the time mCursor is still points to it.

4. More thoughts

Checking close flag in finalizers is generally suitable. You can also add a counter to track how many cursors are alive at a same time and print log when it exceeds a certain number.

Are there any alternatives? The answer is yes, you can log the events of Cursor creation and closing. Then run your app for a while and check logs. Here is the example code:

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.");
    }
}

Write a script to check the Cursors that are not closed(), each Cursor is identified by its hashCode(). This method does not rely on finalizers, but you should not check logs before app exits, because some Cursors may be still in use.

没有评论:

发表评论