[Разработка под Android] «Холодный» запуск приложения (перевод)

Автор Сообщение
news_bot ®

Стаж: 6 лет 9 месяцев
Сообщений: 27286

Создавать темы news_bot ® написал(а)
01-Окт-2020 12:32

Всем приветъ! Давно ничего не писал=)
Данный пост является кастомизированным переводом статьи с моими вставками =)
Это будет серия постов о процессе «холодного» запуска Android приложения, с момента нажатия на иконку и до создания процесса приложения.
Общая схема

Открывая «окно»…
Перед тем как запустить новый процесс приложения, 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 и не будет отрисован первый кадр. То есть пока не будет завершен «холодный» запуск. Пользователь может видеть данное окно длительное время, поэтому постарайтесь сделать его приятным =).

Содержимое стартового окна берется из drawable-ресурсов windowSplashscreenContent и windowBackground запускаемого Activity. Банальный пример такого окна:

Если пользователь восстанавливает Activity из режима последнего экрана(Recent screen), при этом на нажимая на иконку приложения, то 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-процесса…

Вкратце об этом я писал в предыдущей статье про запуск Android-а. А теперь давайте посмотрим поглубже на происходящие процессы.
Когда система загружается процесс 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-фреймворка. Подобная предзагрузка не только экономит память но еще и экономит время запуска приложений.
  • Далее он запускает метод 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, которая имеет пул не специализированных Zygote-процессов, для еще более быстрого запуска приложений.
Приложение запустилось!
После форка дочерний процесс запускает метод 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() создает новый поток(Thread) и вызывает метод Looper.loop(), в котором будет запущен новый инстанс Looper-а. Он будет привязан к новому потоку(который становится MainThread-ом aka UiThread) и будет работать(теоретически) бесконечно. Looper привязавшись, будет ожидать сообщений для того чтобы поместить их к своему MessageQueue.
  • Далее, метод ActivityThread.attach() делает IPC-запрос к методу ActivityManagerService.attachApplication() system_server-а, тем самым давая понять, что MainThread нашего приложения запущен и готов к работе.


Контроль над приложением
В процессе 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 делает IPC-запрос к методу ActivityThread.bindApplication() в процессе нашего приложения, который направляет запрос к методу ActivityThread.handleBindApplication() в MainThread-е приложения.
  • Сразу после этого, system_server планирует запуск Pending Activity, Service и BroadcastReciever-ов нашего приложения.
  • Метод ActivityThread.handleBindApplication() загружает APK-файл и компоненты приложения.
  • Разработчики имеют возможность немного повлиять на процессы перед запуском метода ActivityThread.handleBindApplication(), так что именно здесь должен начаться мониторинг холодного запуска приложения.


Давайте немного подробно разберем 3-ий пункт и узнаем что и как происходит при загрузке компонентов и ресурсов приложения. Порядок шагов такой:
  • Загрузка и создание инстанса класса AppComponentFactory.
  • Вызов метода AppComponentFactory.instantiateClassLoader().
  • Вызов метода AppComponentFactory.instantiateApplication() для загрузки и создания инстанса класса Application.
  • Для каждого объявленного ContentProvider-а, в порядке приоритета, вызов метода AppComponentFactory.instantiateProvider() для загрузки его класса и создания инстанса, после вызов метода ContentProvider.onCreate().
  • И наконец, вызов метода Application.onCreate().

Эпилог
Мы начали изучать «холодную» загрузку с очень обще-абстрагированного уровня:

Теперь мы знаем, что происходит «под капотом»:

Ну что же, это был длинный пост =) Но это не все! В следующих постах мы продолжим глубокое погружение в процесс запуска Android-приложения. Оставайтесь с нами!
===========
Источник:
habr.com
===========

===========
Автор оригинала: Roman Guy
===========
Похожие новости: Теги для поиска: #_razrabotka_pod_android (Разработка под Android), #_android_development, #_razrabotka_pod_android (
Разработка под Android
)
Профиль  ЛС 
Показать сообщения:     

Вы не можете начинать темы
Вы не можете отвечать на сообщения
Вы не можете редактировать свои сообщения
Вы не можете удалять свои сообщения
Вы не можете голосовать в опросах
Вы не можете прикреплять файлы к сообщениям
Вы не можете скачивать файлы

Текущее время: 22-Ноя 14:22
Часовой пояс: UTC + 5