Skip to content

Мобильная разработка·6 мин чтения

Устранение ANR в Android: как SQLite WAL спас наше мобильное приложение

Переход с режима rollback journal по умолчанию в SQLite на Write-Ahead Logging (WAL) и настройка пулов соединений в Room устранили блокировки UI-потока на 50 000 устройств.

Анатолий НовогродскийОснователь и CEO, GlanitОпубликовано 14 сентября 2026

Что приводит к блокировке главного потока при использовании SQLite rollback journal?

Транзакции записи в фоновом режиме при стандартном для SQLite режиме rollback journal требуют эксклюзивных блокировок файла, заставляя UI-запросы в главном потоке ожидать завершения записи. Когда SQLite работает в режиме журнала по умолчанию DELETE, перед выполнением изменений он копирует немодифицированные страницы базы данных в отдельный файл журнала, что требует двух полных синхронизаций с диском на каждый коммит. На бюджетных устройствах Android с медленной флеш-памятью эти операции записи удерживают блокировку базы данных секундами. Когда Jetpack Room или прямые запросы пытаются прочитать данные в главном потоке в этот момент, интерфейсный поток замораживается и вызывает сбой Application Not Responding (ANR).

Мы столкнулись именно с этой проблемой в октябре 2023 года в FleetPulse, нашем логистическом приложении для Android, развернутом на 50 000 активных терминалах. Наши фоновые службы синхронизации регулярно загружали обновления для офлайн-каталогов, вставляя до 3 000 записей в рамках одной транзакции. В FleetPulse наблюдатели Flow из Jetpack Room связывались напрямую с UI-компонентами главного потока для отрисовки списков в реальном времени. Каждый раз при запуске фонового процесса главный поток переходил в состояние IOBlocked в ожидании освобождения блокировки записи фоновой базы данных.

Бюджетные устройства, такие как Nokia C21 и Moto E6 с медленной памятью eMMC 5.1, пострадали сильнее всего.

На этих телефонах время удержания блокировки записи регулярно превышало 5 000 миллисекунд из-за всплесков задержек дискового ввода-вывода при температурном троттлинге. Поскольку диспетчеризация ввода в Android OS завершается по таймауту через 5 секунд нечувствительности, панель Google Play Console зафиксировала тысячи ANR за одну неделю. Мы поняли, что архитектура нашей базы данных должна поддерживать честную изоляцию читателей и писателей без голодания запросов в главном потоке.

Как режим SQLite WAL отличается от режима Rollback Journaling?

SQLite Write-Ahead Logging (WAL) заменяет откатные журналы (rollback journals) путем добавления измененных страниц в отдельный лог-файл, оставляя основной файл базы данных незатронутым во время активной записи. Разделение процессов позволяет параллельным читателям получать данные из основного файла базы данных и индекса в общей памяти без ожидания снятия блокировок записи. В отличие от режимов DELETE или TRUNCATE, где читатели и писатели полностью блокируют друг друга, изоляция в WAL обеспечивает непрерывное выполнение UI-запросов на мобильном оборудовании.

Различия между этими режимами SQLite напрямую влияют на паттерны дискового ввода-вывода, коэффициенты разрастания записи (write amplification) и блокировку потоков в продакшен-среде мобильных приложений.

Сравнение SQLite Rollback Journal и Write-Ahead Logging (WAL) в мобильной среде
Функция / МетрикаRollback Journal (DELETE / TRUNCATE)Write-Ahead Logging (WAL)
Параллелизм (Concurrency)Эксклюзивный (писатели блокируют читателей; читатели блокируют писателей)Параллельный (писатели не блокируют читателей; читатели не блокируют писателей)
Безопасность чтения в главном потокеНебезопасно (задержки во время синхронизации фоновой записи)Безопасно (чтение происходит непрерывно из файла базы данных и индекса SHM)
Производительность записиМедленнее (требуется 2 вызова fsync на коммит)Быстрее (последовательное добавление в WAL-файл; 1 вызов fsync на коммит)
Профиль I/O дискаСлучайная запись по страницам основного файла базы данныхПоследовательная запись путем добавления в конец файла .db-wal
Накладные расходы памяти / файловМинимальные (один файл базы данных + временный журнал)Требует сопутствующие файлы .db-wal и .db-shm в памяти
Сравнение SQLite Rollback Journal и Write-Ahead Logging (WAL) в мобильной среде

