深入Android冷启动

1,015 阅读5分钟

如何监控冷启动

根据App启动时间文档:

冷启动是指应用程序从头开始:系统进程在此启动之前还没有创建应用程序进程。 冷启动发生在您的应用程序自设备启动后第一次启动或系统终止应用程序后再次启动的情况。

在冷启动开始时,系统有 3 个任务:

  1. 加载和启动应用程序。

  2. 显示启动窗口。

  3. 创建应用程序进程。

这篇文章深入探讨了冷启动的开始,从点击启动器图标到创建应用程序进程。

Activity.startActivity()

当用户点击桌面APP图标时,启动器应用进程调用 Activity.startActivity(),后者委托给 Instrumentation.execStartActivity():

public class Instrumentation {

  public ActivityResult execStartActivity(...) {
    ...
    ActivityTaskManager.getService()
        .startActivity(...);
  }
}

然后,启动器应用进程在 system_server 进程中对 ActivityTaskManagerService.startActivity() 进行 IPC 调用。 system_server 进程承载大多数系统服务。

盯着启动窗口

在创建新的应用进程之前,system_server 进程通过 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);
  }
}

启动窗口是用户在应用进程启动时看到的,直到它创建其活动并绘制其第一帧,即直到完成冷启动。 用户可能会长时间盯着启动窗口,所以请确保它看起来不错。

起始窗口内容是从已启动活动的 windowSplashscreenContent 和 windowBackground 可绘制对象加载的。

如果用户从“最近”屏幕带回活动而不是点击启动器图标,则 system_server 进程将调用 TaskSnapshotSurface.create() 以创建一个开始窗口,该窗口绘制已保存的活动快照。

显示启动窗口后,system_server 进程准备启动应用进程并调用 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() 通过套接字将起始参数发送到 Zygote 进程。

分裂 Zygote

根据关于内存管理的 Android 文档:

每个应用程序进程都是从一个名为 Zygote 的现有进程派生出来的。 Zygote 进程在系统启动并加载通用框架代码和资源(例如活动主题)时启动。 为了启动一个新的应用程序进程,系统会派生 Zygote 进程,然后在新进程中加载并运行应用程序的代码。 这种方法允许为框架代码和资源分配的大部分 RAM 页面在所有应用进程之间共享。

当系统启动时,Zygote 进程启动并调用 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();
  }
}

ZygoteInit.main() 做了两件重要的事情:

它预加载 Android 框架类和资源、共享库、图形驱动程序等。这种预加载不仅可以节省内存,还可以缩短启动时间。

然后它调用 ZygoteServer.runSelectLoop() 打开一个套接字并等待。

当在该套接字上接收到分叉命令时,ZygoteConnection.processOneCommand() 通过 ZygoteArguments.parseArgs() 解析参数并调用 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;
  }
}

注意:Android 10 添加了称为 Unspecialized App Process (USAP) 的优化的支持,这是一个等待专门化的分叉 Zygotes 池。 以额外内存为代价的稍微快一点的启动(默认关闭)。 Android 11 附带 IORap,可提供更好的结果。

应用程序诞生

分叉后,子应用进程运行 RuntimeInit.commonInit() 它安装了默认的 UncaughtExceptionHandler。 然后应用进程运行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);
    }
  }
}

这里有两个有趣的部分:

ActivityThread.main() 调用 Looper.loop() ,它永远循环,等待新消息发布到它的 MessageQueue。 ActivityThread.attach() 在 system_server 进程中对 ActivityManagerService.attachApplication() 进行 IPC 调用,让它知道应用程序的主线程已准备就绪。

应用操纵

在 system_server 进程中,ActivityManagerService.attachApplication() 调用 ActivityManagerService.attachApplicationLocked() 完成应用程序的设置:

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;
  }
}

一些关键要点:

system_server 进程在应用程序进程中对 ActivityThread.bindApplication() 进行 IPC 调用,该调用在应用程序主线程上调度对 ActivityThread.handleBindApplication() 的调用。

system_server 进程调度任何挂起的活动、服务和广播接收器的启动。

ActivityThread.handleBindApplication() 按以下顺序加载 APK 并加载应用程序组件:

  • 加载应用程序 AppComponentFactory 子类并创建一个实例。

  • 调用 AppComponentFactory.instantiateClassLoader()。

  • 调用 AppComponentFactory.instantiateApplication() 来加载应用程序 Application 子类并创建一个实例。

  • 对于每个声明的 ContentProvider,按优先级顺序,调用 AppComponentFactory.instantiateProvider() 来加载它的类并创建一个实例,然后调用 ContentProvider.onCreate()。

  • 调用 Application.onCreate()。

应用程序开发人员对 ActivityThread.handleBindApplication() 之前花费的时间几乎没有影响,因此应用程序冷启动监控应该从这里开始。

早期初始化

如果需要尽早运行代码,有几种选择:

最早的钩子是在加载 AppComponentFactory 类时。

  • 将 appComponentFactory 属性添加到 AndroidManifest.xml 中的应用程序标记。

  • 如果使用 AndroidX,则需要添加 tools:replace="android:appComponentFactory" 并将调用委托给 AndroidX AppComponentFactory

  • 可以在那里添加一个静态初始化程序并执行诸如存储时间戳之类的操作。

  • 缺点:这仅在 Android P+ 中可用,您将无法访问上下文。

对于应用程序开发人员来说,一个安全的早期挂钩是 Application.onCreate()。

对于库开发人员来说,一个安全的早期钩子是 ContentProvider.onCreate()。

有一个新的 AndroidX 应用启动库,它依赖于相同的 ContentProvider 技巧。 目标是只声明一个 ContentProvider 程序而不是多个,因为每个声明的 ContentProvider 都会使应用程序启动速度减慢几毫秒,并增加来自包管理器的 ApplicationInfo 对象的大小。

结论

启动 Activity 的用户体验在用户触摸屏幕时开始,但是应用程序开发人员对 ActivityThread.handleBindApplication() 之前花费的时间几乎没有影响,因此应用程序冷启动监控应该从这里开始。