Где у Android-приложения main() — от Zygote до onCreate

· 11 минут чтения

androidосновы

Обычная Java-программа начинается с public static void main(String[] args). Вы пишете точку входа, JVM её вызывает, дальше всё под вашим контролем.

В Android вы не пишете ничего похожего. Есть класс, унаследованный от Activity, есть onCreate, который кто-то зовёт, и есть «главный поток», который нельзя блокировать. Кто этот «кто-то», откуда взялся поток и почему он главный — обычно остаётся за кадром.

А ответ на удивление конкретный: main есть, он лежит в ActivityThread, и до вашего onCreate от него ведёт вполне обозримая цепочка вызовов. Разберём её целиком — от загрузки системы до строчки setContentView(...), — потому что из неё напрямую выводится половина повседневных вопросов: почему приложение вообще может словить ANR, почему холодный старт стоит дорого, что означают непонятные нижние кадры в краш-репорте и почему у Activity нельзя сделать конструктор с параметрами.

main() всё-таки есть

Точка входа Android-приложения — статический метод в классе android.app.ActivityThread. Вот он, сильно упрощённо (убраны трейсы, обработка аргументов и настройка окружения):

public static void main(String[] args) {
    // ... инициализация окружения процесса

    Looper.prepareMainLooper();          // делаем этот поток «главным»

    ActivityThread thread = new ActivityThread();
    thread.attach(false, startSeq);      // сообщаем системе: процесс поднялся

    if (sMainThreadHandler == null) {
        sMainThreadHandler = thread.getHandler();
    }

    Looper.loop();                       // бесконечный цикл обработки сообщений

    throw new RuntimeException("Main thread loop unexpectedly exited");
}

Несколько выводов сразу.

ActivityThread — не поток. Имя обманывает: это не Thread и не «поток одной Activity». Это класс, который живёт в главном потоке процесса и управляет всем приложением: создаёт Application, инстанцирует Activity, сервисы и провайдеры, обрабатывает команды от системы. На процесс он ровно один.

Главный поток — обычный поток с циклом. В нём нет ничего волшебного. Волшебство — в том, что на нём заведён Looper и очередь сообщений, а весь фреймворк договорился слать команды именно туда.

Последняя строчка — не ошибка автора. Looper.loop() не должен возвращать управление никогда. Если он всё же вернулся, значит очередь главного потока остановили, и жить приложению дальше незачем — поэтому сразу RuntimeException.

Looper, MessageQueue и Handler

Всё, что происходит в главном потоке, проходит через один цикл. Он выглядит примерно так:

for (;;) {
    Message msg = queue.next();     // блокируется, пока сообщений нет
    if (msg == null) {
        return;                     // очередь завершена — выходим из loop()
    }
    msg.target.dispatchMessage(msg);
    msg.recycleUnchecked();
}

Три участника:

Важная деталь: queue.next() не крутит процессор вхолостую. Внутри он уходит в нативный nativePollOnce(), который блокируется на epoll по файловому дескриптору. Пока сообщений нет, поток спит и не тратит батарею; система будит его записью в pipe. Поэтому «бесконечный цикл в каждом Android-приложении» — не расточительство, а самый обычный event loop.

На этой же очереди сидит и отрисовка. Choreographer подписывается на вертикальную синхронизацию и планирует кадры теми же сообщениями. Отсюда прямое следствие: любая долгая работа в главном потоке — это не «замедление UI» абстрактно, а конкретная задержка обработки следующего сообщения в очереди, будь то кадр анимации или событие касания.

Кто вызывает ActivityThread.main()

Теперь снизу вверх: main() есть, но откуда он запускается.

При старте системы init по своим .rc-скриптам поднимает процесс app_process, который выполняет ZygoteInit.main(). Это и есть Zygote — процесс-инкубатор, из которого потом форкаются все приложения. Он делает три вещи:

  1. preload() — заранее загружает в память тысячи классов фреймворка, системные ресурсы, шрифты, нативные библиотеки. Список классов лежит в /system/etc/preloaded-classes.
  2. forkSystemServer() — форкает system_server, где живут ActivityManagerService, WindowManagerService, PackageManagerService и остальные системные сервисы.
  3. runSelectLoop() — садится на unix-сокет (/dev/socket/zygote) и ждёт команд «форкни мне процесс».

