Did you ever encounter a use case, where you put heavy initialisations/calls at the time of launching a screen but the initialisation is not critical for showing the screen to the user but, we can not wait for on demand initialisations also. We end up adding that code to the android’s lifecycle callbacks. In this article we will try to find out a solution to this and discuss the pros and cons to the approach.

Improve android app/screen launch times using LazyLifecycle callbacks.

Want to improve screen launch times of your android app lazily? Lazy lifecycle callbacks can help you out there.

Vishal Ratna
Microsoft Mobile Engineering

--

Photo by Ilona Celeste on Unsplash

Lazy loading is one of the suggested philosophies in android. Learn how Microsoft Teams did some of the lazy optimisations.

Screen is considered as rendered only when the activity is in resumed state and all upward lifecycle callbacks have returned. In the documentation, it says that the screen is considered rendered when the first draw is finished on the screen. There is something like reportFullyDrawn() but we will not talk about it in this post.

If we put any thing expensive in OnCreate, onStart, onResume that could potentially eat CPU cycles before the screen is rendered and may be we do not want that in bigger apps, that have lots of things to initialise and eating CPU cycles could be a cause of high impact performance issues.

credits: https://developer.android.com/guide/components/activities/activity-lifecycle

Some of the example of expensive(required) but not critical(for screen rendering) kind of calls could be,

  1. Initialising screen specific libraries/managers.
  2. Making telemetry calls.
  3. Prefetches from db or n/w.
  4. Calls to download files that are not required at the time of first screen render but are eventually required by the app.
  5. Warming up RN engines, web views.
  6. Refreshing auth tokens.

Confused?

Let me give a concrete example. You can plugin a RN app or a web app with MSTEAMS, for RN first time launches the VM needs to get warmed up(and some part of it needs to happen on the main thread), if the warm up is there in lifecycle callbacks, it would eat the main thread and impact screen launch times. Similarly, you need to sync with servers for conversations, every time you come from background to foreground and start some services. Though this happens on a worker thread but still it competes for the same CPU. We want something that would make sure, while the rendering is finishing, we do not do anything that is not required.

We did consider, worker manager but, below are the reasons why we did not use it.

Why not WorkManager? 1. WorkerManager is a construct that creates a service internally that is expensive. Our use case was not that of a service clearly. We did not want any threading construct that moves the work in the background thread. We wanted to delay calls that are not required for the 1st rendering of the screen.2. Worker manager never guarantees that your work will get executed, it is done on best attempt basis, it depends on the app standby bucket your app is in, and other constraints too.3. Creating worker would move out code from the activity that we would not want, when you are dealing with code that was once a part of android lifecycle methods. It will look illogical and difficult to pursue the code.4. Imagine each activity and fragment creating their own worker. And there is code in each lifecycle callback like onStart(), onResume(), workers won't fit in that model where, we need to fire lazy versions of callbacks even after we move from foreground to background.We are around 450+ developers and android lifecycle callbacks quickly get polluted with lots of code and it all starts adding up.Lazy callbacks ensure, that you show the screen with cached data and get into eventual consistency.

LazyLifecycleCallbacks was born with all these needs in mind…..

Photo by Jonathan Borba on Unsplash

The idea was simple, what if we build a setup that could fire a piece of code after the rendering of the app has finished( We could leverage onPreDrawListener for this and wait for couple of draws on the screen ).

But there were issues, we were dealing with legacy code, where a lot of code was already there in the activity/fragment lifecycle callbacks. So if we defer the onResume code, it should fire every time the app resumes but after the rendering has finished. Code in onCreate should fire only once in the activity lifetime. We had to maintain the contracts of each lazy lifecycle callback.

We created 3 callbacks

  1. onLazyCreate — fires after screen rendering has finished once per activity instance.
  2. onLazyStart— fires after screen rendering has finished, multiple times when activity starts.
  3. onLazyResume — fires after screen rendering has finished, multiple times when activity resumes.
LazyLifecycleCallbacks.kt

We have a watched view in the contract. As I explained, we will depend on draws on the screen. We need a view to install listeners on that. You can chose a view that is critical for a screen or decor view can be used in case you do not have any.

So, the idea was to make the activities/fragments that implement this interface be able to onboard this. And some of the screens were very heavy in terms of layout hierarchy. Profiler showed that couple of draws were not enough to fully render a meaningful screen, when we had screenful of data. So we needed to create a mechanism where we can set the number of draws(N) we should wait before we fire the callbacks. And a backup plan, if N draws do not happen(kind of an override deadline). That will fire the code anyhow if the N draws did not happen.

