[Android Component Interpretation] Leak Canary Explanation

Keywords: Android Java OkHttp Fragment

Preface

Leak Canary is a small tool for Android memory detection provided by Square. It can help us locate code hidden BUG quickly and reduce the chance of OOM.

Here is the git address link: https://github.com/square/leakcanary

Side Topic: Square is really a conscientious company, providing many well-known components. Subsequently, the well-known components on the market will be sorted out. For example, Facebook's Open Source Components... Now let's talk about Square's Open Source Components.

OKHttp is an open source and stable Http communication dependency library. It feels better than HttpUrlConnection.
    okhttp is now officially recognized by Google.

Okie OKHttp relies on this library

dagger fast dependency injection framework. Now it's maintained by google.
Now it should be Dagger 2. Official address: https://google.github.io/dagger/

picasso, a picture cache library, can download and cache pictures

Retrofit is an encapsulation of the Http network request framework of RESTFUL (Baidu). Based on OKHttp, retrofit is the encapsulation of the interface. In essence, it uses OKHttp to make network requests.

leakcanary is a small tool to detect memory. This is what I'm talking about in this article.

otto Android Event Bus, which reduces code coupling, can be compared with EventBus.

...

Back to the text, let's start with the use of Leak Canary.

Use LeakCanary

Actually, it can be referred to. Introduction to leakcanary's Sample

  1. First of all, it's referenced in build.gradle
dependencies {
    compile fileTree(dir: 'libs', include: ['*.jar'])
    androidTestCompile('com.android.support.test.espresso:espresso-core:2.2.2', {
        exclude group: 'com.android.support', module: 'support-annotations'
    })
    compile 'com.android.support:appcompat-v7:25.2.0'
    compile 'com.android.support.constraint:constraint-layout:1.0.0-alpha9'
    testCompile 'junit:junit:4.12'

    // LeakCanary
    debugCompile 'com.squareup.leakcanary:leakcanary-android:1.5'
    releaseCompile 'com.squareup.leakcanary:leakcanary-android-no-op:1.5'
    testCompile 'com.squareup.leakcanary:leakcanary-android-no-op:1.5'
}
  1. Adding methods to onCreate in Application
    public class ExampleApp extends Application{
    @Override
    public void onCreate() {
        super.onCreate();
        // LeakCanary initialization
        LeakCanary.install(this);
    }
}
  1. Adding memory leak code to App, this paper writes a System Clock. sleep (20000) referring to the example in sample.
public class MainActivity extends AppCompatActivity {

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);

        setContentView(R.layout.activity_main);

        Button asynTaskBtn = (Button) this.findViewById(R.id.async_task);
        asynTaskBtn.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                startAsyncTask();
            }
        });
    }

    private void startAsyncTask() {
        // This async task is an anonymous class and therefore has a hidden reference to the outer
        // class MainActivity. If the activity gets destroyed before the task finishes (e.g. rotation),
        // the activity instance will leak.
        new AsyncTask<Void, Void, Void>() {
            @Override protected Void doInBackground(Void... params) {
                // Do some slow work in background
                SystemClock.sleep(20000);
                return null;

            }
        }.execute();
    }
}
  1. Running, you will find a LeakCanary icon. Later, we will introduce how this icon appears. When there is a memory leak, a memory leak notification will be displayed in the notification bar. Clicking on the notification will enter the specific problem of memory leak.

    LeakCanary Icon

Notification Bar Displays Memory Leakage

Memory leak details

According to the icon, we can see that the memory leak is in AsyncTask, and we can modify the memory according to AsyncTask.

After explaining how to use it, let's start with Leak Canary.

Leak Canary

Code directory structure

.
├── AbstractAnalysisResultService.java 
- ActivityRefWatcher. Java - Activity monitors their life cycles
 - Android Debugger Control. Java - Android Debugger Control Switch is to judge Debug. is Debugger Connected ()