Дальше пользователь тапает по иконке. Лаунчер отправляет интент, тот доезжает до ActivityTaskManagerService в system_server, система видит, что процесса приложения ещё нет, и вызывает Process.start(...). ZygoteProcess пишет в сокет zygote аргументы: имя класса точки входа, UID/GID, целевой SDK, ABI, имя процесса.

Zygote делает forkAndSpecialize(). В ребёнке:

handleChildProc()
  └─ ZygoteInit.zygoteInit()
       ├─ RuntimeInit.commonInit()        // хендлер необработанных исключений, TZ, логи
       ├─ ZygoteInit.nativeZygoteInit()   // старт биндер-потоков процесса
       └─ RuntimeInit.applicationInit()
            └─ RuntimeInit.findStaticMain("android.app.ActivityThread", argv, cl)

И вот здесь — интересный трюк. findStaticMain не вызывает main сразу:

protected static Runnable findStaticMain(String className, String[] argv,
        ClassLoader classLoader) {
    Class<?> cl = Class.forName(className, true, classLoader);
    Method m = cl.getMethod("main", new Class[] { String[].class });
    // ... проверки модификаторов
    return new MethodAndArgsCaller(m, argv);
}

static class MethodAndArgsCaller implements Runnable {
    public void run() {
        mMethod.invoke(null, new Object[] { mArgs });
    }
}

Он возвращает Runnable, который поднимается наверх по стеку до самого ZygoteInit.main(), и только там вызывается run(). Сделано это ради чистого стека: к моменту старта приложения все кадры инициализации zygote уже раскручены, и ActivityThread.main оказывается на дне стека, а не поверх десятка служебных вызовов. Раньше тот же эффект достигался броском исключения MethodAndArgsCaller — отсюда странные упоминания «zygote бросает исключение, чтобы запустить приложение» в старых статьях.

Итого: ваше приложение — это форк zygote, в котором рефлексией вызвали ActivityThread.main.

Почему через fork: copy-on-write

fork() в Linux не копирует память процесса. Он заводит новую таблицу страниц, помечает страницы как copy-on-write, и физическое копирование происходит только для тех страниц, в которые кто-то пишет. Читать можно бесплатно.

Для Android это ключевая оптимизация. Классы фреймворка, распарсенные системные ресурсы, прогретый код — всё это загружено один раз в zygote и после форка разделяется всеми приложениями без повторной загрузки и без дублирования в памяти. Запуск приложения не начинается с нуля: к моменту вызова ActivityThread.main половина работы уже сделана.

Отсюда, впрочем, вылезает и обратная сторона. У всех процессов одинаковое стартовое состояние памяти, а всё, что попало в zygote, попадает и в каждое приложение. Именно так работал известный троян Triada: закрепившись в zygote, он оказывался внутри любого запускаемого приложения, потому что все они — его потомки. Одна точка компрометации на всю систему.

Два дополнения из более новых версий:

Между fork и вашим кодом: bindApplication

После форка процесс уже живой, но приложения в нём ещё нет — есть только ActivityThread. Связывает их вызов attach() из main():

final IActivityManager mgr = ActivityManager.getService();
mgr.attachApplication(mAppThread, startSeq);

mAppThread — это ApplicationThread, биндер-объект, через который система будет отдавать процессу команды. Получив его, system_server знает, куда стучаться, и в ответ шлёт bindApplication(...) со всеми данными: пакет, ApplicationInfo, список провайдеров, конфигурация, инструментирование.

