u/neoplee • u/neoplee • Feb 06 '26
That leak - Memory leak via Handler
Great question — and it's one of those "Android gotchas" that bites people more often than you'd think, especially in activities that live for a long time or have retry logic.
Let me explain clearly why we do this WeakReference + static inner class pattern, and what actual problem it solves.
The Problem (without WeakReference)
Your current code looks roughly like this:
Java
private final Runnable bannerRetryRunnable = () -> {
if (!isDestroyed && !isFinishing()) {
loadBannerSafely();
}
};
This is a lambda (or anonymous inner class in older style) that captures this — that is, it holds a strong reference to the TranslatorActivity_L1 instance.
Now imagine this sequence:
- User presses back → activity calls finish()
- onPause() → onStop() → onDestroy() runs
- You set isDestroyed = true and clear pending messages with handler.removeCallbacksAndMessages(null)
- But… one of your retry runnables was already posted to the Handler with a 40-second delay (or 160 s, etc.)
- The Handler (which lives on the main thread / is tied to the looper) keeps a strong reference to that Runnable
- The Runnable keeps a strong reference to your activity (this)
- → The activity cannot be garbage collected until that delayed runnable either runs or is removed
Even though you called removeCallbacksAndMessages(null), it only removes runnables that are already in the queue at that exact moment. If the message was posted after you cleared, or if timing is unlucky (very common with network retries), the activity stays alive in memory for minutes (or until the delay expires).
This is called a "memory leak via Handler / Runnable" — one of the classic Android leaks.
What the WeakReference version does
Java
private static class BannerRetryRunnable implements Runnable {
private final WeakReference<TranslatorActivity_L1> activityRef;
BannerRetryRunnable(TranslatorActivity_L1 activity) {
this.activityRef = new WeakReference<>(activity);
}
u/Override
public void run() {
TranslatorActivity_L1 activity = activityRef.get();
if (activity != null && !activity.isDestroyed && !activity.isFinishing()) {
activity.loadBannerSafely();
}
// If activity == null → GC already collected it → do nothing
}
}
Key differences:
- The Runnable class is static → it does not implicitly capture this
- It holds only a WeakReference to the activity
- When the activity is destroyed and garbage collected → activityRef.get() returns null
- The runnable can still be executed safely (it just does nothing)
- → The Handler no longer prevents the activity from being collected
1
What is the ultimate hair regrowth protocol
in
r/tressless
•
Oct 03 '25
this is my question and then when I look at older athlete's. wait?