Три недели назад я начал получать сообщения от пользователей приложения, которое мы выпустили в прошлом году.
«Приложение постоянно перезапускается после того, как я делаю снимок».
Оно не зависало и не выдавало ошибок. Просто закрывалось и перезапускалось.
Самое странное, что на моём телефоне всё работало идеально. И на устройствах моих коллег тоже.
Поначалу это казалось какой-то «призрачной ошибкой», которая проявляется только тогда, когда пользователь находится далеко, и вы не видите его экран.
Но это приложение — инструмент на основе GPS для сбора данных. Пользователи сканируют штрихкод, чтобы предотвратить дублирование активов, заполняют несколько экранов с подробными данными, делают снимки, регистрируют серийные и модельные номера, и мы сохраняем всё локально перед синхронизацией с сервером. Изображения автоматически сжимаются до 100 КБ или меньше, поэтому загрузка работает даже в медленных сетях.
Это серьёзный рабочий процесс. Не приложение типа «заполнил одну форму и отправил».
И где-то внутри этого процесса всё начало разваливаться.
Симптом
На некоторых устройствах, в основном на бюджетных телефонах Tecno, Itel и Redmi, происходило следующее:
- Пользователь делает снимок
- Приложение начинает обработку
- Изображение не сжимается
- Приложение внезапно перезапускается
Четкого сообщения об ошибке не было. Экрана сбоя не было. Просто возвращение на главный экран.
В то же время, на высококлассных устройствах Samsung и Pixel, все работало плавно. Каждый раз.
И это различие оказалось самым важным ключом к разгадке.
Реальная проблема: я проводил тестирование в изоляции
Я разработал и протестировал эту функцию на мощных устройствах с большим объемом оперативной памяти и высокой частотой процессора.
Но многие реальные пользователи использовали телефоны с жесткими ограничениями по памяти и процессорами, которые агрессивно снижали производительность для экономии заряда батареи. И мой конвейер обработки изображений незаметно потреблял память, как будто она была бесплатной.
После тщательного изучения всего процесса захвата → сжатия → сохранения я обнаружил пять отдельных проблем, которые в совокупности стали опасными.
1. Декодирование изображений блокировало основной поток
Мы использовали пакет image для синхронного декодирования фотографий в полном разрешении в основном потоке.
На флагманском устройстве с 12 ГБ оперативной памяти это всего лишь едва заметная заминка. Но на бюджетном устройстве это запускает выполнение системных команд.
Когда основной поток блокируется слишком долго, вмешивается сторожевой таймер Android, который выдает ошибку ANR (приложение не отвечает). Одновременно внезапный спрос на несжатую память для растровых изображений вызывает скачок OOM (Out Of Memory — нехватка памяти).
Операционная система просто завершает процесс, чтобы защитить остальную часть системы.
Никаких журналов ошибок. Никакой трассировки стека. Просто… пуф.
Исправление
Я перенёс декодирование изображений в фоновый изолят, используя функцию compute() Flutter. Таким образом, вместо того, чтобы тормозить пользовательский интерфейс и запускать сторожа, тяжёлая работа теперь безопасно выполняется параллельно.
2. Разрешение камеры было выше, чем нам было нужно
В одной части приложения камера инициализировалась с помощью: ResolutionPreset.high
Вот в чём ловушка: «высокое» — понятие относительное. На старых телефонах это может означать 720p. Но на многих современных устройствах по умолчанию установлено 1080p или даже 4K.
Мы часто забываем, что 2 МБ JPEG-изображения на диске сжаты. Но когда вы загружаете это изображение в память для обработки, оно становится несжатым растровым изображением.
Исходное изображение в формате 1080p или 4K на самом деле занимает не 2 МБ. Его размер рассчитывается как (Ширина × Высота × 4 байта). Для существования такого изображения может потребоваться от 15 до 40 МБ оперативной памяти.
Мы захватывали эти огромные растровые изображения только для того, чтобы потом уменьшить их размер. На устройстве с 2 ГБ оперативной памяти, где операционная система и так испытывает нехватку места, этот внезапный скачок в 40 МБ часто является разницей между успешным получением фото и немедленным сбоем.
Исправление
Я стандартизировал всё следующим образом:
ResolutionPreset.medium
Это ограничило размер исходного файла до того, как он попадал в память. В результате пользовательский опыт оставался тем же, но потребление памяти было значительно меньше.
3. Скрытая ошибка коллизии имен файлов
В одной из моделей просмотра изображений сжатые изображения записывались во временный файл с фиксированным именем.
Таким образом, если две операции происходили близко друг к другу или система повторяла что-то, возникали перезаписи файлов и состояния гонки. Иногда читаемый файл не совпадал с файлом, который мы только что записали. Иногда он уже не существовал.
Исправление
Теперь каждому сжатому изображению присваивается уникальное имя файла с меткой времени. Таким образом, больше не было коллизий или загадочных сбоев.
4. Сбои сохранения в галерее игнорировались
На некоторых устройствах сохранение изображений в галерею завершалось с ошибкой. Но ошибка игнорировалась, и приложение продолжало работу, как будто все работало. Это приводило к неправильным путям к файлам и неверным предположениям на более поздних этапах обработки.
Исправление
Правильная обработка ошибок. Если сохранение не удается, мы теперь:
- Отслеживаем это
- Записываем это в журнал
- Показываем обратную связь пользователю
Скрытые сбои могут быть опасны, особенно в многоэтапных рабочих процессах.
5. Изображения сохранялись во временное хранилище
Мы записывали сжатые изображения во временную директорию. На некоторых устройствах система удаляла эти файлы раньше, чем ожидалось.
Таким образом, последующие этапы рабочего процесса пытались прочитать файлы, которые незаметно исчезли.
Исправление
Теперь сжатые изображения сохраняются в директории документов приложения, постоянно и предсказуемо.
Результат
После этих изменений:
- Больше никаких случайных перезапусков
- Больше никаких пропавших изображений
- Больше никаких жалоб типа «у меня на телефоне не работает»
Приложение стало стабильным даже на недорогих устройствах, которые ранее падало с ошибкой.
Урок, который я извлек из этого
Это был не «баг Flutter».
Это была и не проблема «неисправного устройства».
Это была реальная проблема производительности, скрывающаяся за удобным тестовым оборудованием. Современные телефоны могут маскировать неэффективные методы работы с памятью. Но устройства ваших пользователей мгновенно их выявят.
Теперь, когда я разрабатываю что-либо, связанное с:
- Камерой
- Изображениями
- Файловым вводом-выводом
- Тяжелой обработкой
Я задаю один дополнительный вопрос: «Что происходит на телефоне с оперативной памятью, которая вдвое меньше моего?»
Потому что именно там часто скрываются настоящие ошибки.
Вы когда-нибудь пытались исправить ошибку, которая существовала только на телефонах ваших пользователей, но отказывалась проявляться на ваших?
Мне бы очень хотелось услышать эти истории, странные проблемы, специфичные для конкретных устройств, неожиданные моменты производительности, моменты «у меня на телефоне все работает», которые превратились в глубокие инженерные уроки.
Давайте учиться на ошибках друг друга.

