Once we have established metrics and scenarios that trigger slow application startup, the next step is to improve performance.

To understand what is causing the application to start slowly, we need to analyze it. Android Studio provides several types of profiler recording configurations:

Trace System Calls (aka Systrace, perfetto) : Has little impact on the runtime and is very helpful in understanding how the application interacts with the System and CPU, but not the Java method Calls that occur inside the application VM.

Sample C/C++ Functions (aka Simpleperf) : I’m not interested, I’m dealing with applications that run much more bytecode than native code. On Q+, this should now also sample the Java stack in a low overhead way, but I haven’t managed to get it to work yet.

Trace Java Methods: This captures all the VM method calls that introduce so much overhead that the results don’t make much sense.

Sample Java Methods: Less expensive than tracing, but shows the Java method calls that occur within the VM. This is my preferred option when analyzing application startup.

Start recording when the application starts

Android Studio Profiler has a UI to start tracing by connecting to an already running process, but there is no obvious way to start recording when the application starts.

This option exists but is hidden in the run Configuration of the application: Start this record when startup is selected on the Profiling TAB.

The application is then deployed using the Run > Profile app.

Analysis the release builds

Android developers often use the debug build type in their daily work, and debug builds often include additional libraries such as LeakCanary.

Developers should analyze releases rather than debug releases to ensure that they are solving real problems faced by customers.

Unfortunately, releases are not debugable, so the Android profiler can’t keep track of releases.

Here are a few options for solving the problem.

1. Create a debuggable release

We can temporarily make our release build debugable, or create a new release build type for analysis.

