Sunday, 21 February 2016

Android App Profiling and Optimization - part 2

In the previous blog post, I talked about memory leaks, or more specifically, leaked Activity. We went through the process of how to identify the leak and what you should not do in order to avoid it. Finally, I showed you how to fix the issue, resulting in an improved performance and stability of your app.

In this blog post, I'm going to talk about an amazing open source Android library called LeakCanary by Square, which some even define it as a "life-saver" (me among them). Simply put, LeakCanary finds memory leaks in your app during runtime and provides detailed info of where this leak might occur. The info is provided in two forms, LeakCanary UI and a more detailed leak trace printed in LogCat.

LeakCanary App showing the source of the leak

Considering the code example given in part 1 where we observed a memory leak, let's see how we can detect the memory leak using LeakCanary.

Integrating LeakCanary


In your build.gradle:

dependencies {
   debugCompile 'com.squareup.leakcanary:leakcanary-android:1.4-beta1'
   releaseCompile 'com.squareup.leakcanary:leakcanary-android-no-op:1.4-beta1'
   testCompile 'com.squareup.leakcanary:leakcanary-android-no-op:1.4-beta1'
 }

You probably noticed that there are two types of dependencies. The one ending with no-op basically contains only method stubs, without any concrete implementation, so we don't need to worry about LeakCanary showing memory leak alerts in our release builds.

In your Application class:

public class App extends Application {
  @Override public void onCreate() {
    super.onCreate();
    LeakCanary.install(this);
  }
}

In case you didn't have your own Application class implementation, remember to also register it in AndroidManifest.xml:

<manifest xmlns:android="http://schemas.android.com/apk/res/android"
    package="org.codepond.memoryleak" >
    <application
        android:allowBackup="true"
        android:icon="@mipmap/ic_launcher"
        android:label="@string/app_name"
        android:name=".App"
        android:theme="@style/AppTheme" >

LeakCanary is now integrated and will automatically catch any Activity instance that leaked memory as long as we're in debug mode. It is of course possible to configure LeakCanary to watch any object. For more info, please see LeakCanary readme.

LeakCanary in Action


Fire up the app and change the orientation, then wait a few seconds. If you're targeting Android API 23, you'll get a notification from LeakCanary, saying that it needs permission to external storage. At the same time you'll also see the following log message in LogCat: "Could not write to leak storage to dump heap." Once you enabled the storage permission, re-launch the app and repeat the same step to produce a memory leak.
LeakCanary notification asking for storage permission

An alert will be shown in the middle of the screen "Dumping memory, app will freeze" and at the same time you'll see a log message in LogCat: "hprof: heap dump/sdcard/Download/leakcanary/suspected_leak_heapdump.hprof" starting...".




This means that LeakCanary caught a memory leak and will now do a heap dump, which will block the UI for a few seconds. The heap dump is stored in the emulator/device's external storage.

Once it's done, the alert will disappear and you'll see a message in LogCat similar to: "hprof: heap dump completed (25MB) in 3.848s". It's not over yet, LeakCanary will now analyze the heap dump in a background thread and within a few seconds you'll get a notification in addition to a trace print in LogCat:

In org.codepond.memoryleak:1.0:1.
* org.codepond.memoryleak.Splash has leaked:
* GC ROOT android.os.MessageQueue.mMessages
* references android.os.Message.callback
* references org.codepond.memoryleak.Splash$1.this$0 (anonymous class implements java.lang.Runnable)
* leaks org.codepond.memoryleak.Splash instance
* Retaining: 1.8 KB.
* Reference Key: 086cbbfa-f610-438a-b28f-750aa0135d2d
* Device: LGE google Nexus 5 hammerhead
* Android Version: 6.0.1 API: 23 LeakCanary: 1.4-beta1 02804f3


First, let's check the log message. The most important part is between lines 2-7. Line 2 states which class has leaked and line 7 states what it leaked. In this case Splash Activity, or to be more precisely - the anonymous class that implements Runnable in the call to postDelayed(), has leaked Splash Activity instance. Just a quick recap of part 1, when we changed the orientation, Splash Activity was essentially destroyed and a new instance was created. The old instance was supposed to be garbage collected, but it didn't, since it was still referenced from the Handler's Runnable (which was set to be executed after a 5 seconds delay).

new Handler().postDelayed(new Runnable() {
   @Override
   public void run() {
      /* This block leaks Splash instance */
      Intent mainIntent = new Intent(Splash.this, MainActivity.class);
      Splash.this.startActivity(mainIntent);
      Splash.this.finish();
  }
}, SPLASH_DISPLAY_LENGTH); // 5 seconds delay
The code snippet containing the memory leak


Now let's check the notification. After tapping it, LeakCanary will show tree-like UI with the most important part of the trace print that we've just went through.



Note that LeakCanary doesn't delete the heap dumps it stores in the external storage. You'll have to delete them manually by either tapping the DELETE button in the UI or directly access the SDCARD and delete the files from there, though I've personally always done the former so I'm unsure how the latter will affect LeakCanary's UI. If you don't delete previously stored heap dumps, at some point, LeakCanary will reach its maximum capacity and won't do any more heap dumps until the old ones have been deleted.

Check out the previous blog post on how to fix this memory leak.

Summary


LeakCanary saved me a lot of headache before and nowadays is my preferred way of tracking down memory leaks. Memory leaks in Android come in different flavors. In most cases they occur when the developer isn't careful enough. The common cases for memory leaks are a registered listener that should have been unregistered in Activity onDestroy(), a close() method that wasn't called, an anonymous class that holds a reference to an outer class, as seen in the example presented in this blog post, and more. Square has published a more advanced blog post about memory leaks, which I highly recommend you to read once you get the hang of memory leaks in Android.

Now go hunt those buggers!