Все такие команды приходят в биндер-потоке, а не в главном, поэтому ApplicationThread просто перекладывает их в очередь через хендлер H — приватный Handler внутри ActivityThread. Это тот самый класс с длинным списком констант (BIND_APPLICATION, EXECUTE_TRANSACTION, RECEIVER, CREATE_SERVICE, STOP_SERVICE, LOW_MEMORY, …). Дальше handleBindApplication выполняется уже в главном потоке и делает, в порядке:

1. настраивает процесс: имя, часовой пояс, StrictMode, кэш пакета
2. создаёт LoadedApk и ClassLoader приложения
3. создаёт Instrumentation
4. makeApplication()  →  new Application()  →  attachBaseContext(context)
5. installContentProviders()  →  ContentProvider.onCreate() для каждого провайдера
6. mInstrumentation.callApplicationOnCreate(app)  →  Application.onCreate()

Пункты 5 и 6 стоит запомнить именно в этом порядке. ContentProvider.onCreate() вызывается раньше Application.onCreate() — и это не случайность, а официально гарантированное поведение, на котором держится автоинициализация библиотек. Firebase, WorkManager и androidx App Startup объявляют в манифесте фиктивный провайдер только затем, чтобы получить управление до вашего кода и не просить вас ничего вызывать в Application.

Как запуск доезжает до Activity.onCreate

Процесс поднят, Application создан. Осталось запустить ту Activity, ради которой всё затевалось.

Современный путь (Android 9 / API 28 и новее) выглядит так. Система на своей стороне собирает ClientTransaction — пакет из элементов (ClientTransactionItem), описывающих, что клиенту нужно сделать: запустить Activity, доставить конфигурацию, перевести в определённое состояние жизненного цикла. Транзакция уезжает в процесс приложения через ApplicationThread.scheduleTransaction, попадает в очередь как H.EXECUTE_TRANSACTION и исполняется TransactionExecutor:

TransactionExecutor.execute(transaction)
  └─ для каждого item транзакции:
       ├─ cycleToPath(...)          // довести Activity до нужного состояния
       ├─ item.execute(...)         // собственно операция
       └─ item.postExecute(...)

Для запуска Activity элемент называется LaunchActivityItem, и его execute() предельно короткий:

@Override
public void execute(ClientTransactionHandler client, PendingTransactionActions pendingActions) {
    final ActivityClientRecord r = new ActivityClientRecord(/* ... */);
    client.handleLaunchActivity(r, pendingActions, mDeviceId, null);
}

client здесь — тот самый ActivityThread (он реализует ClientTransactionHandler). Дальше уже знакомая по старым статьям цепочка:

handleLaunchActivity(r, ...)
  └─ performLaunchActivity(r, ...)
       ├─ Instrumentation.newActivity(...)      // создание объекта Activity рефлексией
       │     └─ AppComponentFactory.instantiateActivity(...)   // API 28+
       ├─ activity.attach(appContext, this, instrumentation,
       │                  token, application, intent, ...)     // ← вот тут появляется Context
       ├─ mInstrumentation.callActivityOnCreate(activity, icicle)
       │     └─ activity.performCreate(icicle)
       │           └─ onCreate(savedInstanceState)   // ← ВАШ КОД
       └─ r.activity = activity

После этого следующие элементы транзакции (ResumeActivityItem и компания) через cycleToPath проводят Activity по остальным состояниям: onStart, onResume.

Два практических следствия видно прямо из этого листинга:

class MainActivity : AppCompatActivity() {
    // ⚠️ выполняется в конструкторе, attach() ещё не был вызван
    private val prefs = getSharedPreferences("app", MODE_PRIVATE)   // NPE

    // так — нормально: lazy сработает уже после attach()
    private val prefsLazy by lazy { getSharedPreferences("app", MODE_PRIVATE) }
}

Как было до Android 9 и почему переделали

Если открыть статьи 2016–2018 годов (и, к сожалению, часть свежих), там описан другой путь: у хендлера H была константа LAUNCH_ACTIVITY, система слала именно её, а handleMessage вызывал handleLaunchActivity напрямую. Никаких транзакций.

