Разработка
Преодолевая барьер скорости: как неблокирующие заставки сокращают время запуска приложений Android на 90%
Подход с неблокируемой заставкой обеспечивает значительное повышение производительности (на 90% быстрее загрузка страницы при консервативном тестировании и до 95% при сложной анимации), но и здесь есть свои недостатки.
Наступили праздники, и мы видим красивые сплеш-скрины и логотипы в каждом приложении. При их разработке каждый Android-разработчик сталкивается с дилеммой: пользователи ожидают красивого, фирменного оформления сплеш-скрина, но нативный API Google имеет существенные ограничения. Распространенное решение — создание собственного SplashActivity — кажется логичным, но приводит к скрытому снижению производительности, из-за которого приложение может стать медленным и неотзывчивым.
Для решения этой проблемы я разработал тестовую библиотеку EventSplash, реализующую подход с неблокируемыми экранами-заставками. Полный код реализации и бенчмарк-теста доступен на GitHub.
В этом исследовании представлены эмпирические данные контролируемого эксперимента, сравнивающего традиционные сплеш-скрины на основе активити с инновационным подходом на основе view. Результаты, полученные с помощью консервативных сравнений, показывают: сокращение времени загрузки страницы на 90%, улучшение времени первой отрисовки контента на 78% и сокращение времени полной отрисовки на 41%.
Мы рассмотрим существенные преимущества, которые могут дать сложные анимации, такие как Lottie, при этом остановившись на компромиссах и затратах ресурсов, связанных с параллельной обработкой.
Мини-глоссарий
- Первая отрисовка контента (First Contentful Paint, FCP): Время до появления первого значимого контента на экране.
- Время полной отрисовки (Fully Painted Time, FPT): Время до полной отрисовки экрана и появления интерактивности.
- Холодный старт: Запуск приложения, когда процесс не запущен (наибольшее влияние на производительность).
- Jank: Заикание или пропуск кадров, которые пользователи воспринимают как низкую производительность.
- TTID/TTFD (Time to Initial Display/Time to Fully Drawn): Время до первоначального отображения/Время до полной отрисовки (официальные метрики Android).
- Нехватка памяти: Состояние системы при критически низком уровне доступной памяти.
- Low Memory Killer (LMK): Демон Android, завершающий процессы при нехватке памяти.
- Choreographer.doFrame: Система координации кадров Android, которая управляет анимацией, вводом и отрисовкой.
Скрытая цена красивых сплеш-скринов
Смысл проблемы
Нативный SplashScreen API от Google для Android 12+ обеспечивает отличную производительность, но ограниченные возможности настройки. Он не поддерживает:
- Видео на фоне
- Анимацию Lottie
- Сложные элементы брендинга
- Рекламный контент во время распродаж/мероприятий
- Настраиваемые эффекты перехода
Это вынуждает разработчиков использовать собственные реализации, обычно с использованием отдельного SplashActivity. Хотя такой подход предоставляет творческую свободу, он создает блокирующую последовательность, которая задерживает загрузку основного контента приложения.
Почему традиционные SplashActivity снижают производительность
В документации Android подчеркивается, что приложения следует оптимизировать для холодного запуска, поскольку это «также может повысить производительность теплых и горячих запусков». Однако традиционные реализации SplashActivity противоречат этому принципу.
При использовании отдельного SplashActivity система должна:
- Создать и инициализировать SplashActivity
- Расширить представления экрана-заставки
- Запустить анимацию заставки до завершения
- Удалить активити
- Создать и инициализировать основную активити
- Расширить представления основного контента
Этот последовательный процесс означает, что загрузка основного контента не может начаться до завершения SplashActivity, что является фундаментальным архитектурным недостатком, влияющим на воспринимаемую пользователем производительность.
Путь: изучение различных подходов
Прежде чем прийти к финальной реализации EventSplash, я изучил несколько подходов. Понимание этих подходов даёт ценный контекст для принятия окончательных решений по дизайну и демонстрирует итеративный характер оптимизации производительности.
Отвергнутый подход: полупрозрачное наложение Activity
Одной из первоначальных идей было использовать SplashActivity с полупрозрачной темой для наложения на MainActivity. Теоретически MainActivity могла загружаться в фоновом режиме, пока поверх неё отображался экран-заставка.
Последовательность запуска:
- Приложение запускается со
SplashActivityс полупрозрачной темой. - Эта активити отображается поверх
MainActivity, не перекрывая её полностью. - После небольшой задержки или после завершения инициализации
SplashActivityзавершается, открывая MainActivity.
Почему от него отказались:
Этот подход привёл к снижению производительности на 14%. Проблема заключается в том, как Android обрабатывает жизненные циклы активити и её отрисовку. Система не запускает оба вида активити по-настоящему параллельно. Вместо этого она создаёт последовательную зависимость, и графический процессор вынужден формировать два отдельных буфера, что приводит к значительным накладным расходам оперативной памяти, потреблению батареи и иногда даже отключает анимацию перехода между окнами.
Как отмечено в документации Android по полупрозрачным активити:
Оконный менеджер сохраняет предыдущую поверхность в Z-порядке и накладывает новую поверх неё. Предыдущая активити по-прежнему видна через любые прозрачные или частично прозрачные пиксели в новом окне.
Именно эта операция наложения и привела к снижению производительности.
Выигрышный подход: механизм защищённого экрана-заставки
В конечном итоге я остановился на более сложном подходе, включающем механизм защищённого сплеш-скрина. Этот метод использует ViewTreeObserver.OnPreDrawListener для блокировки отрисовки пользовательского интерфейса до тех пор, пока не будут выполнены определённые условия.
Как это работает:
- Слушатель
OnPreDrawListenerприкрепляется к активитиdecorViewсразу при запуске - Метод
onPreDraw()слушателя возвращаетfalse, фактически блокируя отрисовку - Слушатель возвращает
trueтолько при выполнении всех условий, что позволяет отрисовать контент
Ключевая реализация:
// The gate mechanism gate.onPreDraw() → returns false = BLOCK all drawing gate.onPreDraw() → returns true = ALLOW drawing to proceed
Этот подход идеально соответствует рекомендациям официальной документации Android по увеличению времени отображения заставок на экране:
«Если вам нужно асинхронно загрузить небольшой объём данных, например, настройки приложения с локального диска, вы можете использовать ViewTreeObserver.OnPreDrawListener для приостановки приложения до отрисовки первого кадра».
Библиотека EventSplash расширяет эту концепцию, обеспечивая точный контроль над тем, что видит пользователь при запуске приложения, предотвращая мерцание контента и обеспечивая плавный интерфейс.
Эксперимент: измерение реального воздействия
Тестовая среда
- Устройство: Xiaomi POCO F1, Android 10
- Сборка: релизная конфигурация
- Методология: 35 холодных запусков на конфигурацию, 2-секундная пауза между запусками
- Метрики: кастомная библиотека PerfTracker, измеряющая загрузку страницы, FCP и FPT
- Скрипт: автоматизирован с помощью
perf_loop.shдля воспроизводимости
Весь тестовый код и скрипты доступны в репозитории GitHub для воспроизводимости.
Протестированные подходы к реализации
- Блокирующий сплеш-скрин по умолчанию: простая
SplashActivityс базовой маршрутизацией (консервативный показатель для сравнения) - Неблокирующий сплеш-скрин по умолчанию: библиотека EventSplash с простым оверлеем
- Блокирующий сплеш-скрин с Lottie: традиционный подход со сложной анимацией
- Неблокирующий сплеш-скрин с Lottie: EventSplash с параллельной анимацией Lottie
Результаты: впечатляющий потенциал
Честное сравнение: производительность сплеш-скрина по умолчанию
Для сравнения «яблок с яблоками» мы сосредоточимся на реализациях сплеш-скрина по умолчанию, где блокирующий подход просто расширяет активити для маршрутизации:
| Подход | Page Load (ms) | FCP (ms) | FPT (ms) | Пользовательский опыт |
|---|---|---|---|---|
| По умолчанию блокирующий | 366 | 744 | 2,195 | Заметная задержка |
| По умолчанию не блокирующий | 37 | 164 | 1,295 | Плавный и отзывчивый |
| Улучшение | 90% | 78% | 41% | Значительное улучшение |
Преимущество Lottie анимации
Когда мы внедряем сложную анимацию Lottie, архитектурное различие становится ещё более заметным:
| Подход | Page Load (ms) | FCP (ms) | FPT (ms) | Примечание |
|---|---|---|---|---|
| Lottie блокирующий | 2,228 | 2,347 | 3,524 | Время включает продолжительность анимации |
| Lottie не блокирующий | 109 | 312 | 1,467 | Анимация идет параллельно |
| Улучшение | 95% | 87% | 58% | Кардинальное улучшение |
Понимание цифр Lottie
Важное примечание: числа в блокирующем сплеш-скрине с Lottie изначально учитывают длительность анимации. Пользователь должен дождаться полного завершения анимации, прежде чем увидеть какой-либо основной контент. При неблокирующем подходе анимация и загрузка контента выполняются параллельно, поэтому к моменту завершения анимации Lottie FPT обычно уже завершен или почти завершен.
Параллельное выполнение — ключевое преимущество архитектуры: вы получаете красивую анимацию без ущерба для производительности.
Разбор улучшений производительности
Обзор: что произошло
Даже при консервативном сравнении со сплеш-скрином по умолчанию неблокирующий подход обеспечил 90% ускорения загрузки страницы. Пользовательский опыт изменился с «заметной задержки» на «плавный и отзывчивый».
При использовании сложных анимаций, таких как Lottie, преимущества становятся ещё более заметными, поскольку традиционный подход заставляет пользователей ждать всю последовательность анимации, прежде чем появится нужный контент.
Технический механизм
Выигрыш в производительности достигается за счёт параллельного выполнения. В то время как традиционный подход запускает заставку и основной контент последовательно, подход на основе представлений запускает их одновременно:
Традиционный (последовательный):
Splash Activity → Animation → Destroy → Main Activity → Content Load → Display
Неблокирующий (параллельный):
Main Activity + Content Load (background)
↓
Splash View (overlay) → Remove overlay → Display loaded content
Это архитектурное различие полностью устраняет блокирующее узкое место.
Подробное описание: техническая реализация
Традиционная реализация Splash Activity:
class SplashActivity : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
installSplashScreen()
enableEdgeToEdge()
setContent {
Loader { // Blocks until animation completes
startActivity(Intent(this@SplashActivity, MainActivity::class.java))
}
}
}
@Composable
fun Loader(onComplete: () -> Unit) {
val composition by rememberLottieComposition(LottieCompositionSpec.RawRes(R.raw.sale_tags))
val progress by animateLottieCompositionAsState(composition)
// Animation blocks main content loading
if (progress == 1.0f) {
onComplete.invoke()
}
}
}
EventSplash: неблокирующая реализация:
class EventSplash(
private val activity: ComponentActivity,
private val config: EventSplashConfig,
) {
private val decorView: ViewGroup = activity.window.decorView as ViewGroup
private var composeView: ComposeView? = null
// Gate prevents premature display until main content ready
private val gate = object : ViewTreeObserver.OnPreDrawListener {
override fun onPreDraw(): Boolean {
return if (isReady) {
decorView.viewTreeObserver.removeOnPreDrawListener(this)
true
} else false
}
}
init {
decorView.viewTreeObserver.addOnPreDrawListener(gate)
setupSplashCompose() // Non-blocking overlay
isReady = true
}
private fun setupSplashCompose() {
val view = ComposeView(activity).apply {
layoutParams = ViewGroup.LayoutParams(MATCH_PARENT, MATCH_PARENT)
setContent {
getProvider(config).Content(onFinish = { dismiss() })
}
}
composeView = view
decorView.addView(view) // Overlay on main content
}
}
Сравнение способов применения
Традиционный подход:
// Requires separate activity, blocks main content
class MainActivity : ComponentActivity() {
// Main content only loads after splash completes
}
Подход EventSplash:
class MainActivity : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
// Non-blocking: splash displays while content loads
EventSplashApi.attachTo(this).with(getSaleConfig()).show()
setContent {
// Main content loads immediately in parallel
MainAppContent()
}
}
}
Резюме: различия в реализации
Традиционный подход требует отдельного жизненного цикла активити, в то время как EventSplash внедряет наложение представления, которое сосуществует с основным процессом загрузки контента.
Почему: архитектурные преимущества
- Единый контекст активити: устраняет накладные расходы на переход между активити
- Параллельная обработка: основное содержимое загружается во время отображения заставки
- Меньший объём памяти: отсутствие дублирующихся объектов активити
- Меньше циклов
Choreographer.doFrame: снижение нагрузки на конвейер рендеринга - Оптимизированная иерархия представлений: единое представление декорирования вместо отдельных активити
Проблема Choreographer.doFrame
Понимание проблем рендеринга кадров
Система рендеринга Android использует Choreographer.doFrame для координации анимации, ввода и отрисовки. В документации предупреждается:
«Если Systrace показывает, что сегмент Layout в Choreographer#doFrame работает слишком интенсивно или слишком часто, это означает, что вы столкнулись с проблемами производительности макета».
Почему Splash Activities вызывают пропуски кадров
Традиционные реализации сплеш-скринов создают множество узких мест производительности:
- Двойной проход разметки: каждая активность требует отдельного создания и размещения представлений (инфляция View и макета)
- Издержки переключения контекста: операционная система должна управлять контекстами нескольких активити
- Повышенная нагрузка на память: дублированные иерархии представлений потребляют дополнительную оперативную память
- Проблемы с таймингом кадров: переходы между активностями вызывают дополнительные циклы doFrame
Анализ Perfetto
При анализе трассировок с помощью Perfetto традиционные заставки демонстрируют:
- Увеличенное время выполнения
Choreographer.doFrame - Множественные пики инфляции макета
- Повышенная нагрузка на сборку мусора
- Задержка доступности основного потока
Подход, основанный на представлениях, устраняет эти проблемы, поддерживая единый контекст рендеринга на протяжении всего процесса запуска.
Обратная сторона медали: скрытые затраты и компромиссы
Важное замечание: параллельная обработка — это не бесплатно
Хотя наши результаты показывают значительное повышение производительности, неблокирующий подход имеет свои собственные проблемы, которые необходимо тщательно учитывать. Запуск сплеш-скринов с анимациями одновременно с загрузкой основного контента создает дополнительную нагрузку на ресурсы, которая отсутствует при последовательных подходах.
Нагрузка на память: основная проблема
Увеличение пикового использования памяти:
// Memory usage pattern comparison Traditional Approach: Splash: 50MB → 0MB → Main Content: 120MB = Peak: 120MB Non-blocking Approach: Splash + Main Content: 50MB + 120MB = Peak: 170MB
Реальные последствия:
- Простые сплеш-скрины добавляют 20–50 МБ при параллельном выполнении
- Анимации Lottie могут потреблять более 50–100 МБ во время рендеринга
- Совокупное пиковое использование может быть на 40–70% выше, чем при последовательном подходе
- Устройства начального уровня (1–2 ГБ ОЗУ) подвержены нехватке памяти
Риск Low Memory Killer (нехватка памяти)
Демон Android Low Memory Killer отслеживает системную память и может завершать приложения, находящиеся под нагрузкой:
«Нехватка памяти, состояние, при котором системе не хватает памяти, требует от Android освобождения памяти путем ограничения или завершения ненужных процессов».
Факторы риска:
- Завершение процесса приложения при запуске ухудшает UX
- Более агрессивное завершение фоновых приложений
- Фрагментация памяти из-за параллельного выделения ресурсов
- Особенно проблематично на бюджетных устройствах
Влияние на ЦП и батарею
Повышенная нагрузка на ЦП:
Choreographer.doFrameобрабатывает несколько одновременных операций- Основной поток становится более загруженным из-за работы с наложением UI
- Конвейер рендеринга графического процессора обрабатывает как заставку, так и контент одновременно
Проблемы энергопотребления: Исследования показывают, что «рендеринг пользовательского интерфейса на смартфонах требует мощных центрального и графического процессоров для достижения плавности, воспринимаемой пользователем, и это вносит значительный вклад в энергопотребление».
Проблемы совместимости устройств
Важные моменты для устройств начального уровня:
- Одноядерные или двухъядерные процессоры испытывают трудности с распараллеливанием
- Ограниченный объем оперативной памяти делает нагрузку на память критической
- Более медленная загрузка данных приводит к задержкам
- Преимущества могут быть не применимы к бюджетным устройствам
Когда НЕ следует использовать неблокируемый подход
Сценарии, в которых традиционный подход может быть предпочтительнее:
- Устройства с ограниченными ресурсами (<2 ГБ ОЗУ)
- Приложения, критически важные для батареи, где энергопотребление имеет первостепенное значение
- Простые заставки без сложной анимации
- Приложения с высокой нагрузкой при запуске, которые уже нагружают систему
- Устаревшие кодовые базы, где риски рефакторинга перевешивают преимущества
Стратегии снижения рисков
Адаптивная реализация:
class AdaptiveSplashStrategy {
fun chooseSplashApproach(): SplashConfig {
return when {
isLowEndDevice() -> SimpleSplashConfig()
isBatteryLow() -> ReducedAnimationConfig()
isHighPerformanceDevice() -> FullLottieConfig()
else -> DefaultConfig()
}
}
private fun isLowEndDevice(): Boolean {
val activityManager = getSystemService(Context.ACTIVITY_SERVICE) as ActivityManager
return activityManager.isLowRamDevice ||
Runtime.getRuntime().maxMemory() < 256 * 1024 * 1024
}
}
Мониторинг памяти:
private fun monitorMemoryPressure() {
val memoryInfo = ActivityManager.MemoryInfo()
activityManager.getMemoryInfo(memoryInfo)
if (memoryInfo.lowMemory) {
// Fallback to simpler splash
simplifyOrDismissSplash()
}
}
Отраслевой контекст и валидация
Соответствие передовым отраслевым практикам
Подход EventSplash соответствует последним отраслевым тенденциям и официальным рекомендациям. Такие компании, как Turo, добились схожих результатов, исключив выделенные активити-заставки. Как сообщается в их исследовании:
«Изначально мы использовали выделенную активити-заставку SplashActivity для выполнения всех задач запуска перед перенаправлением приложения в HomeActivity. Однако последние рекомендации не советуют использовать этот подход. Поэтому мы исключили избыточную активити-заставку SplashActivity и перенесли всю логику запуска в нашу корневую активити».
Turo добился сокращения времени запуска на 77%, используя аналогичные принципы.
Валидация с помощью официальной документации
Подход дополнительно подтверждается явной рекомендацией в официальной документации Android использовать ViewTreeObserver.OnPreDrawListener для управления сплеш-скрином, что и является основой EventSplash.
Рекомендации и распространённые ошибки
✅ Что нужно делать:
- Используйте реализации заставок на основе представлений для кастомных анимаций на совместимых устройствах
- Реализуйте адаптивные стратегии, основанные на возможностях устройств
- Измеряйте производительность на реальных устройствах и выпускайте сборки для всех уровней устройств
- Отслеживайте использование памяти и реализуйте предотвращение утечек
- Оптимизируйте для холодного запуска как наихудшего сценария
- Тщательно тестируйте на устройствах начального уровня для обеспечения широкой совместимости
- Реализуйте правильное управление жизненным циклом представлений заставок
❌ Чего не стоит делать:
- Не используйте универсальные решения: возможности устройств сильно различаются
- Не игнорируйте нехватку памяти: отслеживайте и адаптируйтесь к системным ограничениям
- Не используйте отдельные
SplashActivity, не рассматривая альтернативы - Не блокируйте загрузку основного контента анимацией заставок
- Не игнорируйте показатели Android Vitals в Play Console
- Не тестируйте только на высокопроизводительных устройствах или только debug сборки
- Не создавайте сложные иерархии представлений на заставках
- Не выполняйте ресурсоёмкие операции во время отображения заставок
- Не забывайте очищать заставочные представления и очищать кэши
Распространённые ошибки:
- Утечки памяти: невозможность очистить LottieCompositionCache
- Предположения о возможностях устройства: неадаптированность к ограничениям бюджетных устройств
- Проблемы жизненного цикла: неправильная обработка изменений состояния активити
- Конфликты анимации: анимации сплеш-скрина мешают основному контенту
- Смещение тестирования: тестирование только на быстрых устройствах или отладочных сборках
- Непонимание метрик: ориентация на длительность анимации вместо воспринимаемой пользователем производительности
- Пренебрежение мониторингом ресурсов: отсутствие мониторинга моделей использования памяти и процессора
Принятие обоснованных архитектурных решений
Структура принятия решений
При выборе между подходами к экранам-заставкам учитывайте следующее:
Демография устройств:
- Какой процент ваших пользователей используют бюджетные устройства?
- Какова ваша минимальная поддерживаемая конфигурация оперативной памяти?
- Вы ориентируетесь на развивающиеся рынки с бюджетными устройствами?
Характеристики приложения:
- Насколько сложна загрузка вашего основного контента?
- Высокая ли у вас зависимость от сети?
- Каков ваш текущий объём памяти?
Бизнес-требования:
- Насколько важны кастомные анимации на сплеш-скрине для вашего бренда?
- Можете ли вы реализовать прогрессивное улучшение?
- Каковы ваши возможности разработки и тестирования?
Рекомендуемая стратегия
Подход к прогрессивному улучшению:
EventSplashApi.attachTo(this)
.withFallback(SimpleSplashConfig()) // Low-end devices
.withStandard(ImageSplashConfig()) // Mid-range devices
.withEnhanced(LottieConfig()) // High-end devices
.adaptToDevice() // Automatic selection
.show()
Этот подход обеспечивает:
- Базовую функциональность для всех устройств
- Расширенный опыт при наличии ресурсов
- Автоматическую адаптацию к возможностям устройства
- Плавное снижение производительности при нехватке памяти
Выводы и рекомендации
Подход с неблокируемой заставкой обеспечивает значительное повышение производительности (на 90% быстрее загрузка страницы при консервативном тестировании и до 95% при сложной анимации), но и здесь есть свои недостатки. Параллельная обработка увеличивает пиковое использование памяти и нагрузку на процессор, что может быть проблематично на устройствах начального уровня.
Ключевой вывод: преимущества существенны и измеримы, но они сопровождаются затратами ресурсов, которые необходимо контролировать с помощью стратегий адаптивной реализации.
Честная рекомендация: используйте неблокируемый подход с откатами, учитывающими особенности устройства. Даже консервативные оценки показывают значительный прирост производительности, а архитектурные преимущества убедительны. Однако реализация должна быть достаточно сложной для поддержки всего спектра устройств Android.
Заключение
Этот пример демонстрирует, что оптимизация производительности требует баланса между конкурирующими ограничениями и сохранением своих заявлений. Неблокирующий подход, основанный на представлениях, обеспечивает существенные, измеримые преимущества, но для его успешного внедрения требуется глубокое понимание как выгод, так и затрат.
Отказавшись от традиционного шаблона SplashActivity и перейдя на более сложную архитектуру с параллельными процессами, мы можем создавать более быстрые и отзывчивые приложения для Android, которые надёжно работают во всей экосистеме.
Цель заключается не просто в создании более быстрых приложений, а в создании приложений, которые работают мгновенно и приятно использовать, поскольку, в конечном счёте, производительность — это характеристика, которую пользователи замечают и ценят.
-
Аналитика магазинов2 недели назад
Мобильный рынок Ближнего Востока: исследование Bidease и Sensor Tower выявляет драйверы роста
-
Интегрированные среды разработки3 недели назад
Chad: The Brainrot IDE — дикая среда разработки с играми и развлечениями
-
Новости4 недели назад
Видео и подкасты о мобильной разработке 2025.45
-
Новости3 недели назад
Видео и подкасты о мобильной разработке 2025.46




