Search code examples
javaandroidmemory-leaksmessage-queueleakcanary

Memory Leak in MessageQueue in android?


I am having a memory leak in my MainActivity.java which was detected by LeakCanary. This is my Leak Trace.

┬───
│ GC Root: Input or output parameters in native code
│
├─ android.os.MessageQueue instance
│    Leaking: NO (MessageQueue#mQuitting is false)
│    HandlerThread: "main"
│    ↓ MessageQueue.mMessages
│                   ~~~~~~~~~
├─ android.os.Message instance
│    Leaking: UNKNOWN
│    Retaining 14.2 kB in 348 objects
│    Message.what = 0
│    Message.when = 37524601 (681 ms after heap dump)
│    Message.obj = null
│    Message.callback = instance @319985112 of com.application.app.
│    MainActivity$$ExternalSyntheticLambda2
│    ↓ Message.callback
│              ~~~~~~~~
├─ com.application.app.MainActivity$$ExternalSyntheticLambda2 instance
│    Leaking: UNKNOWN
│    Retaining 12 B in 1 objects
│    f$0 instance of com.application.app.MainActivity with mDestroyed =
│    true
│    ↓ MainActivity$$ExternalSyntheticLambda2.f$0
│                                             ~~~
╰→ com.application.app.MainActivity instance
      Leaking: YES (ObjectWatcher was watching this because com.defenderstudio.
      geeksjob.MainActivity received Activity#onDestroy() callback and
      Activity#mDestroyed is true)
      Retaining 11.2 MB in 5622 objects
      key = e98df529-afa0-4e0c-b0f0-51a5d3eaf67c
      watchDurationMillis = 5249
      retainedDurationMillis = 248
      mApplication instance of android.app.Application
      mBase instance of androidx.appcompat.view.ContextThemeWrapper

METADATA

Build.VERSION.SDK_INT: 30
Build.MANUFACTURER: samsung
LeakCanary version: 2.7
App process name: com.application.app
Count of retained yet cleared: 6 KeyedWeakReference instances
Stats: LruCache[maxSize=3000,hits=6544,misses=134885,hitRate=4%]
RandomAccess[bytes=5904498,reads=134885,travel=75990168059,range=41137566,size=5
3483782]
Heap dump reason: 7 retained objects, app is visible
Analysis duration: 31639 ms

I can't understand what is the problem here. I closed all the postdelayed() method when ondestroy() is called. This is the code :

@Override
protected void onDestroy() {
    dialog = new Dialog(MainActivity.this, R.style.dialog);
    if (dialog.isShowing()) {
        dialog.dismiss();
    }
    if (handler != null && statusChecker != null) {
        handler.removeCallbacks(statusChecker);
    }
    if (databaseReference != null && userSignInInfoReference != null && eventListener != null) {
        databaseReference.removeEventListener(eventListener);
        userSignInInfoReference.removeEventListener(eventListener);
    }
    progressDialog = new ProgressDialog(MainActivity.this, R.style.ProgressDialogStyle);
    if (progressDialog.isShowing()) {
        progressDialog.dismiss();
    }
    headerView = null;
    super.onDestroy();
}

Please help me out here!

NOTE : Also Please tell me what is MessageQueue and how close all leaks of it. Thanks in advance!


Solution

  • What is a MessageQueue?

    There are 3 key Android classes tied together: Handler, Looper and MessageQueue. When a Looper instance is created, it creates its own MessageQueue instance. Then you can create a Handler et give it the Looper instance. When you call Handler.post() (or postDelayed), under the hood you're actually calling Handler.sendMessage which enqueues a Message instance on the Message queue associated to the Looper associated to that Handler.

    What happens to those enqueued messages? Somewhere else in the code (i.e. dedicated HandlerThread), something calls Looper.loop() which loops forever, removing one entry at a time from the associated Message queue and running that message. If the queue is empty, the Looper waits for the next message (that's done via native code). Some more context: https://developer.squareup.com/blog/a-journey-on-the-android-main-thread-psvm/

    What can we read from the LeakTrace?

    We see that the MessageQueue at the top is for the following HandlerThread: "main". That's the main thread. So if you're doing a postDelayed on the main thread, a message gets enqueued into the message queue.

    Messages are stored as a linked list: the MessageQueue holds on to the first message (via its mMessages field) and then each message holds on to the next.

    We can see that the head of the queue is a Message and we can see its content.

    Message.when = 37524601 (681 ms after heap dump)

    This tells us the message was enqueued with a delay and will execute in 681ms (after the heap dump was taken)

    Message.callback = instance @319985112 of com.application.app.MainActivity$$ExternalSyntheticLambda2

    This tells us the callback enqueued is an inner lambda defined in MainActivity. It's hard to figure out which one, but if you decompile the bytecode (e.g. class files or dex) you should be able to tell which lambda has that name.

    Fix

    Most likely, you have a piece a code that keeps rescheduling itself as a postDelayed on the main thread, even after the activity is destroyed. That callback needs to be canceled in onDestroy()

    Edit: note on the advice to using a WeakReference in the other answer: that's not good general advice. From the doc: https://square.github.io/leakcanary/fundamentals-fixing-a-memory-leak/#4-fix-the-leak

    Memory leaks cannot be fixed by replacing strong references with weak references. It’s a common solution when attempting to quickly address memory issues, however it never works. The bugs that were causing references to be kept longer than necessary are still there. On top of that, it creates more bugs as some objects will now be garbage collected sooner than they should. It also makes the code much harder to maintain.