Проблема была в том, что состояние жизненного цикла вычислялось на стороне клиента. Система присылала отдельные независимые сообщения — «запусти», «останови», «возобнови», — а ActivityThread сам догадывался, через какие промежуточные состояния провести Activity. При гонках между несколькими такими сообщениями Activity могла, например, получить onStop для уже уничтоженного экземпляра или пропустить переход.

В Android 9 это переписали: теперь system_server присылает явную последовательность операций и конечное состояние, а TransactionExecutor на клиенте достраивает недостающие переходы (cycleToPath). Логика перехода стала одна и лежит в одном месте.

Что означает эта разница на практике: в стектрейсах и логах современных устройств вы не увидите LAUNCH_ACTIVITY — там будет EXECUTE_TRANSACTION и кадры из пакета android.app.servertransaction. Статьи, где этого пакета нет, описывают систему до Android 9.

Версия Что изменилось в цепочке запуска
Android 9 (API 28) ClientTransaction / TransactionExecutor вместо H.LAUNCH_ACTIVITY; появился AppComponentFactory для подмены создания компонентов
Android 10 (API 29) App Zygote (android:useAppZygote), USAP-пул предварительно форкнутых процессов (по умолчанию выключен)
Android 11 (API 30) Looper.loopOnce() — цикл разбит на итерации; в стектрейсах появляется отдельный кадр

Вся цепочка целиком

init
 └─ app_process → ZygoteInit.main()
      ├─ preload()                     классы фреймворка и ресурсы в память
      ├─ forkSystemServer()            → AMS, WMS, PMS
      └─ runSelectLoop()               ждём команд на сокете
                │
  тап по иконке │  Launcher → ATMS (system_server) → Process.start()
                ▼
      forkAndSpecialize()              fork + copy-on-write
                │
                ▼  (в дочернем процессе)
      ZygoteInit.zygoteInit() → RuntimeInit.applicationInit()
                │                       findStaticMain() → Runnable
                ▼
      ActivityThread.main()
        ├─ Looper.prepareMainLooper()
        ├─ thread.attach()  ──биндер──▶ AMS.attachApplication()
        │                                     │
        │        H.BIND_APPLICATION  ◀────────┘  bindApplication()
        │            ├─ makeApplication() → attachBaseContext()
        │            ├─ installContentProviders() → ContentProvider.onCreate()
        │            └─ callApplicationOnCreate() → Application.onCreate()
        │
        └─ Looper.loop()   ◀── бесконечный цикл, дальше всё приходит сюда
                │
                │  H.EXECUTE_TRANSACTION
                ▼
      TransactionExecutor.execute()
        └─ LaunchActivityItem.execute()
             └─ handleLaunchActivity() → performLaunchActivity()
                  ├─ Instrumentation.newActivity()
                  ├─ activity.attach()
                  └─ callActivityOnCreate() → onCreate()   ← ваш код

Что из этого следует на практике

Читать нижние кадры стектрейса

Типичный краш-репорт снизу выглядит так:

FATAL EXCEPTION: main
java.lang.IllegalStateException: ...
    at com.example.app.MainActivity.onCreate(MainActivity.kt:31)
    at android.app.Activity.performCreate(Activity.java:8305)
    at android.app.Instrumentation.callActivityOnCreate(Instrumentation.java:1417)
    at android.app.ActivityThread.performLaunchActivity(ActivityThread.java:3626)
    at android.app.ActivityThread.handleLaunchActivity(ActivityThread.java:3782)
    at android.app.servertransaction.LaunchActivityItem.execute(LaunchActivityItem.java:101)
    at android.app.servertransaction.TransactionExecutor.execute(TransactionExecutor.java:95)
    at android.app.ActivityThread$H.handleMessage(ActivityThread.java:2317)
    at android.os.Handler.dispatchMessage(Handler.java:106)
    at android.os.Looper.loopOnce(Looper.java:201)
    at android.os.Looper.loop(Looper.java:288)
    at android.app.ActivityThread.main(ActivityThread.java:7898)
    at java.lang.reflect.Method.invoke(Native Method)
    at com.android.internal.os.RuntimeInit$MethodAndArgsCaller.run(RuntimeInit.java:548)
    at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:936)