- Android ExcludedRefs. Java -- Memory leak base class
 - Android HeapDumper. Java -- Generating. hrpof classes
 - Android WatchExecutor. Java - Android monitors threads and delays execution for 5s
 - DisplayLeakService.java -- Displays memory leaks in the notification bar, implements AbstractAnalysis ResultService. Java
 -- LeakCanary.java -- Provides install(this) methods for classes
├── ServiceHeapDumpListener.java 
- internal -- This folder is used to display memory leaks (interface related)
    - DisplayLeak Activity. Java -- Activeness for Memory Leak Display
    ├── DisplayLeakAdapter.java 
    ├── DisplayLeakConnectorView.java 
    ├── FutureResult.java
    - HeapAnalyrService. Java is a Service launched by another process to receive data and send it to the interface
    ├── LeakCanaryInternals.java
    ├── LeakCanaryUi.java
    └── MoreDetailsView.java

LeakCanary.install(this)

In fact, LeakCanary offers only a way out.

LeakCanary.install(this);

Start from here, corresponding to the source code

/**
 * Creates a {@link RefWatcher} that works out of the box, and starts watching activity
 * references (on ICS+).
 */
public static RefWatcher install(Application application) {
    return install(application, DisplayLeakService.class,
            AndroidExcludedRefs.createAppDefaults().build());
}

/**
 * Creates a {@link RefWatcher} that reports results to the provided service, and starts watching
 * activity references (on ICS+).
 */
public static RefWatcher install(Application application,
                                 Class<? extends AbstractAnalysisResultService> listenerServiceClass,
                                 ExcludedRefs excludedRefs) {
    // Determine whether the Analyr process is in progress
    if (isInAnalyzerProcess(application)) {
        return RefWatcher.DISABLED;
    }
    // Activity Allows Display of Memory Leakage
    enableDisplayLeakActivity(application);

    HeapDump.Listener heapDumpListener =
            new ServiceHeapDumpListener(application, listenerServiceClass);

    RefWatcher refWatcher = androidWatcher(application, heapDumpListener, excludedRefs);
    ActivityRefWatcher.installOnIcsPlus(application, refWatcher);
    return refWatcher;
}

Why Leak Canary requires more than 4.0

<mark> You can see from the annotations that LeakCanary is a method for more than 4.0

references (on ICS+).

Why use more than 4.0?

ActivityRefWatcher.installOnIcsPlus(application, refWatcher);

This method tells us that this is used for Ics + (i.e. over version 4.0), so what is the specific use of this kind of activity RefWatcher?

@TargetApi(ICE_CREAM_SANDWICH) public final class ActivityRefWatcher {

  public static void installOnIcsPlus(Application application, RefWatcher refWatcher) {
    if (SDK_INT < ICE_CREAM_SANDWICH) {
      // If you need to support Android < ICS, override onDestroy() in your base activity.
      return;
    }
    ActivityRefWatcher activityRefWatcher = new ActivityRefWatcher(application, refWatcher);
    activityRefWatcher.watchActivities();
  }

  private final Application.ActivityLifecycleCallbacks lifecycleCallbacks =
      new Application.ActivityLifecycleCallbacks() {
        @Override public void onActivityCreated(Activity activity, Bundle savedInstanceState) {
        }

        @Override public void onActivityStarted(Activity activity) {
        }

        @Override public void onActivityResumed(Activity activity) {
        }

        @Override public void onActivityPaused(Activity activity) {
        }

        @Override public void onActivityStopped(Activity activity) {
        }

        @Override public void onActivitySaveInstanceState(Activity activity, Bundle outState) {
        }

        @Override public void onActivityDestroyed(Activity activity) {
          ActivityRefWatcher.this.onActivityDestroyed(activity);
        }
      };

  private final Application application;
  private final RefWatcher refWatcher;

  /**
   * Constructs an {@link ActivityRefWatcher} that will make sure the activities are not leaking
   * after they have been destroyed.
   */
  public ActivityRefWatcher(Application application, final RefWatcher refWatcher) {
    this.application = checkNotNull(application, "application");
    this.refWatcher = checkNotNull(refWatcher, "refWatcher");
  }