Как настроить режим WAL и пользовательские пулы соединений в Room?

Чтобы включить SQLite Write-Ahead Logging и настроить пулы соединений потоков в Android Jetpack Room, установите режим журнала в JOURNAL_MODE_WRITE_AHEAD_LOGGING у вашего экземпляра RoomDatabase.Builder. Эта настройка выполняет PRAGMA journal_mode=WAL; при первоначальной установке соединения, создавая необходимые файлы -wal и -shm в хранилище устройства. Затем вы можете подключить пользовательский пул исполнителей и настроить параметры SQLite PRAGMA во время выполнения внутри колбэка базы данных.

Вот наша рабочая конфигурация для настройки Room с поддержкой WAL и пулингом потоков:

@Database(entities = [UserEntity::class, SyncRecordEntity::class], version = 4, exportSchema = true)
abstract class AppDatabase : RoomDatabase() {
    abstract fun userDao(): UserDao
    abstract fun syncDao(): SyncDao

    companion object {
        @Volatile
        private var INSTANCE: AppDatabase? = null

        fun getInstance(context: Context): AppDatabase {
            return INSTANCE ?: synchronized(this) {
                val instance = Room.databaseBuilder(
                    context.applicationContext,
                    AppDatabase::class.java,
                    "fleetpulse_production.db"
                )
                .setJournalMode(JournalMode.WRITE_AHEAD_LOGGING)
                .enableMultiInstanceInvalidation()
                .setQueryExecutor(Executors.newFixedThreadPool(4))
                .setTransactionExecutor(Executors.newSingleThreadExecutor())
                .addCallback(object : RoomDatabase.Callback() {
                    override fun onOpen(db: SupportSQLiteDatabase) {
                        super.onOpen(db)
                        db.execSQL("PRAGMA synchronous = NORMAL;")
                        db.execSQL("PRAGMA busy_timeout = 3000;")
                    }
                })
                .build()

                INSTANCE = instance
                instance
            }
        }
    }
}

В ноябре 2023 года наша команда платформы изначально попыталась установить PRAGMA synchronous = OFF;, чтобы выжать максимальную пропускную способность записи при фоновой синхронизации. Это было ошибкой. Мы потеряли несколько локальных записей во время стресс-тестирования с разрядкой аккумулятора, когда устройства неожиданно отключались в середине синхронизации. Переход на PRAGMA synchronous = NORMAL; сохранил надежность при сбоях, ограничив синхронизацию с диском границами чекпоинтов WAL.

Почему изоляция читателей и писателей в WAL устраняет конфликты в UI?

Изоляция читателей и писателей в WAL предотвращает конфликты в UI-потоке за счет изолирования операций записи базы данных в файле лога с дозаписью в конец, пока читатели запрашивают основной файл с использованием индекса в общей памяти. Когда приложение записывает новые записи в режиме WAL, SQLite добавляет эти правки в отдельный файл <database>-wal вместо изменения основного файла .db. Параллельные запросы чтения сопоставляют расположение страниц с помощью вспомогательного файла <database>-shm, что позволяет главному потоку отрисовывать компоненты UI без ожидания активных транзакций записи.

Вместо блокировки всего файла базы данных при записи SQLite управляет трафиком через отдельные фоновые каналы. Писатели последовательно добавляют изменения в файл журнала и обновляют индекс в общей памяти. В это же время читатели проверяют тот же индекс в общей памяти, чтобы найти наиболее свежую версию страницы. Если в WAL-файле существует более новая версия страницы, SQLite возвращает ее напрямую; в противном случае чтение происходит из оригинального файла базы данных.

Эта структура гарантирует, что операции добавления на диск в фоновых потоках никогда не перехватывают и не задерживают запросы чтения в главном потоке, выполняемые драйверами Jetpack Room.

Что предотвращает чрезмерный рост файла SQLite WAL?

