Header image: A Song of Ice and Fire by Romain Guy.
This blog series is focused on stability and performance monitoring of Android apps in production. Last week, I wrote about measuring time in Android Vitals - What time is it?
Over the next blog posts, I will explore how to monitor cold start. According to the App startup time documentation:
A cold start refers to an app's starting from scratch: the system's process has not, until this start, created the app's process. Cold starts happen in cases such as your app's being launched for the first time since the device booted, or since the system killed the app.
At the beginning of a cold start, the system has 3 tasks:
- Loading and launching the app.
- Displaying a starting window.
- Creating the app process.
This post is a deep dive on the beginning of a cold start, from tapping a launcher icon to the creation of the app process.
Diagram created with WebSequenceDiagram.
Activity.startActivity()
When a user taps a launcher icon, the launcher app process calls Activity.startActivity(), which delegates to Instrumentation.execStartActivity():
public class Instrumentation {
public ActivityResult execStartActivity(...) {
...
ActivityTaskManager.getService()
.startActivity(...);
}
}
The launcher app process then makes an IPC call to ActivityTaskManagerService.startActivity() in the system_server
process. The system_server
process hosts most system services.
Staring at the starting window 👀
Before creating a new app process, the system_server
process creates a starting window via PhoneWindowManager.addSplashScreen():
public class PhoneWindowManager implements WindowManagerPolicy {
public StartingSurface addSplashScreen(...) {
...
PhoneWindow win = new PhoneWindow(context);
win.setIsStartingWindow(true);
win.setType(TYPE_APPLICATION_STARTING);
win.setTitle(label);
win.setDefaultIcon(icon);
win.setDefaultLogo(logo);
win.setLayout(MATCH_PARENT, MATCH_PARENT);
addSplashscreenContent(win, context);
WindowManager wm = (WindowManager) context.getSystemService(
WINDOW_SERVICE
);
View view = win.getDecorView();
wm.addView(view, params);
...
}
private void addSplashscreenContent(PhoneWindow win,
Context ctx) {
TypedArray a = ctx.obtainStyledAttributes(R.styleable.Window);
int resId = a.getResourceId(
R.styleable.Window_windowSplashscreenContent,
0
);
a.recycle();
Drawable drawable = ctx.getDrawable(resId);
View v = new View(ctx);
v.setBackground(drawable);
win.setContentView(v);
}
}
The starting window is what a user sees while the app process starts, until it creates its activity and draws its first frame, i.e. until cold start is done. The user could be staring at the starting window for a long time, so make sure it looks good 😎.
The starting window content is loaded from the started activity's windowSplashscreenContent and windowBackground drawables. To learn more, check out Android App Launching Made Gorgeous.
If the user brings back an activity from the Recents screen instead of tapping a launcher icon, the system_server
process calls TaskSnapshotSurface.create() to create a starting window that draws a saved snapshot of the activity.
Once the starting window is displayed, the system_server
process is ready to start the app process and calls ZygoteProcess.startViaZygote():
public class ZygoteProcess {
private Process.ProcessStartResult startViaZygote(...) {
ArrayList<String> argsForZygote = new ArrayList<>();
argsForZygote.add("--runtime-args");
argsForZygote.add("--setuid=" + uid);
argsForZygote.add("--setgid=" + gid);
argsForZygote.add("--runtime-flags=" + runtimeFlags);
...
return zygoteSendArgsAndGetResult(openZygoteSocketIfNeeded(abi),
zygotePolicyFlags,
argsForZygote);
}
}
ZygoteProcess.zygoteSendArgsAndGetResult() sends the starting arguments over a socket to the Zygote process.
Forking Zygote 🍴
According to the Android documentation on memory management:
Each app process is forked from an existing process called Zygote. The Zygote process starts when the system boots and loads common framework code and resources (such as activity themes). To start a new app process, the system forks the Zygote process then loads and runs the app's code in the new process. This approach allows most of the RAM pages allocated for framework code and resources to be shared across all app processes.
When the system boots, the Zygote process starts and invokes ZygoteInit.main():
public class ZygoteInit {
public static void main(String argv[]) {
...
if (!enableLazyPreload) {
preload(bootTimingsTraceLog);
}
// The select loop returns early in the child process after
// a fork and loops forever in the zygote.
caller = zygoteServer.runSelectLoop(abiList);
// We're in the child process and have exited the
// select loop. Proceed to execute the command.
if (caller != null) {
caller.run();
}
}
static void preload(TimingsTraceLog bootTimingsTraceLog) {
preloadClasses();
cacheNonBootClasspathClassLoaders();
preloadResources();
nativePreloadAppProcessHALs();
maybePreloadGraphicsDriver();
preloadSharedLibraries();
preloadTextResources();
WebViewFactory.prepareWebViewInZygote();
warmUpJcaProviders();
}
}
As you can see, ZygoteInit.main() does 2 important things:
- It preloads the Android framework classes & resources, shared libraries, graphic drivers, etc. This preloading doesn't just save memory, it also improves startup time.
- Then it calls ZygoteServer.runSelectLoop() which opens a socket and waits.
When a forking command is received on that socket, ZygoteConnection.processOneCommand() parses the arguments via ZygoteArguments.parseArgs() and calls Zygote.forkAndSpecialize():
public final class Zygote {
public static int forkAndSpecialize(...) {
ZygoteHooks.preFork();
int pid = nativeForkAndSpecialize(...);
// Set the Java Language thread priority to the default value.
Thread.currentThread().setPriority(Thread.NORM_PRIORITY);
ZygoteHooks.postForkCommon();
return pid;
}
}
Note: Android 10 added support for an optimization called Unspecialized App Process (USAP), a pool of forked Zygotes waiting to be specialized. Slightly faster startup at the cost of extra memory (turned off by default). Android 11 shipped with IORap which gives much better results.
An App is Born ✨
Once forked, the child app process runs RuntimeInit.commonInit()
which installs the default UncaughtExceptionHandler. Then the app process runs ActivityThread.main():
public final class ActivityThread {
public static void main(String[] args) {
Looper.prepareMainLooper();
ActivityThread thread = new ActivityThread();
thread.attach(false, startSeq);
Looper.loop();
}
final ApplicationThread mAppThread = new ApplicationThread();
private void attach(boolean system, long startSeq) {
if (!system) {
IActivityManager mgr = ActivityManager.getService();
mgr.attachApplication(mAppThread, startSeq);
}
}
}
There are two interesting parts here:
- ActivityThread.main() calls Looper.loop() which loops forever, waiting for new messages to be posted to its MessageQueue.
-
ActivityThread.attach() makes an IPC call to ActivityManagerService.attachApplication() in the
system_server
process to let it know that the app's main thread is ready to go 🚀.
App Puppeteering
In the system_server
process, ActivityManagerService.attachApplication() calls ActivityManagerService.attachApplicationLocked() which finishes setting up the application:
public class ActivityManagerService extends IActivityManager.Stub {
private boolean attachApplicationLocked(
IApplicationThread thread, int pid, int callingUid,
long startSeq) {
thread.bindApplication(...);
// See if the top visible activity is waiting to run
// in this process...
mAtmInternal.attachApplication(...);
// Find any services that should be running in this process...
mServices.attachApplicationLocked(app, processName);
// Check if a next-broadcast receiver is in this process...
if (isPendingBroadcastProcessLocked(pid)) {
sendPendingBroadcastsLocked(app);
}
return true;
}
}
A few key takeaways:
- The
system_server
process makes an IPC call to ActivityThread.bindApplication() in the app process, which schedules a call to ActivityThread.handleBindApplication() on the application main thread. - Immediately after, the
system_server
process schedules the start of any pending activity, service, and broadcast receiver. -
ActivityThread.handleBindApplication() loads the APK and loads the app components in the following order:
- Load the app AppComponentFactory subclass and create an instance.
- Call AppComponentFactory.instantiateClassLoader().
- Call AppComponentFactory.instantiateApplication() to load the app Application subclass and create an instance.
- For each declared ContentProvider, in priority order, call AppComponentFactory.instantiateProvider() to load its class and create an instance, then call ContentProvider.onCreate().
- Call Application.onCreate().
- App developers have little influence on the time spent before ActivityThread.handleBindApplication(), so that's where app cold start monitoring should start.
Early initialization
If you need to run code as early as possible, you have several options:
- The earliest hook is when the AppComponentFactory class is loaded.
- Add the appComponentFactory attribute to the application tag in
AndroidManifest.xml
. - If you use AndroidX, you need to add
tools:replace="android:appComponentFactory"
and delegate calls to the AndroidX AppComponentFactory - You can add a static initializer there and do things like storing a timestamp.
- Downsides: this is only available in Android P+, and you won't have access to a context.
- Add the appComponentFactory attribute to the application tag in
- A safe early hook for app developers is Application.onCreate().
- A safe early hook for library developers is ContentProvider.onCreate(). This trick was popularized by Doug Stevenson in How does Firebase initialize on Android?
- There's a new AndroidX App Startup library which relies on the same provider trick. The goal is to have just one provider declared instead of many, because each declared provider slows the app start by a few milliseconds and increases the size of the ApplicationInfo object from package manager.
Conclusion
We started with a high level understanding of how cold start begins:
Now we know exactly what happens:
The user experience of launching an activity starts when the user touches the screen, however app developers have little influence on the time spent before ActivityThread.handleBindApplication()
, so that's where app cold start monitoring should start.
That was a long post, and we're far from being done with cold start. Stay tuned for more!
Top comments (8)
Great article and thanks for the series! I'm wondering why Application subclass constructors are not one of the options for early hook. It's called earlier than Application.onCreate() or ContentProvider.onCreate(). Is it because it's not safe?
Great question! In the Application subclass constructor the base context of the Application instance isn't set up. That means you can't call any of the context related methods, e.g. access the app file system, etc. Probably the same level of safety as doing work on classloading, etc.
Thanks for reply! Does that mean storing a timestamp in memory there is ok-ish?
Yep! I'll cover that topic in more details in a follow up post ;)
Nice!
Thanks for the article! Is there any chance to implement dynamic change of theme-based things? Like android:windowBackground . Looks like it's loaded from manifest and used for Preview window (starting and switches between activity stacks). So programmatical changes like setTheme() (or changing background of getWindow()) in Activity or Application doesn't affect it. It makes trouble when the app should use custom themes with different colors, but android:windowBackground is same for all of them.
The theme of whichever activity is launched will be used, and that's the only way you can somewhat customize the launching window style, since it runs before any of your code runs.
Great article, Thanks for your effort