How Android detects Cursor leakage and how to use it

  • 2020-05-07 20:28:34
  • OfStack

About :
This article describes how to detect Cursor leaks in Android and how to use them. It also points out several common examples of errors. Some leaks are hard to detect in the code, but exceptions are bound to occur when the program runs for a long time. The method is also suitable for other situations where resource leakage needs to be detected.

Recently, it was found that a certain vegetable mobile phone connection program had a serious Cursor leak when querying the media storage (MediaProvider) database. After running for a period of time, all the programs using the database in the system could not be used. In addition, it is often found that some applications have Cursor leakage phenomenon in the work, because it takes a long time to run before there is an exception, so some of these bug have not been discovered for a long time.

However, once the Cursor leakage accumulates to a certain number (usually several hundred), it is inevitable that the database cannot be quered, and only when the process where the database service is located dies and restarts can it return to normal. The usual error message is as follows, indicating that an pid program opened 866 Cursor and did not close, resulting in 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 detection principle
When an Cursor object is recycled by JVM and run into the finalize() method, check whether the close() method is called. This method is also used in ContentResolver. The simplified sample code is as follows:
 
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, when querying, TestCursor is returned to APP as the query result:
1 return new TestCursor (cursor); // cursor is the result of a normal query, for example, from ContentProvider.query ()
This method is also suitable for all cases where you need to detect that the explicitly released resource method is not being called. It is a generic method. However, in the finalize() method, the detection needs attention
advantages : accurate. Because the resource was not released when the Cursor object was recovered, a resource leak must have occurred.
's disadvantage is : relying on the finalize() method also depends on JVM's garbage collection strategy. For example, an APP now has 10 Cursor objects exposed, and these 10 objects are no longer in the recyclable state by any reference, but JVM may not be recycled immediately (the time is unpredictable), if you check now and cannot find the problem. In addition, in some cases even if the object is recovered finalize() may not be executed, which means there is no guarantee that all problems will be detected. More information on finalize() can be found in Effective Java 2nd Edition Item 7: Avoid Finalizers

2. Use method
For APP developers
Starting with GINGERBREAD, Android provides StrictMode tools to help developers check if they've accidentally done something they shouldn't. The method of use is to set StrictMode in Activity. The following example is to open the function of checking the leaked SQLite object and Closeable object (common Cursor/FileInputStream, etc.), and record log and force the program to exit if any violation is found.
 
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(); 
} 
} 

For framework developers
If the database data is provided through ContentProvider, CloseGuard class has already implemented similar detection in ContentResolver, but it needs to be opened by itself (the above example also opens CloseGuard) :
1 CloseGuard. setEnabled (true); A more recommended method is to add ContentResolver inner class CursorWrapperInner according to the detection principle in section 1 of this article. Others that need to detect similar resource leaks can also use this detection principle.

3. The pitfalls
There's nothing wrong with forgetting to call close(), and that should be a big part of it. Here are some less obvious examples.
Return early
Sometimes this is a careless mistake, return before the close() call, especially if the function is large and the logic is complex. This situation can be resolved by placing close() in an finally code block
 
private void method() { 
Cursor cursor = query(); //  Assuming that  query()  is 1 Two query databases are returned  Cursor  Function of the result  
if (flag == false) { //  !!!!! Return early  
return; 
} 
cursor.close(); 
} 

Member variable of the class
If there is a member variable in the class that is globally valid, and the query result is obtained in method A, and another query result is obtained in another place, then the first Cursor object should be closed in the second query.
 
public class TestCursor { 
private Cursor mCursor; 
private void methodA() { 
mCursor = query(); 
} 
private void methodB() { 
//  !!!!! You have to close it first 1 a  cursor  object  
mCursor = query(); 
} 
} 

Note: some people have been confused about mCursor. Why should it be closed first when it is the same variable? First, mCursor is a reference to an Cursor object. When methodA, mCursor points to an Cursor object 1 returned by query(). At methodB() it points to the return of another Cursor object 2. You must close Cursor object 1 before pointing to Cursor object 2, or you will have a situation where Cursor object 1 does not call close() before finalize().
exception handling
opened and closed Cursor between the code exception appears, resulting in not running to the closed place:
 
try { 
Cursor cursor = query(); 
//  Some of the code with exceptions is omitted  
cursor.close(); 
} catch (Exception e) { 
//  !!!!! Abnormal did not run to  cursor.close() 
} 

In this case, close() should be placed in the finally code block:
 
Cursor cursor = null; 
try { 
cursor = query(); 
//  Some of the code with exceptions is omitted  
} catch (Exception e) { 
//  abnormal  
} finally { 
if (cursor != null) 
cursor.close(); 
} 
 
4. Think in summary
The detection in finalize() is feasible and basically meets the requirements. The problem of uncertain execution time and possible non-execution of finalize() can be partially solved by recording the number of Cursor currently open and not closed, and issuing warnings over a fixed number.

Are there any other tests? Yes, add log to the Cursor constructor and close() method. After running for 1 period, check log to see where it is not closed. The simplified code is as follows:
 
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."); 
} 
} 

Check to see if an Cursor of hashCode() ever called the close() method. If not, the resource is compromised. This method has the advantage of being equally accurate and more reliable. The disadvantage is that a lot of log needs to be checked, and the places where it is opened/closed may be far away from each other, which will be painful if you don't write a small script to analyze it manually. In addition, APP cannot be checked until it has fully exterminated, because some Cursor are still in use when the background is running.

Related articles: