Learn open source projects: LeakCanary-How to detect whether Activity is leaking

Learn open source projects: LeakCanary-How to detect whether Activity is leaking

OOM is a common problem in Android development, and memory leaks are often the culprit.

In order to detect memory leaks simply and conveniently, Square has open-sourced LeakCanary, which can monitor whether an Activity has a leak in real time. Once a leak is found, it will automatically pop up a prompt and related leak information for analysis.

The purpose of this article is to explore the Activity leak detection mechanism by analyzing the LeakCanary source code.

How to use LeakCanary

To introduce LeakCanary into our project, we only need to do the following two steps:

  1. dependencies {
  2. debugCompile 'com.squareup.leakcanary:leakcanary-android:1.5.1'  
  3. releaseCompile 'com.squareup.leakcanary:leakcanary-android-no-op:1.5.1'  
  4. testCompile 'com.squareup.leakcanary:leakcanary-android-no-op:1.5.1' } public class ExampleApplication extends Application { @Override public void onCreate() { super.onCreate(); if (LeakCanary.isInAnalyzerProcess(this)) { // This process is dedicated to LeakCanary for heap analysis.
  5. // You should not init your app in this process.
  6. return ;
  7. }
  8. LeakCanary.install(this);
  9. }
  10. }

It can be seen that the most critical thing is LeakCanary.install(this); This sentence officially opens the door to LeakCanary. In the future, it will automatically help us detect memory leaks and pop up notification information when a leak occurs.

Start with LeakCanary.install(this);

Let's take a look at what it does.

  1. public   static RefWatcher install(Application application) { return install(application, DisplayLeakService.class,
  2. AndroidExcludedRefs.createAppDefaults().build());
  3. } public   static RefWatcher install(Application application,
  4. Class<? extends AbstractAnalysisResultService> listenerServiceClass,
  5. ExcludedRefs excludedRefs) { if (isInAnalyzerProcess(application)) { return RefWatcher.DISABLED;
  6. }
  7. enableDisplayLeakActivity(application);
  8. HeapDump.Listener heapDumpListener = new ServiceHeapDumpListener(application, listenerServiceClass);
  9. RefWatcher refWatcher = androidWatcher(application, heapDumpListener, excludedRefs);
  10. ActivityRefWatcher.installOnIcsPlus(application, refWatcher); return refWatcher;
  11. }

First, let’s look at the most important part:

  1. RefWatcher refWatcher = androidWatcher(application, heapDumpListener, excludedRefs);
  2. ActivityRefWatcher.installOnIcsPlus(application, refWatcher);

First, a RefWatcher is generated. This thing is very important. As the name suggests, it is used to watch Reference, that is, a tool for monitoring references. Then pass the refWatcher and the application we provide to ActivityRefWatcher.installOnIcsPlus(application, refWatcher);. Continue reading.

  1. public   static void installOnIcsPlus(Application application, RefWatcher refWatcher) {
  2. ActivityRefWatcher activityRefWatcher = new ActivityRefWatcher(application, refWatcher);
  3. activityRefWatcher.watchActivities();
  4. }

An ActivityRefWatcher is created. You should be able to feel that this thing is used to monitor our Activity leakage. It calls the watchActivities() method to start monitoring. The following is the core principle of its monitoring:

  1. public void watchActivities() {
  2. application.registerActivityLifecycleCallbacks(lifecycleCallbacks);
  3. }

It registers an ActivitylifecycleCallbacks callback function in the application, which can be used to monitor the lifecycle events of all Activities in the entire life cycle of the Application. What is this lifecycleCallbacks?

  1. private final Application.ActivityLifecycleCallbacks lifecycleCallbacks = new Application.ActivityLifecycleCallbacks() { @Override public void onActivityCreated(Activity activity, Bundle savedInstanceState) {
  2. } @Override public void onActivityStarted(Activity activity) {
  3. } @Override public void onActivityResumed(Activity activity) {
  4. } @Override public void onActivityPaused(Activity activity) {
  5. } @Override public void onActivityStopped(Activity activity) {
  6. } @Override public void onActivitySaveInstanceState(Activity activity, Bundle outState) {
  7. } @Override public void onActivityDestroyed(Activity activity) {
  8. ActivityRefWatcher.this.onActivityDestroyed(activity);
  9. }
  10. };

It turns out that it only listens to the onActivityDestroyed events of all Activities. When the Activity is Destoryed, the ActivityRefWatcher.this.onActivityDestroyed(activity); function is called.

I guess, under normal circumstances, when an activity is destroyed by this function, the activity object should become null. If it does not become null, it means that a memory leak has occurred.

Therefore, we think that this function ActivityRefWatcher.this.onActivityDestroyed(activity); should be used to monitor whether the activity object has become null. Continue to watch.

  1. void onActivityDestroyed(Activity activity) {
  2. refWatcher.watch(activity);
  3. }
  4. RefWatcher refWatcher = androidWatcher(application, heapDumpListener, excludedRefs);

It can be seen that this function passes the target activity object to RefWatcher, allowing it to monitor whether the activity is recycled normally. If it is not recycled, it means that a memory leak has occurred.

How does RefWatcher monitor whether the activity is recycled normally?

Let's first take a look at what this RefWatcher is.

  1. public   static RefWatcher androidWatcher(Context context, HeapDump.Listener heapDumpListener,
  2. ExcludedRefs excludedRefs) {
  3. AndroidHeapDumper heapDumper = new AndroidHeapDumper(context, leakDirectoryProvider);
  4. heapDumper.cleanup(); int watchDelayMillis = 5000;
  5. AndroidWatchExecutor executor = new AndroidWatchExecutor(watchDelayMillis); return new RefWatcher(executor, debuggerControl, GcTrigger. DEFAULT , heapDumper,
  6. heapDumpListener, excludedRefs);
  7. }

There are two new objects involved here: AndroidHeapDumper and AndroidWatchExecutor. The former is used to dump the heap memory status, and the latter is used to watch a reference listener. The specific principle will be seen later. In short, a RefWatcher object has been generated here.

Now let's look at refWatcher.watch(activity); called in onActivityDestroyed(Activity activity) above. Let's take a look at this core watch(activity) method to understand how it monitors whether the activity is recycled.

  1. private final Set <String> retainedKeys; public void watch(Object activity, String referenceName) {
  2. String key = UUID.randomUUID().toString();
  3. retainedKeys. add ( key ); final KeyedWeakReference reference = new KeyedWeakReference(activity, key , referenceName, queue);
  4.  
  5. watchExecutor. execute (new Runnable() { @Override public void run() {
  6. ensureGone(reference, watchStartNanoTime);
  7. }
  8. });
  9. }final class KeyedWeakReference extends WeakReference<Object> { public final String key ; public final String name ;
  10. }

As you can see, it first wraps the activity we passed in into a KeyedWeakReference (which can be temporarily regarded as a normal WeakReference), and then watchExecutor will execute a Runnable, which will call the ensureGone(reference, watchStartNanoTime) function.

Before looking at this function, let's guess that we know that the watch function itself is used to monitor whether the activity is recycled normally, which involves two problems:

  1. When to check if it is recycled?
  2. How to efficiently check that it is really recycled?

So we think that what the ensureGone function itself has to do is just as its name suggests, which is to ensure that the reference is recycled, otherwise it means a memory leak.

Core function: ensureGone(reference) detection recovery

