When we talk about mobile applications, performance is a fundamental concept. The low resource availability in these types of devices means that any inefficient solution, no matter how small, is reflected in poor user experience, whether through a non-responsive UI or excessive battery consumption.
As if that wasn’t bad enough, Android Vitals performance metrics are used for app ranking on Google Play searches: apps with healthy metrics tend to appear first while apps that have bad performance are shown last. You can find more details here.
The In Loco SDK is embedded into hundreds of applications. It means that many companies and developers trust us to allow our code to run in their apps. That’s a big responsibility, and that’s why performance is so important to us.
Application startup performance
The first impression the user has from an application is the time it takes to start. Apps that take too long to show something useful may frustrate users, leading to bad reviews and uninstalls. It’s no accident that startup time is one of the Android Vitals metrics. This is also a relevant metric to us because our SDK is initialized whenever the application is started. It’s defined as the time it takes to show an activity since the user triggered the action of opening the app.
The cold start
Depending on the state of the application when the startup process is initiated, some additional effort may be needed to make the app operational. The scenario that is more time consuming and causes the most overhead is a cold start. The application needs to be launched from scratch, which is the case when the app launches for the first time since the device was booted or since the system killed the app. This is the worst case scenario, and it is when the startup delay can be more noticeable.
In this scenario, the system first launches the app and shows a blank window until the startup process is completed. Then, it creates the main app process, which is responsible for creating the Application object and launching the first activity. When some parts of the app are already in memory, the startup process is simplified. These scenarios are known as hot/warm startups. For example, the app can be brought to the foreground if all of the activities are still in memory. In this case, the Application object has already been created, and the startup is faster.
The Application.onCreate() method
The system does most of the heavy lifting, but if you have overridden the Application.onCreate() method on your app, any code written here is going to be executed as part of the initialization process. It is a very handy place to write initialization logic since this method is called before any activity is created and thus before any user interaction. It is no coincidence that lots of libraries and SDKs recommend their initializations to be made in the Application.onCreate(). The result is that we end up with something like this:
The problem is that every single piece of code executed at this stage counts towards a bigger startup delay. The In Loco SDK also has to be initialized here, which explains why we are so obsessed with optimizing initialization time.
How much is too much?
Android Vitals considers a cold startup time to be excessively long when it takes five or more seconds to execute. In extreme scenarios, long-running operations that are part of the initialization logic may also cause ANRs. An ANR (Application Not Responding) event occurs when the UI thread is busy, and the app is unable to respond to user inputs.
You can check the startup time data for your app in the Android Vitals section of the Google Play Console:
The key to reducing startup time is to find the bottlenecks in the initialization process. Sometimes it is not trivial to find the culprit(s) just by looking at the code, but some patterns and tools may help you find them. Keep in mind that we are looking for synchronous operations that take a long time to execute, such as:
- Network operations
- Disk operations
- Heavy calculations
- Synchronization problems (deadlocks)
You should avoid doing these things synchronously on the main thread. Since the Application.onCreate() is executed on the main thread, any of those operations become initialization bottlenecks if not dispatched to the background.
The Android Profiler and the StrictMode are both tools that can help us to identify the sources of slowness. Something interesting about those tools is that they can show slow operations even on third-party code. This is especially useful when we suspect that one of those libraries that we are using is causing the issue.
Android Profiler is a powerful tool introduced in Android Studio 3.0 that replaces Android Monitor. It gives the developer a detailed view of CPU, memory, and network usage of the app.
It allows us to verify which parts of the code are adding extra time to the startup process. Using the method tracing, it is possible to see which methods are taking too long to execute and what they are doing. This is not only useful to find problems in our code, but also to determine if any 3rd party library is poorly optimized.
In the image above we have an example of what the method tracing in the Android Profiler can provide us. We can see that the Application.onCreate() of this sample application takes 2.92 seconds to execute. That’s under 5 seconds, but let’s try to understand what is happening. If we look closely, we’ll see that there are two library initializations calls in this method: RemoteConfigLibrary.init() and AnalyticsLibrary.init().
We can see that as part of its initialization process, RemoteConfigLibrary makes a synchronous network request. We were lucky that the initialization of this library only took 1.17s because depending on network conditions this request could have taken much longer! This is not good!
We can also see that the AnalyticsLibrary writes to the SharedPreferences as part of its initialization process. This is usually not a problem if the SharedPreferences is used for a small amount of data. This does not seem to be the case, and this is taking almost half the time of the method call.
Cool, isn’t it? The Android Profiler gives us the power to look under the hood to find hidden performance issues!
StrictMode is another tool that helps us find hidden performance problems. It works by warning the developer of undesired patterns such as network and disk operations on the main thread.
The app used in the previous example crashes when running with the settings above.
Let’s check Logcat to find out the cause:
We can see that the app crashes intentionally because RemoteConfigLibrary.init() makes a network call on the main thread. In this example, we configured StrictMode to crash our app if any anti-pattern is found, but there are less severe options such as warning dialogs and logs. You can find the full StrictMode documentation and options here.
Fixing the issues
Discovering the source of the problem is a good start, but we need to fix the issues. There are a few things that you can do to offload the initialization:
- Move the blocking code to a background thread: network and disk operations should not be executed synchronously on the main thread, especially during the application startup.
- Use lazy initialization: parts of your application don’t need to be initialized during startup. Try to identify those parts and delay the initialization as much as possible — e.g., the user profile might only be necessary on the profile activity and not very useful during startup.
- Dispatch 3rd party libraries initialization to a background thread: lots of library initializations on the Application.onCreate() will undoubtedly make it slower. Try to find the libraries that can be asynchronously initialized and move their initialization to a background thread.
- Remove defective 3rd party libraries: this is an extreme scenario, but if a specific library is too slow to be initialized and there is no possibility to initialize it elsewhere, you should consider removing it or replacing it with something better.
- Solve synchronization issues: The startup time might be increased if the main thread is blocked waiting for another operation executed on another thread. Try to find those locks.
- Avoid reflection: Reflection operations are known to be slow. Try to avoid a large number of them as part of the initialization. We were able to considerably improve the startup time of our SDK only by avoiding reflection. Reflection operations are very common in serialization libraries like Gson and Jackson.
Other things can affect how long your app takes to start, but fixing those common issues is certainly going to give your app a speed boost. Your users will be happier and more engaged while you enjoy all those 5-star reviews!
Are you interested?
If you are interested in building context-aware products through location, check out our opportunities. Also, we’d love to hear from you! Leave a comment and let us know what you would like us to talk about in the upcoming posts.