  void onActivityDestroyed(Activity activity) {
    refWatcher.watch(activity);
  }

  public void watchActivities() {
    // Make sure you don't get installed twice.
    stopWatchingActivities();
    application.registerActivityLifecycleCallbacks(lifecycleCallbacks);
  }

  public void stopWatchingActivities() {
    application.unregisterActivityLifecycleCallbacks(lifecycleCallbacks);
  }
}

Application. registerActivity Lifecycle Callbacks (lifecycle Callbacks); this method is used on Android 4.0 to observe the life cycle of Activity. As you can see from the above code, LeakCanary monitors the destruction of Activity

  ActivityRefWatcher.this.onActivityDestroyed(activity);

How LeakCanary's icon appears

public static void setEnabled(Context context, final Class<?> componentClass,
                                  final boolean enabled) {
        final Context appContext = context.getApplicationContext();
        // time-consuming operation
        executeOnFileIoThread(new Runnable() {
            @Override
            public void run() {
                ComponentName component = new ComponentName(appContext, componentClass);
                PackageManager packageManager = appContext.getPackageManager();
                int newState = enabled ? COMPONENT_ENABLED_STATE_ENABLED : COMPONENT_ENABLED_STATE_DISABLED;
                // Blocks on IPC.
                packageManager.setComponentEnabledSetting(component, newState, DONT_KILL_APP);
            }
        });
    }

Called when the install method executes

 // Activity Allows Display of Memory Leakage
enableDisplayLeakActivity(application);

This method executes the method setEnable shown above. The core method is packageManager. setComponentEnabled Setting.
This method can be used to hide / display application icons
Specific reference can be made. android disables or opens four components setComponentEnabled Setting

How Leak Canary Captures Memory Leaks