This required serious state management and was prone to error as you have to handle multiple states as demonstrated below. Code can potentially fire from 2 paths but should fire only once ie. If the N draws have finished before SLA, the code should fire due to condition becoming true and SLA expiration should be a no-op. Or if, SLA expired before the condition becoming true then the code should not execute if the condition becomes true.

  1. Condition becoming true ( draws == N) // Path A
  2. SLA breached // Path B
Trigger for lazy lifecycle callbacks

To handle all the state management out of the box, and we can build a scaffold that works for any condition(that we provide), we came up with a concept Barrier that does all these things, handles concurrency for us and is properly unit tested.

Barrier — It is a construct that could prevent execution of a piece of code( defined in runOnBreak) till condition becomes true or the SLA is breached. Timer for SLA starts when we call startSLA() on the barrier.

To install a barrier, in the path where the condition needs to get evaluated we use barrier.strike(). In our setup, we will use strike() call in the pre-draw listener, every time the strike is executed the condition is evaluated, if it is true then the code is fired.

Barrier implementation —

Using barrier we can build there complex constructs easily with one fluent API.

Barrier b = Barrier.with(new Barrier.Condition() {
@Override
public boolean evaluate() {
return false;
}
}).withSLA(2000, true)
.runOnBreak(() -> {
// Some code that runs on condition satisfied
}).startSLA();

So, now we have all the pieces ready. We can build the base class for LazyLifeycycleManager that will take care of firing lazy lifecycle callbacks. The SLA chosen should be wise enough. We took the p95 of our app launch times and used that. So that it fires after the worst case launches. This base class can be used to build multiple managers based on different triggers. I will provide an implementation of view-based trigger.

LazyLifecycleManager.kt

Now we create a view based implementation of base manager.

ViewBasedLazyLifecycleManager.kt

Once we have all the classes ready, to use the small framework that we just build, we have to call activate() in the onResume of the activity/fragment and lazylifecyclecallbacks will take care of everything else.

class Activity extends SomeBaseActivity implements LazyLifecycleCallbacks {
private LazyLifecycleManager mLazyLifecycleManager;
@Override public void onCreate(Bundle savedInstanceState) {
LazyLifecycleManagerFactory factory = new LazyLifecycleManagerFactory(); // In actual project it will be a singleton.
mLazyLifecycleManager = factory.get(this,new LazyLifecycleManagerType.ViewBased(5,3000));}@Override public void onResume() {
mLazyLifecycleManager.activate();
}
@Override public void onLazyResume() {
// Some deferrable job that needs to run on every resume
}
@Override public void onLazyStart() {
// Some deferrable job that needs to run on every start
}
@Override public void onLazyCreate() {
// Some deferrable job that needs to run on every create
}
}

Once all the setup is done, this mechanism could be used for all the Activities and with a little tweak in the code of LifecycleManager( the condition where we check for activity instance), could also be leveraged for fragments.

Results were amazing, where we removed lots of work from the app launch path and gained some app launch points.

Apart from play store console we have internal scorecard mechanism that measures the improvements in terms of %. The P95 for warm improved by 24% and for cold it was was ~12%.

Caveats —
1. Do not defer everything at the same time, or else all the lazy callbacks for different activities could be executing at the same time and compete.
2. This is not the replacement of optimisation, if something is taking time it should be analysed and solved. Deferring would just defer the problem. This is meant for things that are time consuming by nature but are required and we cannot make it on demand.

Post Read — Once you take the code from the Gist, it will look for a class Once. This is to assure that a code is executed only once in a thread safe manner.

Find out full git project with lazy lifecycle callbacks below,
https://github.com/microsoft/LazylifecycleCallbacks/tree/main/LazylifecycleDemo

If you want to see an interesting construct that is at one level higher level of abstraction than barrier, then you can take a look at MultiTriggerBomb.

https://betterprogramming.pub/how-to-prevent-code-execution-till-all-triggers-are-down-or-timer-is-expired-989248849392

Using this we can notify when multiple code paths, callbacks are finished and fire a piece of code.

Signing off.

--

--

Vishal Ratna
Microsoft Mobile Engineering

Senior Android Engineer @ Microsoft. Fascinated by the scale and performance-based software design. A seeker, a learner. Loves cracking algorithmic problems.