Explain the possible memory leakage caused by Handler and the solutions at one time

Author: shrimp Mi Jun

1. Improper use of handler?

First find out what is improper use of} Handler?

Generally, it has the following characteristics:

  1. Handler adopts anonymous internal class or internal class extension, and holds the reference of external class {Activity} by default:
// Anonymous Inner Class 
override fun onCreate(savedInstanceState: Bundle?) {
    ...
    val innerHandler: Handler = object : Handler(Looper.getMainLooper()) {
        override fun handleMessage(msg: Message) {
            Log.d(
                "MainActivity",
                "Anonymous inner handler message occurred & what:${msg.what}"
            )
        }
    }
}
// Inner class
override fun onCreate(savedInstanceState: Bundle?) {
    ...
    val innerHandler: Handler = MyHandler(Looper.getMainLooper())
}

inner class MyHandler(looper: Looper): Handler(looper) {
    override fun handleMessage(msg: Message) {
        Log.d(
            "MainActivity",
            "Inner handler message occurred & what:\${msg.what}"
        )
    }
}
  1. When an Activity exits, the Handler is still reachable. There are two situations:
  • When exiting, there is still a Thread in processing, which references the Handler
  • When exiting, although the Thread ends, the Message is still queued or being processed in the queue, indirectly holding the Handler
override fun onCreate(savedInstanceState: Bundle?) {
    ...
    val elseThread: Thread = object : Thread() {
        override fun run() {
            Log.d(
                "MainActivity",
                "Thread run"
            )
            
            sleep(2000L)
            innerHandler.sendEmptyMessage(1)
        }
    }.apply { start() }
}

2. Why is there a memory leak?

During the execution of the above Thread, if the Activity enters the background, destroy will be triggered due to insufficient memory. When the virtual machine marks the GC} object, the following two situations occur:

  • Thread has not ended and is active

    The active Thread, as the GC Root object, holds the Handler instance, and the Handler holds the instance of the external class Activity by default. The reference chain of this layer can still reach:

  • Although the Thread has ended, the sent Message has not been processed yet

    The Message sent by Thread may still be waiting in the queue, or it may just be in the callback of handleMessage(). At this moment, the "Looper" holds the Message through the "messagequeue", the Handler is held by the Message as the "target" attribute, and the Handler holds the Activity, resulting in the Looper indirectly holding the Activity.

    You may not notice that the Main Looper of the main thread is different from the loopers of other threads.

    In order to make it easy for any thread to obtain the Looper instance of the main thread, Looper defines it as the static attribute sMainLooper.

public final class Looper {
    private static Looper sMainLooper;  // guarded by Looper.class
    ...
    public static void prepareMainLooper() {
        prepare(false);
        synchronized (Looper.class) {
            sMainLooper = myLooper();
        }
    }
}

The static attribute is also a GC Root object, which causes the Activity to be reachable through the above application chain.

In both cases, the Activity instance will not be marked correctly until the Thread ends and the Message is processed. Until then, the Activity instance will not be recycled.

The inner class Thread will also make the Activity unable to recycle, right?

In order to focus on the memory leak caused by Handler, the reference chain directly generated by Thread is not described.

In the above code example, the Thread also takes the form of an anonymous inner class, which of course also holds an Activity instance. From this point of view, the unfinished Thread will directly occupy the Acitvity instance, which is also a reference chain leading to Activity memory leakage. You should pay attention to it!

3. Will the sub thread Looper cause memory leakage?

In order to facilitate each thread to obtain a unique Looper instance, the Looper class uses the static sThreadLocal attribute to manage each instance.

public final class Looper {
    static final ThreadLocal<Looper> sThreadLocal = new ThreadLocal<Looper>();
    ...
    public static @Nullable Looper myLooper() {
        return sThreadLocal.get();
    }
}

However, as a static attribute, sThreadLocal is also a GC Root object. From this perspective, will it also indirectly lead to the failure of Message recycling?

Yes, but not because of ThreadLocal, but because of Thread.

Looking at the source code of ThreadLocal, you will find that the target object is not stored in ThreadLocal, but in its static internal class ThreadLocalMap. In addition, in order to be unique to threads, the Map is held by threads, and their life cycles are the same.