Let's look at this function implementation:

  1. void ensureGone(KeyedWeakReference reference, long watchStartNanoTime) {
  2. removeWeaklyReachableReferences(); if (gone(reference) || debuggerControl.isDebuggerAttached()) { return ;
  3. }
  4. gcTrigger.runGc();
  5. removeWeaklyReachableReferences(); if (!gone(reference)) {
  6. File heapDumpFile = heapDumper.dumpHeap();
  7. heapdumpListener.analyze( new HeapDump(heapDumpFile, reference. key , reference. name , excludedRefs, watchDurationMs,
  8. gcDurationMs, heapDumpDurationMs));
  9. }
  10. }private boolean gone(KeyedWeakReference reference) { return !retainedKeys. contains (reference. key );
  11. }private void removeWeaklyReachableReferences() {
  12. KeyedWeakReference ref; while ((ref = (KeyedWeakReference) queue.poll()) != null ) {
  13. retainedKeys.remove(ref. key );
  14. }
  15. }

Here we first explain how WeakReference and ReferenceQueue work.

1. WeakReference

Strongly referenced objects will never be reclaimed by the garbage collector even if OOM occurs; weakly referenced objects will be reclaimed immediately once they are discovered by the garbage collector; softly referenced objects are memory-sensitive and will only be reclaimed when memory is insufficient, and are often used as memory-sensitive caches; phantom references may be reclaimed at any time and are less commonly used.

2. ReferenceQueue

We often use a WeakReference<Activity> reference = new WeakReference(activity);, where we create a reference to a certain activity. When the activity is garbage collected, the reference will be put into the internal ReferenceQueue. In other words, all references taken from the queue ReferenceQueue, the real objects they point to have been successfully recycled.

Then go back to the code above.

When an activity is passed to RefWatcher, a unique key corresponding to the activity is created and stored in a collection retainedKeys. In other words, all the unique keys corresponding to the activities we want to observe will be put into the retainedKeys collection.

Based on our understanding of ReferenceQueue, as long as all references in the queue are taken out and the keys in the corresponding retainedKeys are removed, the objects corresponding to the remaining keys will not be recycled.

  1. ensureGone first calls removeWeaklyReachableReferences to remove the keys of the recycled objects from retainedKeys, and the remaining keys are all unrecycled objects;
  2. if (gone(reference)) is used to determine whether the key of a reference is still in retainedKeys. If not, it means it has been recycled, otherwise continue;
  3. gcTrigger.runGc(); Manually trigger GC and immediately recycle all objects referenced by WeakReference;
  4. removeWeaklyReachableReferences(); clean up retainedKeys again. If the reference is still in retainedKeys (if (!gone(reference))), it indicates a leak;
  5. Use heapDumper to dump the memory situation into a file, and call heapdumpListener to perform memory analysis to further confirm whether a memory leak occurs.
  6. If a memory leak is confirmed, call DisplayLeakService to send a notification.

At this point, we have finished looking at the core memory leak detection mechanism.

Memory leak detection summary

From the above, we have a general understanding of the memory leak detection mechanism, which is roughly the following steps:

  1. Use application.registerActivityLifecycleCallbacks(lifecycleCallbacks) to monitor the Activity onDestoryed event throughout the lifecycle;
  2. When an Activity is destroyed, it is passed to RefWatcher for observation to ensure that it will be recycled normally later;
  3. RefWatcher first references the Activity using KeyedWeakReference, and uses a ReferenceQueue to record whether the object pointed to by the KeyedWeakReference has been recycled;
  4. AndroidWatchExecutor will start to check whether the Activity in this weak reference is recycled normally after 5 seconds. The judgment condition is: if the Activity is recycled normally, the KeyedWeakReference that references it will be automatically put into the ReferenceQueue.
  5. The judgment method is: first check whether the KeyedWeakReference corresponding to the Activity has been put into the ReferenceQueue; if not, manually GC: gcTrigger.runGc();; and then judge again whether the ReferenceQueue already contains the corresponding KeyedWeakReference. If it has not been collected, it is considered that a memory leak may have occurred.
  6. Use HeapAnalyzer to analyze the memory situation of the dump and further confirm it. If a leak is confirmed, use DisplayLeakService to send a notification.

Explore some interesting questions about LeakCanary