Неконтролируемый рост WAL-файла сдерживается с помощью автоматических операций чекпоинта (checkpointing), которые переносят зафиксированные страницы WAL обратно в основной файл базы данных. SQLite автоматически запускает пассивные чекпоинты, когда журнал WAL достигает 1 000 страниц (приблизительно 4 МБ). Однако, если длительные транзакции чтения удерживают открытые хэндлы базы данных, пассивные чекпоинты не могут завершиться, что приводит к разрастанию WAL-файла и замедлению чтения со временем.

Чтобы предотвратить разрастание хранилища на устройствах клиентов, мы запускаем фоновую задачу WorkManager по расписанию в периоды простоя приложения для проведения явных чекпоинтов WAL:

class DatabaseMaintenanceWorker(
    context: Context,
    params: WorkerParameters
) : CoroutineWorker(context, params) {

    override suspend fun doWork(): Result = withContext(Dispatchers.IO) {
        return@withContext try {
            val db = AppDatabase.getInstance(applicationContext).openHelper.writableDatabase
            
            val cursor = db.query("PRAGMA wal_checkpoint(PASSIVE);")
            if (cursor.moveToFirst()) {
                val busy = cursor.getInt(0)
                val logPages = cursor.getInt(1)
                val checkpointedPages = cursor.getInt(2)
                Log.d("FleetPulseWAL", "Checkpoint - Busy: $busy, Log: $logPages, Checkpointed: $checkpointedPages")
            }
            cursor.close()
            
            db.execSQL("PRAGMA wal_checkpoint(TRUNCATE);")
            Result.success()
        } catch (e: Exception) {
            Log.e("FleetPulseWAL", "WAL maintenance failed", e)
            Result.failure()
        }
    }
}

Выполнение wal_checkpoint(TRUNCATE) во время неактивности пользователя сбрасывает размер файла -wal обратно до 0 байт и поддерживает высокую скорость поиска страниц на протяжении длительных сеансов работы.

Какие метрики в продакшене подтвердили снижение числа ANR?

Данные мониторинга из Firebase Crashlytics и Google Play Console на 50 000 активных устройствах показали снижение общего количества сбоев ANR на 98% в течение двух недель после внедрения режима WAL. Время ожидания базы данных в главном потоке во время фоновой синхронизации снизилось с пикового значения 99-го перцентиля в 6 200 мс до менее чем 12 мс. Общий уровень ANR приложения стабилизировался на отметке 0,04% от ежедневных активных пользователей, что значительно ниже порога негативного поведения Google Play в 0,47%.

Влияние на производительность приложения

  • Ежедневный уровень ANR снизился с 2,15% до 0,04% на всех активных устройствах
  • Время блокировки базы данных в главном потоке (99-й перцентиль) сократилось с 6 200 мс до менее чем 12 мс
  • Пропускная способность пакетной фоновой записи при синхронизации увеличилась в 3,8 раза
  • Частота пропусков кадров (slow frames) снизилась на 32% во время активной сетевой синхронизации

Частые вопросы

Ещё статьи

Мобильная разработка ·

Почему сериализация моста принудила к переписыванию приложения на Swift

Передача BLE-телеметрии на частоте 100Hz через устаревший мост React Native перегружала event loop, что вынудило полностью переписать приложение на нативный Swift.

6 мин чтения

Облака, DevOps и высокие нагрузки ·

Почему мы ушли с AWS Aurora PostgreSQL на managed bare metal и сэкономили 58%

Миграция высоконагруженной БД PostgreSQL размером 12TB с AWS Aurora снизила наш месячный счет на 58%. Представляем технический разбор расходов на storage I/O и стратегии переключения.

6 мин чтения

ИИ и машинное обучение ·

Почему гибридный поиск обошел чистые эмбеддинги в нашем пайплайне Invoice AI

Чистые плотные эмбеддинги не справились с буквенно-цифровыми серийными номерами в нашем пайплайне Invoice AI. Объединение полнотекстового поиска PostgreSQL tsvector с косинусным расстоянием pgvector подняло точность совпадений top-1 с 61,4% до 94,8%.

6 мин чтения