// TheadLocal.java
public class ThreadLocal<T> {
    public void set(T value) {
        Thread t = Thread.currentThread();
        ThreadLocalMap map = getMap(t);
        if (map != null)
            map.set(this, value);
        else
            createMap(t, value);
    }

    // Create a Map and put it into the Thread
    void createMap(Thread t, T firstValue) {
        t.threadLocals = new ThreadLocalMap(this, firstValue);
    }
    
    ThreadLocalMap getMap(Thread t) {
        return t.threadLocals;
    }
}
// Thread.java
public class Thread implements Runnable {
    ThreadLocal.ThreadLocalMap threadLocals = null;
}

Further details are: the element {Entry} in ThreadLocalMap uses a weak reference to hold the ThreadLocal object as} key}, but it is strongly referenced as the target object of {value}.

This causes the Thread to indirectly hold the target object, such as the Looper instance this time. This can ensure that the Looper life cycle is consistent with the Thread, but if the Looper life cycle is too long, there will be a risk of memory leakage (of course, this is not the responsibility of the Looper designer).

// TheadLocal.java
public class ThreadLocal<T> {
    ...
    static class ThreadLocalMap {
        private Entry[] table;

        static class Entry extends WeakReference<ThreadLocal<?>> {
            /** The value associated with this ThreadLocal. */
            Object value;

            Entry(ThreadLocal<?> k, Object v) {
                super(k);
                value = v;
            }
        }
    }
}

The reference relationship between the weakly referenced key instance and the Map will be cut off because of GC, but the value will not be set to null until the next manual execution of {set}, get} or {remove} of the Entry. During this period of time, value has been strongly quoted, causing hidden dangers.

The entries in this hash map extend WeakReference, using its main ref field as the key (which is always a ThreadLocal object).

Note that null keys (i.e. entry.get() == null) mean that the key is no longer referenced, so the entry can be expunged from table.

Such entries are referred to as "stale entries" in the code that follows.

However, it should be noted that the instance sThreadLocal held by Looper here is static and will not be recycled until the process ends. It is impossible for the above-mentioned key to be recycled and value to be isolated.

In short, Looper's ThreadLocal does not cause memory leaks.

Back to the Thread, because it strongly refers to Looper, the Thread will still cause memory leakage of Message due to this factor. For example, if a Runnable written in an internal class is sent to the child Thread Handler, when the Activity ends, the Runnable has not arrived or is in the process of execution, the Activity will always be reachable because of the following reference chain, that is, the possibility of memory leakage.

The solution is not complex:

  • At the end of the Activity, remove all unprocessed messages from the child thread Handler, such as #removecallbacksandmessages(). This will cut off the reference relationship from MessageQueue to Message and from Message to Runnable
  • A better approach is to call} loop #quit() or} quitsafe(), which will empty all messages or future messages and promote the end of} loop() polling. The end of the child thread means that the reference starting point GC Root no longer exists

Finally, make clear several points of consensus:

  1. Manages the static property sThreadLocal of the Looper instance. Instead of holding the ThreadLocalMap that actually stores the Looper, it reads and writes through the Thread. Although sThreadLocal is expensive as GC Root, it cannot reach the reference chain of Looper, and thus cannot constitute a memory leak from this path!

  2. In addition, because it is a static attribute, there is no risk of memory leakage in which the key is recycled and the value is isolated

  3. Finally, since Thread holds Looper value, there is a possibility of memory leakage from this path

4. Will non inner class handlers leak memory?

As mentioned above, anonymous inner class or inner class is a feature of memory leakage caused by Handler. Will it cause leakage if Handler does not use the writing method of inner class?

For example:

override fun onCreate(...) {
    Handler(Looper.getMainLooper()).apply {
        object : Thread() {
            override fun run() {
                sleep(2000L)
                post { 
                    // Update ui
                }
            }
        }.apply { start() }
    }
}

Memory leaks may still occur.

Although Handler is not an internal class, post's {Runnable} is also an internal class, which also holds an instance of Activity. In addition, the Runnable of the post to the Handler will eventually be held by the Message as a callback attribute.

Based on these two performances, even if the Handler is not an internal class, because Runnable is an internal class, there is also a risk that the Activity is improperly held by Thread or Main Looper.

5. Can the Callback interface of network transmission really be solved?

