Эта серия статей посвящена мониторингу производительности и стабильности Android-приложений в эксплуатационной среде. В прошлой статье автор писал об измерении времени в Android Vitals — Сколько времени?

В следующих статьях серии будет рассказано, как мониторить холодный запуск. Согласно документации по этапу запуска приложения:

Холодный запуск - это запуск приложения с нуля: до этого запуска системный процесс еще не создавал процесс приложения. Холодный запуск происходит, когда ваше приложение запускается впервые после загрузки устройства или после того, как система завершила работу приложения.

В начале холодного запуска у системы есть 3 задачи:

  1. Загрузка и запуск приложения.

  2. Отображение стартового окна.

  3. Создание процесса приложения.

В этой статье мы с вами подробно разберем начало холодного запуска, от тапа по иконке лаунчера до создания процесса приложения.

Диаграмма, созданная с помощью WebSequenceDiagram.

Activity.startActivity()

Когда пользователь тапает по иконке лаунчера, процесс запуска приложения вызывает Activity.startActivity(), делегируя выполнение в Instrumentation.execStartActivity():

public class Instrumentation {

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

Затем процесс запуска приложения выполняет IPC-вызов ActivityTaskManagerService.startActivity() процесса system_server. Процесс 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);
  }
}

Стартовое окно - это то, что видит пользователь во время запуска процесса приложения, ровно до тех пор, пока он не создаст свою activity и не отобразит свой первый кадр. То есть до тех пор, пока не будет завершен холодный запуск. Пользователь может любоваться стартовым окном достаточно долго, поэтому убедитесь, что оно выглядит очень хорошо .

Содержимое стартового окна загружается из графических ресурсов (drawables) windowSplashscreenContent и windowBackground запущенного activity. Чтобы узнать об этом больше, почитайте статью Android App Launching Made Gorgeous.

Если пользователь возвращает activity с экранного менеджера, а не посредством тапа по лаунчеру, процесс system_server вызывает TaskSnapshotSurface.create(), который создает стартовое окно из ранее сохраненного снапшота этого activity.

Как только стартовое окно отображено, процесс 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 запускается на старте системы, когда загружает общий системный код и ресурсы (например, темы activity). Чтобы запустить процесс нового приложения, система форкает этот процесс от 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() выполняет 2 важные функции:

  • Он предзагружает системные классы и ресурсы Android, общие библиотеки, графические драйверы и т. д. Эта предзагрузка не только экономит память, но и сокращает время запуска.

Когда по этому сокету получена команда к форку, 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), пула форков Zygote, ожидающих своей специализации. Запуск стал чуть быстрее за счет дополнительного потребленя памяти (по умолчанию эта фича отключена). 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);
    }
  }
}

Здесь есть два интересных момента:

Манипуляции с приложением

В процессе 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;
  }
}

Несколько ключевых моментов:

Ранняя инициализация

Если вам нужно запустить код как можно раньше, у вас есть несколько вариантов:

  • Самый ранний вариант - это когда загружается класс AppComponentFactory.

    • Добавьте атрибут appComponentFactory к тегу приложения в AndroidManifest.xml.

    • Если вы используете AndroidX, вам необходимо добавить tools:replace="android:appComponentFactory" и делегировать вызовы в AndroidX AppComponentFactory.

    • Вы можете добавить туда статический инициализатор и выполнять такие действия, как сохранение таймстемпов.

    • Недостатки: это доступно только в Android P+, и у вас не будет доступа к контексту.

  • Безопасный ранний вариант для разработчиков приложений - Application.onCreate().

  • Надежный ранний вариант для разработчиков библиотек - ContentProvider.onCreate(). Этот трюк популяризировал Дуг Стивенсон (Doug Stevenson) в своей книге “Как инициализируется Firebase на Android?”

  • Есть новая AndroidX App Startup library, основанная на том же приеме с провайдером. Цель состоит в том, чтобы объявить только один провайдер вместо нескольких, потому что каждый объявленный провайдер замедляет запуск приложения на несколько миллисекунд и увеличивает размер объекта ApplicationInfo из диспетчера пакетов.

Заключение

Мы начали с высокого уровня понимания того, как начинается холодный запуск:

Теперь мы точно представляем, что происходит:

Пользовательский опыт запуска activity начинается, когда пользователь касается экрана, однако разработчики приложений мало влияют на время до ActivityThread.handleBindApplication(), так что именно здесь должен начинаться мониторинг холодного запуска приложения.

Эта статья получилась достаточно длинной, а мы еще далеки от завершения холодного старта. Следите за обновлениями!


Перевод статьи подготовлен в преддверии старта курса Android Developer. Professional.

Комментарии (0)