After studying the source code of LeakCanary, I would like to raise a few more interesting questions for discussion.

Why is the LeakCanary project directory structure divided like this?

The following is the entire LeakCanary project structure:

For developers, they only need to use LeakCanary.install(this);. Why is the entire project divided into so many modules?

In fact, each module here has its own role.

  • leakcanary-watcher: This is a general memory detector that provides a RefWatcher#watch(Object watchedReference). It can be seen that it can not only detect Activity, but also monitor the leakage of any regular Java Object.
  • leakcanary-android: This module is the access point to the Android world, and is used to monitor Activity leaks. It uses the application#registerActivityLifecycleCallbacks method internally to listen to the onDestory event, and then uses leakcanary-watcher to perform weak reference + manual GC mechanism for monitoring.
  • leakcanary-analyzer: This module provides HeapAnalyzer, which is used to analyze the dumped memory and return the memory analysis result AnalysisResult, which contains information such as the path where the leak occurred for developers to find and locate.
  • leakcanary-android-no-op: This module is specifically for release versions. It only provides two completely blank classes, LeakCanary and RefWatcher, which will not perform any memory leak analysis. Why? Because LeakCanary itself will affect the operation of the app due to constant GC, and it is mainly used for memory leak detection in the development phase. Therefore, for release, all leak analysis can be disabled.
  • leakcanary-sample: This is very simple, it just provides a usage sample.

When an Activity is destroyed, how long does it take for LeakCanary to check for leaks?

As can be seen in the source code, LeakCanary does not check immediately after destruction, but lets an AndroidWatchExecutor do the check. What does it do?

  1. @Override public void execute (final Runnable command) { if (isOnMainThread()) {
  2. executeDelayedAfterIdleUnsafe(command);
  3. } else {
  4. mainHandler.post(new Runnable() { @Override public void run() {
  5. executeDelayedAfterIdleUnsafe(command);
  6. }
  7. });
  8. }
  9. }void executeDelayedAfterIdleUnsafe(final Runnable runnable) { // This needs to be called from the main thread.
  10. Looper.myQueue().addIdleHandler(new MessageQueue.IdleHandler() { @Override public boolean queueIdle() {
  11. backgroundHandler.postDelayed(runnable, delayMillis); return   false ;
  12. }
  13. });
  14. }

As you can see, it first adds an IdleHandler to the main thread's MessageQueue.

What is IdleHandler? We know that Looper will continuously take out Message from MessageQueue and execute it. When there is no new Message to execute, Looper enters the Idle state and takes out IdleHandler to execute.

In other words, IdleHandler is a message with a lower priority, which is only processed when Looper has no messages to process. Moreover, if the internal queueIdle() method returns true, it means that the task is always alive and will be executed every time Looper enters Idle; otherwise, if it returns false, it means that it will only be executed once and discarded after execution.

So, what is this lower priority task? backgroundHandler.postDelayed(runnable, delayMillis); , runnable is the previous ensureGone().

That is to say, when the main thread is idle and has nothing to do, it starts sending a delayed message to the background thread, telling the background thread to start checking whether the Activity has been recycled after 5s (delayMillis).

Therefore, when an Activity is destroyed, it must first wait until the main thread is idle, and then delay for 5 seconds (delayMillis) before starting the leak check.

Knowledge points:

1. How to create a low-priority main thread task that will only execute when the main thread is idle and will not affect the performance of the app?

  1. Looper.myQueue().addIdleHandler(new MessageQueue.IdleHandler() { @Override public boolean queueIdle() { // do task
  2. return   false ; // only once
  3. }
  4. });

2. How to quickly create a main/sub-thread handler?

  1. //Main thread handlermainHandler = new Handler(Looper.getMainLooper()); //Sub thread handlerHandlerThread handlerThread = new HandlerThread("Sub thread task");
  2. handlerThread.start();
  3. Handler backgroundHandler = new Handler(handlerThread.getLooper());