There is a saying on the Internet: when creating a Handler, instead of overriding handleMessage(), specify the instance of the Callback interface, so as to avoid memory leakage. The reason is that Android studio will not pop up the following warning after this writing:

This Handler class should be static or leaks might occur.

In fact, if the Callback instance is still an anonymous inner class or the writing method of an inner class, it will still cause memory leakage, but the AS does not pop up this layer of warning.

private Handler mHandler = new Handler(new Handler.Callback() {
    @Override  
    public boolean handleMessage(Message msg) {  
        return false;  
    }  
}); 

For example, in the above writing method, the Handler will hold the Callback instance passed in, and the Callback is written as an internal class, which holds the reference of the external class Activity by default.

public class Handler {
    final Callback mCallback;

 public Handler(@NonNull Looper looper, @Nullable Callback callback) {
        this(looper, callback, false);
    }

    public Handler(@NonNull Looper looper, @Nullable Callback callback, boolean async) {
        ...
        mCallback = callback;
    }
}

Whether from the point of view of Thread Activity or from the point of view of Thread ending but Message still not being executed, it will lead to the risk of memory leakage that the Activity can still be indirectly referenced by GC Root.

Essentially, it is the same problem as the Runnable example above:

6. Correctly use Handler?

When GC marks, the Thread has ended and the Message has been processed. Once the conditions are not met, the life cycle of the Activity will be wrongly extended, resulting in memory leakage!

How can this be avoided? In fact, there should be an answer to the above characteristics:

  • First, change the strong reference Activity into a weak reference
  • The second is to cut off the reference chain relationship between the two GC roots in time: Main Looper to Message, and end the sub thread

The code example is briefly described as follows:

  1. Define Handler or Callback or Runnable as static inner class:
class MainActivity : AppCompatActivity() {
    private class MainHandler(looper: Looper?, referencedObject: MainActivity?) :
            WeakReferenceHandler<MainActivity?>(looper, referencedObject) {
        override fun handleMessage(msg: Message) {
            val activity: MainActivity? = referencedObject
            if (activity != null) {
                // ...
            }
        }
    }
}
  1. You also need to weakly reference an instance of an external class:
open class WeakReferenceHandler<T>(looper: Looper?, referencedObject: T) :     Handler(looper!!) {
    private val mReference: WeakReference<T> = WeakReference(referencedObject)

    protected val referencedObject: T?
        protected get() = mReference.get()
}
  1. When onDestroy, cut off the reference chain relationship and correct the life cycle:
  • When the Activity is destroyed, if the sub Thread task has not ended, interrupt the {Thread in time:
override fun onDestroy() {
    ...
    thread.interrupt()
}

If a Looper is created in a child thread and becomes a Looper thread, you must manually quit. For example, HandlerThread:

override fun onDestroy() {
    ...
    handlerThread.quitSafely()
}
  • The Looper of the main thread cannot be quit manually, so it is also necessary to manually clear the unprocessed messages of the Handler in the main thread:
override fun onDestroy() {
    ...
    mainHandler.removeCallbacksAndMessages(null)
}

※ 1: Message will clear its reference relationship with Main Handler after executing ※ recycle()

※ 2: the loop sub thread will clear the Message when calling quit, so there is no need to clear the Message for the Handler of the sub thread

epilogue

Review the key points of this article:

  • The life cycle of the Handler that holds the Activity instance should be consistent with that of the Activity
  • If the Activity should have been destroyed, but the asynchronous Thread is still active or the sent Message has not been processed, the life cycle of the Activity instance will be wrongly extended
  • As a result, the Activity instance that should be recycled cannot be recycled in time because it is occupied by sub threads or main loopers

Simply put, the right thing to do:

  1. When using the Handler mechanism, whether overriding the Handler's {handleMessage() method, specifying the Callback} Callback} method, or sending the task's {Runnable} method, try to use static internal classes + weak references to avoid strong references to instances holding activities.

Ensure that the Activity can be recycled by GC in time even if the life cycle is wrongly extended

  1. At the same time, when the Activity ends, clear the Message, terminate the Thread or exit the Looper in time to recycle the Thread or Message.

Ensure that the reference chain from GC Root to Activity can be completely cut off

Keywords: Java Android Design Pattern Framework Handler

Added by minds_gifts on Wed, 05 Jan 2022 12:03:06 +0200