Теперь эти пятнадцать строк читаются как оглавление статьи: снизу вверх — zygote, рефлексивный вызов main, цикл Looper, хендлер H, транзакция запуска, создание Activity, ваш код. Полезно и обратное: если в стектрейсе внизу нет ActivityThread.main — исключение прилетело не из главного потока, и искать надо в своём пуле или корутинном диспетчере.

ANR — это переполненная очередь, а не «зависший поток»

Главный поток обрабатывает сообщения строго по одному. Пока ваш handleMessage, onCreate или колбэк из runOnUiThread не вернул управление, Looper.loop() не возьмёт следующее сообщение — ни кадр от Choreographer, ни событие ввода. Система замечает, что событие не обработано за отведённое время, и показывает ANR.

Поэтому «не блокируй главный поток» — это не суеверие, а описание устройства цикла: любая операция на нём выполняется вместо отрисовки и обработки ввода.

Холодный старт: что реально лежит на критическом пути

Из схемы видно, что до первого кадра успевают выполниться: ContentProvider.onCreate() всех библиотек, Application.onCreate(), создание Activity, ваш onCreate с раздуванием разметки. Всё это — последовательно, в одном потоке.

Практический вывод: тяжёлая инициализация в Application.onCreate дорога не «немного», а ровно на своё время, при каждом холодном старте. То же касается библиотек с провайдерами-инициализаторами: их onCreate выполняется всегда, даже если функциональность в этом запуске не понадобится. Здесь помогают ленивая инициализация, androidx.startup с ручным управлением зависимостями и удаление автопровайдеров тех библиотек, которые вы инициализируете сами.

Померить, а не гадать, помогает трейс: Perfetto/Systrace покажет bindApplication, activityStart и Choreographer#doFrame на одной шкале, а adb shell am start -W <пакет>/<activity> вернёт TotalTime и WaitTime.

android:process — второй Application в том же APK

Если у компонента в манифесте указан android:process, система форкнет для него отдельный процесс, а значит будет и отдельный ActivityThread, и отдельный вызов Application.onCreate(). Классический баг: аналитика или крэш-репортер инициализируются дважды, а глобальный синглтон в двух процессах — это два разных объекта с разным состоянием.

Отсюда же — привычка проверять имя процесса перед тяжёлой инициализацией:

class App : Application() {
    override fun onCreate() {
        super.onCreate()
        if (Application.getProcessName() != packageName) return   // API 28+
        // инициализация только в основном процессе
    }
}

Смерть процесса и статические поля

Процесс приложения — обычный процесс Linux, который система убивает, когда ей нужна память. Вместе с ним исчезает всё: ActivityThread, Application, статические поля, синглтоны, кеши в памяти.

Когда пользователь возвращается в приложение, zygote форкает новый процесс, снова выполняется ActivityThread.main, снова создаётся Application — но Activity восстанавливается на том экране, где пользователь был, с сохранённым savedInstanceState. Именно поэтому «а у меня всё работает» ломается на устройствах с малым объёмом памяти: состояние, положенное в статику, было потеряно, а состояние в SavedStateHandle — нет. Проверяется это без гаданий:

adb shell am kill com.example.app

Приложение при этом должно остаться в списке недавних — а после возврата в него сработает ровно тот сценарий с пересозданием процесса.

Итого

Ничего магического на этом пути нет — есть очередь сообщений, биндер и рефлексия. Зато, зная маршрут, гораздо проще объяснить и ANR, и медленный старт, и половину строк в краш-репорте.


Статья написана по мотивам двух разборов на Хабре: «Главный метод Android-приложения» и «Как Android запускает MainActivity». Механика запуска Activity с тех пор изменилась, поэтому цепочка выше сверена с актуальным AOSP: ActivityThread.java, RuntimeInit.java, ZygoteInit.java, LaunchActivityItem.java, TransactionExecutor.java.