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.4Version 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 directlyactivityIs 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 againgcAnd 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