3. How to quickly determine whether the current thread is running?

  1. Looper.getMainLooper().getThread() == Thread.currentThread();

Can System.gc() trigger an immediate GC? If not, how can I trigger an immediate GC?

In LeakCanary, it is necessary to trigger GC immediately and then determine whether the weak reference is recycled immediately. This means that the GC must be able to be executed synchronously immediately.

The commonly used method to trigger GC is System.gc(), but can it meet our requirements?

Let's look at how it is implemented:

  1. /**
  2. * Indicates to the VM that it would be a good time   to run the
  3. * garbage collector. Note that this is a hint only . There is   no guarantee
  4. * that the garbage collector will actually be run.
  5. */ public   static void gc() { boolean shouldRunGC; synchronized(lock) {
  6. shouldRunGC = justRanFinalization; if (shouldRunGC) {
  7. justRanFinalization = false ;
  8. } else {
  9. runGC = true ;
  10. }
  11. } if (shouldRunGC) {
  12. Runtime.getRuntime().gc();
  13. }
  14. }

The comments clearly state that System.gc() only suggests that the garbage collector should perform collection, but it cannot guarantee that it will actually be collected. It can also be seen from the code that you must first determine shouldRunGC before deciding whether to actually perform GC.

Knowledge points:

So how to implement real-time GC?

LeakCanary refers to a piece of AOSP code

  1. // System.gc() does not garbage collect every time . Runtime.gc() is // more likely to perfom a gc.Runtime.getRuntime().gc();
  2. enqueueReferences();
  3. System.runFinalization(); public   static void enqueueReferences() { /*
  4. * Hack. We don't have a programmatic way to wait for the reference queue
  5. * daemon to   move   references   to the appropriate queues.
  6. */
  7. try {
  8. Thread.sleep(100);
  9. } catch (InterruptedException e) { throw new AssertionError();
  10. }
  11. }

How can LeakCanary be modified?

Ignore certain classes or activities that are known to leak

LeakCanary provides an ExcludedRefs class, to which you can add some actively ignored classes. For example, if there are some memory leaks in the Android source code that are not leaks in our App, then we can exclude them.

In addition, if you do not want to monitor some special Activities, you can filter out the special Activities in onActivityDestroyed(Activity activity) and only call refWatcher.watch(activity) to monitor other Activities.

Upload memory leak data to the server

LeakCanary provides AbstractAnalysisResultService, which is an intentService. The received intent contains HeapDump data and AnalysisResult results. As long as we inherit this class and implement our own listenerServiceClass, we can upload the heap data and analysis results to our own server.

summary

This article analyzes the principle of LeakCanary through source code, raises some interesting questions, and learns some practical knowledge points. I hope it will be inspiring to readers, and you are welcome to discuss with me.

We will continue to select high-quality open source projects for analysis, and welcome your comments.

<<:  Why did Ke Jie say "I lost my temper after losing"? 8 questions to interpret the first game of the man-machine match

>>:  A brief analysis of the HTTPS principle and its use in Android

Recommend

Brand promotion: How to create a hit product?

When it comes to hot products, what do you think ...

European truck manufacturers may face 100 billion euros in claims

Recently, according to Reuters, litigation manage...

Tsavorite: The new noble green gemstone

introduction Starting today, the "Treasures ...

Wentian Laboratory, what is it going to do in space?

Mixed Knowledge Specially designed to cure confus...

【3Dmax】LM Ranger advanced animation full process case teaching binding chapter

【3Dmax】LM Ranger Advanced Animation Full Process C...

5G mobile phones, waiting for spring

[[330468]] Currently, from mobile phone manufactu...

Upgrading to iOS 10 will render 40% of iPads obsolete

According to ZDNet, Apple's iPad tablet sales...

A complete and effective event planning plan!

Event planning refers to the planning of differen...

If I keep exercising, will I still have thick, black hair when I am 70?

Review expert: Peng Guoqiu, deputy chief physicia...