android { buildTypes { release { debuggable true // ... }}}Copy the code

If APK is debuggable, libraries and Android framework code will often behave differently. ART disables many optimizations to enable the connection debugger, which can significantly and unpredictably affect performance. Therefore, this solution is not ideal.

2. Debug the device on the root device

The Root device allows the Android Studio profiler to record traces of undebugable builds.

Analysis on emulators is generally not recommended – each system component has different performance (CPU speed, cache size, disk performance), so “tuning” can actually slow things down by shifting work to slower things on the phone. If you don’t have a root physical device available, you can create an emulator without the Play service and run ADB root.

3. Use Simpleperf on Android Q

A tool called Simpleperf is said to enable analysis build on non-root Q+ devices if they have a special manifest flag. The document calls this profileableFromShell, and the XML sample has a profileable tag with the Android :shell attribute; the official manifest document shows nothing.

<manifest ... > <application ... > <profileable android:shell="true" /> </application> </manifest>Copy the code

I looked at the listing parsing code at cs.android.com:

if (tagName.equals("profileable")) { sa = res.obtainAttributes( parser, R.styleable.AndroidManifestProfileable ); if (sa.getBoolean( R.styleable.AndroidManifestProfileable_shell, false )) { ai.privateFlags |= ApplicationInfo.PRIVATE_FLAG_PROFILEABLE_BY_SHELL; }}Copy the code

If the list has (WHICH I haven’t tried), you seem to be able to trigger analysis from the command line. As FAR as I know, the Android Studio team is still working on integrating with this new feature.

Analyze the downloaded APK

At Square, our version is built with CI. As we’ve seen before, launching from the Android Studio analytics application requires checking an option in the run configuration. How do we do this using the downloaded APK?

Turns out this is possible, but hidden under File > Profile or Debug APK. This opens a new window containing the unzipped APK from which you can set up to run the configuration and begin the analysis.

The Android Studio profiler slows everything down

Unfortunately, when I tested these methods on a production application, even on recent versions of Android, Android Studio’s analysis slowed the application’s startup considerably (by about 10 times). I don’t know why, maybe it’s “advanced analytics”, it can’t seem to be disabled. We need to find a new way!

Analysis from code

Instead of analyzing from Android Studio, we can trace directly from the code:

val tracesDirPath = TODO("path for trace directory") val fileNameFormat = SimpleDateFormat( "yyyy-MM-dd_HH-mm-ss_SSS'.trace'", Locale.US ) val fileName = fileNameFormat.format(Date()) val traceFilePath = tracesDirPath + fileName // Save up to 50Mb  data. val maxBufferSize = 50 * 1000 * 1000 // Sample every 1000 microsecond (1ms) val samplingIntervalUs = 1000 Debug.startMethodTracingSampling( traceFilePath, maxBufferSize, samplingIntervalUs ) // ... Debug.stopMethodTracing()Copy the code

We can then extract the trace file from the device and load it into Android Studio.

When do you start sampling

We should start recording traces early in the application life cycle. The first code to run at application startup prior to Android P is ContentProvider, which on Android P+ is AppComponentFactory.

Android P / API < 28

class AppStartListener : ContentProvider() {
  override fun onCreate(): Boolean {
    Debug.startMethodTracingSampling(...)
    return false
  }
  // ...
}
Copy the code
<? The XML version = "1.0" encoding = "utf-8"? > <manifest xmlns:android="http://schemas.android.com/apk/res/android" xmlns:tools="http://schemas.android.com/tools"> <application> <provider android:name=".AppStartListener" android:authorities="com.example.appstartlistener" android:exported="false" /> </application> </manifest>Copy the code

When defining the provider, we can set an initOrder flag, and the highest number is initialized first.

Android P+ / API 28+

@RequiresApi(28)
class MyAppComponentFactory() :
  androidx.core.app.AppComponentFactory() {

  @RequiresApi(29)
  override fun instantiateClassLoader(
    cl: ClassLoader,
    aInfo: ApplicationInfo
  ): ClassLoader {
    if  (Build.VERSION.SDK_INT >= 29) {
      Debug.startMethodTracingSampling(...)
    }
    return super.instantiateClassLoader(cl, aInfo)
  }

  override fun instantiateApplicationCompat(
    cl: ClassLoader,
    className: String
  ): Application {
    if  (Build.VERSION.SDK_INT < 29) {
      Debug.startMethodTracingSampling(...)
    }
    return super.instantiateApplicationCompat(cl, className)
  }
}
Copy the code
<? The XML version = "1.0" encoding = "utf-8"? > <manifest xmlns:android="http://schemas.android.com/apk/res/android" xmlns:tools="http://schemas.android.com/tools"> <application android:appComponentFactory=".MyAppComponentFactory" tools:replace="android:appComponentFactory" tools:targetApi="p"> </application> </manifest>Copy the code

Where do I store samples

val tracesDirPath = TODO("path for trace directory")
Copy the code
  • API < 28: The broadcast receiver has access to the Context on which we can call context.getDatadir () to store the trace in the application directory.

  • API 28: AppComponentFactory instantiateApplication () is responsible for creating a new application instance, so there is no available context. We can hardcode the path to /sdcard/ directly, but this requires the WRITE_EXTERNAL_STORAGE permission.

  • API 29+ : Hardcoding /sdcard/ stops working when facing API 29. We can add requestLegacyExternalStorage sign, but no matter how the API 30 don’t support it. You are advised to try MANAGE_EXTERNAL_STORAGE on API 30+. Either way, AppComponentFactory instantiateClassLoader will pass a ApplicationInfo (), So we can use applicationInfo.datadir to store traces in the application directory.

When to stop sampling

The cold start ends when the first frame of the application is fully loaded. We can stop method tracing based on this condition:

class MyApp : Application() {

  override fun onCreate() {
    super.onCreate()

    var firstDraw = false
    val handler = Handler()

    registerActivityLifecycleCallbacks(
      object : ActivityLifecycleCallbacks {
      override fun onActivityCreated(
        activity: Activity,
        savedInstanceState: Bundle?
      ) {
        if (firstDraw) return
        val window = activity.window
        window.onDecorViewReady {
          window.decorView.onNextDraw {
            if (firstDraw) return
            firstDraw = true
            handler.postAtFrontOfQueue {
              Debug.stopMethodTracing()
            }
          }
        }
      }
    })
  }
}
Copy the code

We can also record a fixed time that is longer than the application startup time, such as 5 seconds:

Handler(Looper.getMainLooper()).postDelayed({
  Debug.stopMethodTracing()
}, 5000)
Copy the code

Use Nanoscope for analysis

Another option for analyzing application launches is Uber/Nanoscope. This is an Android image with built-in low overhead tracking. It’s great, but with some limitations:

It only tracks the main thread.

Large applications will overflow the memory trace buffer.

Application Startup Procedure

Once we have startup tracking, we can start investigating what actions are consuming time. There are 3 main parts to look forward to:

ActivityThread. HandlingBindApplication () contains the Activity created before starting work. If this is slow, then we might need to optimize application.oncreate ().

TransactionExecutor. The execute () is responsible for the creation and restore the Activity, including filling the view hierarchy.

ViewRootImpl. PerformTraversals () is the first time frame to perform measurement, layout and drawing. If this is slow, it could be a view hierarchy that is too complex, or a view that has custom drawing that needs to be optimized.

If you notice that the service is started before the first view walk, it may be worth delaying the start of the service so that it happens after the view walk.

conclusion

Some highlights:

Analyzing releases focuses on practical issues.

The startup state of analytics applications on Android is far from ideal. There’s basically no good out-of-the-box solution, but the Jetpack Benchmark team is working on it.

Start sampling from code to prevent Android Studio from slowing everything down.