January 17, 2017

Activity Leak Detection

Prior Knowledge

Reference and ReferenceQueue

Begin with Java 1.2, the java.lang.ref package give us limited degree of interaction with garbage collector. The subclasses of Reference and ReferenceQueue are quite useful if we wanna do something with garbage collection.
The constructor of Reference takes a referent and a ReferenceQueue as parameters.

Reference(T r, ReferenceQueue<? super T> q) {
    referent = r;
    queue = q;
}

We can regard ReferenceQueue as a callback of garbage collector. At some point, garbage collector will enqueue the reference. For different reference types, the time is different.
Three different type of references exist, each being weaker than the preceding one: SoftReference, WeakReference and PhantomReference. "Weakness" here means that less restrictions are being imposed on the garbage collector as to when it is allowed to actually garbage-collect the referenced object.
Garbage collector will not clear the referent of SoftReference until the runtime must reclaim memory to satisfy an allocation. But it's not recommended using it for caching as it's inefficient.
WeakReference is commonly used for "canonical mappings". WeakHashMap is a typical case. Unlike SoftReference, WeakReference will be cleared and enqueued as soon as is known to be weakly-referenced.
PhantomReference is a better alternative for finalize() method. As finalize() has performance problem and is unpredictable, we can use PhantomReference to do some pre-mortem cleanup.
For both SoftReference and WeakReference, the referent will be nullified before enqueued. And the garbage collector will finalize these referent at some future time.
As for PhantomReference, garbage collector will finish the finalization of referent and then enqueue the reference, but it will not nullify the referent. Thus PhantomReference must be used together with a valid reference queue, otherwise it's useless. And we have to clear the reference queue manually, if not, memory of the referent cannot be reclaimed by garbage collector.

Activity Leak Detection

How to Define Activity Leak?

In brief, if an activity has run over its onDestroy() method but garbage collector cannot reclaim its memory, the activity is leaked.

Activity Diagram

I made a activity diagram to demonstrate how to detect an activity leak in Android.

  1. Register life cycle callback by void registerActivityLifecycleCallbacks (Application.ActivityLifecycleCallbacks callback).
  2. On every activity's onDestroy() callback, create a WeakReference of it with a non-null ReferenceQueue.
  3. Force the VM do a garbage collection.
  4. Check the reference queue, if the reference we created before has not been enqueued, it's very likely that it leaks.
    ActivityLeakDetection
Further Explaination
  1. void registerActivityLifecycleCallbacks (Application.ActivityLifecycleCallbacks callback) only supports Android API level 14 and above. If you need compatibility for API level below 14, you can substitute mInstrumentation in ActivityThread with your own implementation. It's not hard to do with reflection. But on some few third-party ROM which modified the ActivityThread.java, this method does not work. As ActivityThread is not in public API, this method doesn't guarantee to work on future Android versions.
  2. Actually, there's no way to force the VM do garbage collection. We can only suggest the VM to do a garbage collection, but we can't guarantee it really did. That's why I said it is just very likely that the activity leaks if its reference is not enqueued after a forced garbage collection. There will be a few false positives due to Java garbage collector's behavior.
    Here's code taken from Android Finalization Tester:
    public static void induceFinalization() {
        System.gc();
        enqueueReferences();
        System.runFinalization();
    }
    
  3. We can't check reference queue immediately, as garbage collector need time to enqueue references, but we don't have a programmatic way to determine the time, we can only use an empirical value. Here's the implementation of enqueueReferences():
    public static void enqueueReferences() {
        /*
         * Hack. We don't have a programmatic way to wait for the       reference queue
         * daemon to move references to the appropriate queues.
         */
        try {
            Thread.sleep(100);
        } catch (InterruptedException e) {
            throw new AssertionError();
        }
    }