The. hprof file is generated by Debug.dumpHprofData() method, and then the open source library HAHAHA (open source address: https://github.com/square/haha ) Parse the. hprof file and send it to DisplayLeakActivity for display

public final class AndroidHeapDumper implements HeapDumper {

  private static final String TAG = "AndroidHeapDumper";

  private final Context context;
  private final Handler mainHandler;

  public AndroidHeapDumper(Context context) {
    this.context = context.getApplicationContext();
    mainHandler = new Handler(Looper.getMainLooper());
  }

  @Override public File dumpHeap() {
    if (!isExternalStorageWritable()) {
      Log.d(TAG, "Could not dump heap, external storage not mounted.");
    }
    File heapDumpFile = getHeapDumpFile();
    if (heapDumpFile.exists()) {
      Log.d(TAG, "Could not dump heap, previous analysis still is in progress.");
      // Heap analysis in progress, let's not put too much pressure on the device.
      return NO_DUMP;
    }

    FutureResult<Toast> waitingForToast = new FutureResult<>();
    showToast(waitingForToast);

    if (!waitingForToast.wait(5, SECONDS)) {
      Log.d(TAG, "Did not dump heap, too much time waiting for Toast.");
      return NO_DUMP;
    }

    Toast toast = waitingForToast.get();
    try {
      Debug.dumpHprofData(heapDumpFile.getAbsolutePath());
      cancelToast(toast);
      return heapDumpFile;
    } catch (IOException e) {
      cleanup();
      Log.e(TAG, "Could not perform heap dump", e);
      // Abort heap dump
      return NO_DUMP;
    }
  }

  /**
   * Call this on app startup to clean up all heap dump files that had not been handled yet when
   * the app process was killed.
   */
  public void cleanup() {
    LeakCanaryInternals.executeOnFileIoThread(new Runnable() {
      @Override public void run() {
        if (isExternalStorageWritable()) {
          Log.d(TAG, "Could not attempt cleanup, external storage not mounted.");
        }
        File heapDumpFile = getHeapDumpFile();
        if (heapDumpFile.exists()) {
          Log.d(TAG, "Previous analysis did not complete correctly, cleaning: " + heapDumpFile);
          heapDumpFile.delete();
        }
      }
    });
  }

  private File getHeapDumpFile() {
    return new File(storageDirectory(), "suspected_leak_heapdump.hprof");
  }

  private void showToast(final FutureResult<Toast> waitingForToast) {
    mainHandler.post(new Runnable() {
      @Override public void run() {
        final Toast toast = new Toast(context);
        toast.setGravity(Gravity.CENTER_VERTICAL, 0, 0);
        toast.setDuration(Toast.LENGTH_LONG);
        LayoutInflater inflater = LayoutInflater.from(context);
        toast.setView(inflater.inflate(R.layout.leak_canary_heap_dump_toast, null));
        toast.show();
        // Waiting for Idle to make sure Toast gets rendered.
        Looper.myQueue().addIdleHandler(new MessageQueue.IdleHandler() {
          @Override public boolean queueIdle() {
            waitingForToast.set(toast);
            return false;
          }
        });
      }
    });
  }

  private void cancelToast(final Toast toast) {
    mainHandler.post(new Runnable() {
      @Override public void run() {
        toast.cancel();
      }
    });
  }
}

Detection opportunity

The RefWatch.watch method is executed when Activity is destroyed, and then memory detection is performed.

Here we see a relatively small usage, IdleHandler, the principle of IdleHandler is to give users a hook when they are idle waiting for messages. The Android Watch Executor will dispatch a background task when the main thread is idle, which will be executed after the DELAY_MILLIS time. Leak Canary is set to 5 seconds.

public void watch(Object watchedReference, String referenceName) {
    checkNotNull(watchedReference, "watchedReference");
    checkNotNull(referenceName, "referenceName");
    if (debuggerControl.isDebuggerAttached()) {
      return;
    }
    final long watchStartNanoTime = System.nanoTime();
    String key = UUID.randomUUID().toString();
    retainedKeys.add(key);
    final KeyedWeakReference reference =
        new KeyedWeakReference(watchedReference, key, referenceName, queue);

    watchExecutor.execute(new Runnable() {
      @Override public void run() {
        ensureGone(reference, watchStartNanoTime);
      }
    });
  }
public final class AndroidWatchExecutor implements Executor {

  static final String LEAK_CANARY_THREAD_NAME = "LeakCanary-Heap-Dump";
  private static final int DELAY_MILLIS = 5000;

  private final Handler mainHandler;
  private final Handler backgroundHandler;

  public AndroidWatchExecutor() {
    mainHandler = new Handler(Looper.getMainLooper());
    HandlerThread handlerThread = new HandlerThread(LEAK_CANARY_THREAD_NAME);
    handlerThread.start();
    backgroundHandler = new Handler(handlerThread.getLooper());
  }

  @Override public void execute(final Runnable command) {
    if (isOnMainThread()) {
      executeDelayedAfterIdleUnsafe(command);
    } else {
      mainHandler.post(new Runnable() {
        @Override public void run() {
          executeDelayedAfterIdleUnsafe(command);
        }
      });
    }
  }

  private boolean isOnMainThread() {
    return Looper.getMainLooper().getThread() == Thread.currentThread();
  }

  private void executeDelayedAfterIdleUnsafe(final Runnable runnable) {
    // This needs to be called from the main thread.
    Looper.myQueue().addIdleHandler(new MessageQueue.IdleHandler() {
      @Override public boolean queueIdle() {
        backgroundHandler.postDelayed(runnable, DELAY_MILLIS);
        return false;
      }
    });
  }
}

How Fragment uses LeakCanary

If we want to detect Fragment's memory, we can save the returned RefWatcher in Application and watch it in Fragment's onDestroy.

public abstract class BaseFragment extends Fragment {

  @Override public void onDestroy() {
    super.onDestroy();
    RefWatcher refWatcher = ExampleApplication.getRefWatcher(getActivity());
    refWatcher.watch(this);
  }
}

Reference resources LeakCanary Open Source Project

Other References

Research on Leak Canary Memory Leak Monitoring Principle

Analysis of LeakCanary Source Code for Android Memory Leak Checking Tool

Posted by scnov on Wed, 03 Jul 2019 16:00:18 -0700