Four Java references
- Strong references: Never recycle
- Soft reference: Reclaim only when memory is insufficient
- Weak references: recycle on touch
- Virtual reference: Equivalent to no reference, but used to indicate whether the object to which it refers was reclaimed.
Use of weak references
We can specify a reference queue for weak references. When the object to which the weak reference points is collected, the weak reference will be added to the queue. We can determine whether the object to which the weak reference points is collected by checking whether the weak reference is in the queue.
// Create a reference queue
ReferenceQueue<Object> queue = new ReferenceQueue<>();
private void test(a) {
// Create an object
Object obj = new Object();
// Create a weak reference, point to the object, and pass the reference queue to the weak reference
WeakReference<Object> reference = new WeakReference(obj, queue);
// Print the weak reference to prove the same as the comparison in the queue after gc
System.out.println("The weak reference is :" + reference);
// select * from gc.
System.gc();
// Print queue (should be empty)
printlnQueue("before");
// Set obj to null, obj can be recycled
obj = null;
// Then gc, obj should be collected and queue should have weak references
System.gc();
// Print the queue again
printlnQueue("after");
}
private void printlnQueue(String tag) {
System.out.print(tag);
Object obj;
// Loop to print the reference queue
while((obj = queue.poll()) ! =null) {
System.out.println(":" + obj);
}
System.out.println();
}
Copy the code
The print result is as follows:
The weak reference is: Java. Lang. Ref. The WeakReference @ 6 e0be858 before after: Java lang. Ref. 6 e0be858 WeakReference @Copy the code
From the above code, we can see that when obj is not null, gc will find nothing in the queue; If obj is set to null, the queue contains a weak reference, which indicates that obj has been collected. You can select Add Vm Options in the Run/Debug Configuration of idea to print gc logs. No more nonsense here.
With this feature, we can detect memory leaks in an Activity. As we know, an Activity is destroyed after onDestroy(), so if we use a weak reference to point to the Activity and specify a reference queue for it, then after onDestroy(), To determine whether the Activity was reclaimed, check to see if there is a weak reference for the Activity in the reference queue.
So, how in the onDestroy (), with the Application of registerActivityLifecycleCallbacks () the API, you can detect all the lifecycle of the Activity, Check onActivityDestroyed(activity) that the weak reference is in the reference queue. If the weak reference is in the reference queue, the activity is destroyed. Otherwise, the activity leaks. At this point, you can print out the relevant information.
It is important to note, however, that onDestroy() is called, which means that the activity is destroyed, not that gc has already occurred. Therefore, if necessary, we need to manually invoke GC to ensure that our memory leak detection logic is executed after GC. This will prevent false positives.
So when is it necessary? Actually Leakcanary has already been written for us, let’s just look at its code.
How LeakCanary works
This article is aimed at1.5.4
Version of the
Let’s start by integrating LeanCanary into our project as follows:
1 Add dependencies in Gradle
debugCompile 'com. Squareup. Leakcanary: leakcanary - android: 1.5.4'
Copy the code
2 Perform initialization in MainaApplication
LeakCanary.install(this);
Copy the code
After the above two steps, we have integrated LeakCanary into our project, and let’s see how it works.
Let’s follow along with install():
public static RefWatcher install(Application application) {
return refWatcher(application) // Create an object
.listenerServiceClass(DisplayLeakService.class) // It is used to analyze and display leakage data
.excludedRefs(AndroidExcludedRefs.createAppDefaults().build()) // Exclude references that do not need parsing
.buildAndInstall(); // mainline logic
}
Copy the code
RefWatcher (application) simply creates an object and saves the application argument as follows:
public static AndroidRefWatcherBuilder refWatcher(Context context) {
return new AndroidRefWatcherBuilder(context);
}
AndroidRefWatcherBuilder(Context context) {
// This saves the context
this.context = context.getApplicationContext();
}
Copy the code
We’ll just follow the main line buildAndInstall():
public RefWatcher buildAndInstall(a) {
RefWatcher refWatcher = build(); // Create objects, and create log parsers, GC triggers, heap dumps, etc.
if(refWatcher ! = DISABLED) { LeakCanary.enableDisplayLeakActivity(context);// Take the context and convert it to Application
ActivityRefWatcher.install((Application) context, refWatcher);
}
return refWatcher;
}
Copy the code
Follow the mainline code ActivityRefWatcher. Install (), the following code in ActivityRefWatcher:
public static void install(Application application, RefWatcher refWatcher) {
new ActivityRefWatcher(application, refWatcher).watchActivities();
}
// Only the variable is saved
public ActivityRefWatcher(Application application, RefWatcher refWatcher) {
this.application = checkNotNull(application, "application");
this.refWatcher = checkNotNull(refWatcher, "refWatcher");
}
// Observe all activities
public void watchActivities(a) {
// Stop the previous observation first to prevent repeated observation
stopWatchingActivities();
// Observe all activities directly
application.registerActivityLifecycleCallbacks(lifecycleCallbacks);
}
// Remove the Activity observation
public void stopWatchingActivities(a) {
application.unregisterActivityLifecycleCallbacks(lifecycleCallbacks);
}
// The Activity's lifecycle observer
private final Application.ActivityLifecycleCallbacks lifecycleCallbacks =
new Application.ActivityLifecycleCallbacks() {
/ /... Omit useless code
@Override public void onActivityDestroyed(Activity activity) {
// When the Activity is destroyed, it checks to see if it is recycled
ActivityRefWatcher.this.onActivityDestroyed(activity); }};// Check whether the activity is recycled
void onActivityDestroyed(Activity activity) {
refWatcher.watch(activity);
}
Copy the code
Now back to RefWatcher:
// The argument is the destroyed Activity
public void watch(Object watchedReference) {
watch(watchedReference, "");
}
public void watch(Object watchedReference, String referenceName) {
if (this == DISABLED) {
return;
}
checkNotNull(watchedReference, "watchedReference");
checkNotNull(referenceName, "referenceName");
// Record the current time
final long watchStartNanoTime = System.nanoTime();
// Generate a corresponding key for the Activity
String key = UUID.randomUUID().toString();
// Add the Activity's corresponding key to the collection retainedKeys
retainedKeys.add(key);
// Core code that creates a weak reference to the Activity and specifies a reference queue
final KeyedWeakReference reference = new KeyedWeakReference(watchedReference, key, referenceName, queue);
// Mainline code
ensureGoneAsync(watchStartNanoTime, reference);
}
Copy the code
KeyedWeakReference is a weak reference:
final class KeyedWeakReference extends WeakReference<Object> {
public final String key;
public final String name;
KeyedWeakReference(Object referent, String key, String name, ReferenceQueue<Object> referenceQueue) {
super(checkNotNull(referent, "referent"), checkNotNull(referenceQueue, "referenceQueue"));
this.key = checkNotNull(key, "key");
this.name = checkNotNull(name, "name"); }}Copy the code
Following the main line ensureGoneAsync:
private void ensureGoneAsync(final long watchStartNanoTime, final KeyedWeakReference reference) {
// The watchExecutor implementation is AndroidWatchExecutor
watchExecutor.execute(new Retryable() {
@Override public Retryable.Result run(a) {
// Check whether the Activity is recycled
returnensureGone(reference, watchStartNanoTime); }}); }// Core code
Retryable.Result ensureGone(final KeyedWeakReference reference, final long watchStartNanoTime) {
// Calculate the time difference prompt to the development
long gcStartNanoTime = System.nanoTime();
long watchDurationMs = NANOSECONDS.toMillis(gcStartNanoTime - watchStartNanoTime);
// Try to remove the key corresponding to the Activity that has been collected (because the code may have already been gc)
removeWeaklyReachableReferences();
// Check whether the Activity has been recycled (if the key is removed, it is recycled)
if (gone(reference)) {
return DONE;
}
// If it is not collected, try a gc(this is what we said above when necessary, more on this later)
gcTrigger.runGc();
// Remove again after gc
removeWeaklyReachableReferences();
// If the Activity has not been reclaimed, a leak has occurred
if(! gone(reference)) {long startDumpHeap = System.nanoTime();
long gcDurationMs = NANOSECONDS.toMillis(startDumpHeap - gcStartNanoTime);
// Grab heap information and generate files
File heapDumpFile = heapDumper.dumpHeap();
if (heapDumpFile == RETRY_LATER) {
return RETRY;
}
long heapDumpDurationMs = NANOSECONDS.toMillis(System.nanoTime() - startDumpHeap);
// The leak results are analyzed and notified to the appropriate service, and a notification pops up telling us that a leak has occurred
heapdumpListener.analyze(
newHeapDump(heapDumpFile, reference.key, reference.name, excludedRefs, watchDurationMs, gcDurationMs, heapDumpDurationMs)); }return DONE;
}
// Check whether the Activity has been reclaimed, as long as the corresponding Activity key is missing, it has been reclaimed
private boolean gone(KeyedWeakReference reference) {
return! retainedKeys.contains(reference.key); }// Remove all objects that have already been reclaimed, and remove the corresponding activity key when reclaimed
private void removeWeaklyReachableReferences(a) {
KeyedWeakReference ref;
// Iterate through the reference queue while removing the Activity key to which the weak reference points
while((ref = (KeyedWeakReference) queue.poll()) ! =null) { retainedKeys.remove(ref.key); }}Copy the code
As you can see, first of all, we throw the code logic to the watchExecutor (watchExecutor is actually an AndroidWatchExecutor, which is used to switch threads), and when our logic runs, There is a good chance that a GC has already occurred (thanks to the watchExecutor), so we try to clear the activity’s key queue once, and then check if the destroyed activity has been retrieved. If it hasn’t been retrieved, it doesn’t necessarily leak because gc hasn’t been done yet. So we do a manual GC, and then check again to see if the activity’s corresponding key is still in the key queue. If it is, then a leak has occurred, and dump the heap space and related information directly to the developer.
Remember the key we generated for the Activity, when the Activity is reclaimed, the weak reference to it is put into the reference queue, so when we detect this reference in the queue, the Activity has been reclaimed. Remove the key from the retainedKeys queue. So, when an Activity is destroyed, add its corresponding key to the retainedKeys queue and wait until gc detects the retainedKeys queue. If the corresponding key is still in the queue, a memory leak has occurred.
There is a question here, why gc may or may not have occurred, and is it possible to accurately determine whether GC has occurred?
Can’t!
Quite simply, we know that Android Gc is implemented through GcIdler, which is an IdleHandler.
final class GcIdler implements MessageQueue.IdleHandler {
@Override
public final boolean queueIdle(a) {
doGcIfNeeded();
purgePendingResources();
return false; }}Copy the code
The system posts a Message labeled GC_WHEN_IDLE to the ActivityThread when it is idle and calls
Looper.myQueue().addIdleHandler(mGcIdler)
Copy the code
To put it bluntly: Android’s Gc process is implemented through idle messages and is of low priority.
So, when is the system idle?
When there is no message executing in the MainLooper, it is idle and the contents of the mIdleHandlers are executed and the GC is executed.
According to the above analysis, our detection logic needs to be placed after GC to ensure correctness, so it needs to be executed after mIdleHandlers. However, the system does not provide lower priority than mIdleHandlers. We have to put our detection logic in mIdleHandlers and take a chance, because what if it runs after the GC, what if it doesn’t? More on that later.
AndroidWatchExecutor does just that.
AndroidWatchExecutor
When we analyzed the main line code, we executed the check logic in watchExecutor.execute(), so here’s the branch logic:
// Mainline logic entry code.
// Check and switch to the Main thread for execution. Why must it be on the Main thread?
@Override
public void execute(Retryable retryable) {
if (Looper.getMainLooper().getThread() == Thread.currentThread()) {
waitForIdle(retryable, 0);
} else {
postWaitForIdle(retryable, 0); }}void postWaitForIdle(final Retryable retryable, final int failedAttempts) {
// mainHandler is for the Main thread
mainHandler.post(new Runnable() {
@Override public void run(a) { waitForIdle(retryable, failedAttempts); }}); }// An idle message is delivered directly to addIdleHandler
void waitForIdle(final Retryable retryable, final int failedAttempts) {
// Because you need to be in the Main thread
Looper.myQueue().addIdleHandler(new MessageQueue.IdleHandler() {
@Override public boolean queueIdle(a) {
// Post to the worker thread to detect if a leak has occurred
postToBackgroundWithDelay(retryable, failedAttempts);
return false; }}); }// Post to the worker thread for detection
void postToBackgroundWithDelay(final Retryable retryable, final int failedAttempts) {
long exponentialBackoffFactor = (long) Math.min(Math.pow(2, failedAttempts), maxBackoffFactor);
long delayMillis = initialDelayMillis * exponentialBackoffFactor;
// This handler is created by HandlerThread
backgroundHandler.postDelayed(new Runnable() {
@Override public void run(a) {
// A callback is triggered here
Retryable.Result result = retryable.run();
// Retry logic, negligible
if (result == RETRY) {
postWaitForIdle(retryable, failedAttempts + 1);
}
}
}, delayMillis);
}
Copy the code
The logic above is simple. The first is to switch to the Main thread, because the system is idle means that the Looper of the Main thread has no messages to process, so we need to put it in the Main thread. The second is to run our code through IdleHandler to take a chance and see if we can stay ahead of GC.
Follow-up question: what if it doesn’t end up behind GC?
Then you have to go back to the bottom logic: do gc again manually! Like gctrigger.rungc () in the code above; The same.
Here someone said, so troublesome, you directly manual GC is not on the line, why so laborious.
This is not true because every GC stops all threads, which causes the app to stall. Also, if a GC has just occurred and we call a GC manually, then The Times of the two gc stacks up and the stalling becomes more obvious, which is not friendly. Therefore, we pray that the check logic occurs after the system GC, coupled with the bottom-of-the-pocket logic of manual GC, is the right solution.
The logic for manual GC is also simple and is implemented with the help of GcTrigger.
GcTrigger
public interface GcTrigger {
// Provides a default implementation, which is used by default if not specified manually
GcTrigger DEFAULT = new GcTrigger() {
// Mainline logic entry code
public void runGc(a) {
// Perform gc first
Runtime.getRuntime().gc();
// Wait for weak references to be queued.
this.enqueueReferences();
// Trigger the Object finalize() method
System.runFinalization();
}
// Sleep 100ms waiting for GC to complete and weak references to enqueue
private void enqueueReferences(a) {
try {
Thread.sleep(100L);
} catch (InterruptedException var2) {
throw newAssertionError(); }}};void runGc(a);
}
Copy the code
So, why don’t we use soft references? Soft references can do the same thing.
Because soft references are: insufficient memory is reclaimed, sufficient memory is not reclaimed, and we are checking for leaks, not sufficient memory.
If there is a leak now, but there is still enough memory, the soft reference will not be detected, so we will use a weak reference and recycle it when it hits.
conclusion
The streamlining process is as follows:
-
1 LeakCanary.install(application); RegisterActivityLifecycleCallbacks using application at this time, to monitor the Activity of when to be destroy.
-
2 In the onActivityDestroyed(Activity Activity) callback, check whether the Activity is recovered as follows:
-
3. Use a WeakReference to point to the activity, specify a reference queue for the WeakReference, and create a key to identify the activity.
-
4 The detected method ensureGone() is then posted to the idle message queue.
-
5 When an idle message is executed, the queue is checked to see if there is a weak reference. If there is a weak reference, the activity has been reclaimed and the corresponding key removed. No memory leak occurs.
-
6 If the weak reference does not exist in the queue, gc is performed manually.
-
If there is no weak reference in the queue, it indicates that the activity has not been collected and a memory leak has occurred. If there is no weak reference in the queue, it directly dumps the stack information and prints the log. Otherwise, there is no memory leak and the process ends.
Key issues:
- 1 Why do I put it in an idle message?
Since GC occurs when the system is idle, by the time the idle message is executed, there is a high probability that gc has already been performed.
- Why can idle messages be detected directly
activity
Is it recycled?
As in problem 1, gc is likely to have occurred by the time the idle message is executed, so you can check to see if the activity is reclaimed after GC.
- 3 if it was not recovered, it should have been leaked. Why was it executed again
gc
And then get tested?
According to Question 2, when the idle message is executed, there is a high probability that gc has already occurred, but it may not have occurred yet, so it is normal for the activity not to be reclaimed. Therefore, we manually perform gc again to ensure that GC has occurred, and then check whether the activity is reclaimed to be 100% sure that a memory leak has occurred.
For those unfamiliar with Java references and collection, see the JVM garbage collection process here
Handler Handler Handler Handler Handler Handler Handler Handler Handler Handler