Управление памятью битовых карт критически важно для снижения риска нехватки памяти в приложениях Android с большим количеством визуальных элементов. Разработчики знают, что нужно уменьшать размер изображений, но для долгосрочной стабильности приложения необходимо освоить ещё два ключевых принципа: повторное использование и утилизацию. На этом занятии вы узнаете, как оптимизировать использование памяти с помощью современных инструментов, библиотек с открытым исходным кодом, таких как Glide и Coil, и системных API.
На этом занятии вы узнаете, как диагностировать реальные регрессии памяти с помощью Android Vitals, реализовать расширенные шаблоны кеширования (повторного использования) с помощью Glide и Coil, а также упреждающе освобождать память (повторно использовать) с помощью обратного вызова onTrimMemory.
BitmapRegionDecoder, вместо того чтобы загружать слишком большие изображения в полном разрешении.onTrimMemory. Остановите фоновые опросы и анимации в onStop, чтобы предотвратить рост объема памяти, используемой в фоновом режиме.Расшифровка
Фун Лам (Fung Lam)
Да, до обеда остался примерно час, так что оставайтесь со мной.
Меня зовут Фунг Лам, я инженер по связям с разработчиками Android в Лондоне.
Да, спасибо.
Прежде чем начать, могу я задать вопрос?
Кто из вас купил новый телефон в этом году?
Вот он,
Вот он,
Скажи честно, я не буду тебя винить, если ты не купишь Pixel.
Ну, новый телефон дороже, верно?
Да, именно поэтому некоторые из вас не купили новый телефон.
Почему цена выше?
Знаете ли вы?
Нет.
ОЗУ.
ОЗУ, да, да.
Да, ОЗУ – это действительно проблема для Android и для нас, разработчиков.
Поэтому сегодня я расскажу, как мы можем решить проблему с памятью, с ОЗУ.
Ранее на Google I/O и Google I/O Connect мы провели несколько сессий, посвященных памяти: от того, как операционная система выделяет память, до того, как вы можете профилировать свое приложение, как вы можете оптимизировать свой код с помощью R8, и, конечно, в то время я немного рассказал о Bitmap.
На скриншоте видны мои фотографии.
Оптимизация памяти – это обширная тема, которая охватывает разные области, такие как Android Runtime, сборка мусора, инструменты для повышения производительности, распределение памяти для объектов, компилятор и т. д. В этом 30-минутном докладе я расскажу, как оптимизировать память для растровых изображений.
Вы можете спросить, почему, почему Bitmap?
Почему нужно обращать внимание на использование памяти битовыми картами?
Потому что одно изображение стоит тысячи слов.
Это правда, и я объясню почему.
На экране телефона, например Pixel 7, Pixel 9 или Pixel 11 Pro, разрешение экрана составляет около 2500 x 1200 или 1400.
Таким образом, для каждого пикселя мы имеем 32 бита глубины цвета.
Так, для изображения в полноэкранном режиме, ой, у меня не хватает пальцев, но, например, это займет около 15 МБ памяти, 50 МБ.
Итак, но, например, слово в системе ОС похоже на 64-битное, это означает, что это 8 байт.
Тысяча слов – это всего лишь 8 МБ.
Да, 8K.
Так что одно изображение определенно стоит больше тысячи слов.
Да, это важно.
Размер одного изображения уже составляет 15 МБ.
А если в приложении 10 или 100 изображений?
Это большое число.
Так что на самом деле Bitmap всегда, обычно является самым большим потребителем памяти в вашем приложении.
Когда изображение декодируется в память, как я уже говорил, оно расширяется до размера и глубины цвета.
Это не размер файла, а его формат – JPEG или PNG.
Нет, он будет декодирован и займет память.
Поэтому всего несколько изображений в полном разрешении могут легко занять сотни мегабайт памяти на вашем устройстве.
Во-вторых, когда памяти очень мало, операционной системе приходится тратить больше энергии на управление ее потреблением.
Это может привести к тому, что интерфейс будет выглядеть неаккуратно.
или завершить работу приложения, чтобы освободить память.
В противном случае в приложении может возникнуть ошибка или исключение Out-Of-Memory.
К счастью, есть и хорошие новости: этот циферблат обычно пользуется огромным успехом при минимальных усилиях.
Большинство скачков памяти происходит из-за распространенной ошибки, которой можно избежать, и мы могли бы исправить это без какого-либо компромисса в отношении качества изображения.
Теперь вы знаете, что память битовых карт важна для вашего приложения не только для обеспечения его бесперебойной работы, но и для стабильности и удержания пользователей.
Сегодня мы рассмотрим пять простых шагов, которые помогут обеспечить бесперебойную работу вашего приложения.
Сначала мы рассмотрим техническое качество в Google Play и новые требования к нему, в том числе предстоящее ограничение на использование памяти и то, где его можно проверить.
Во-вторых, я поделюсь примером из своего исследования 500 приложений для Android, в котором я изучал, как они используют память, правильно ли они это делают и можем ли мы чему-то у них научиться.
Затем я покажу вам, как оптимизировать использование памяти битовыми картами.
В конце мы расскажем об инструментах, которые помогут вам проверить свое приложение.
Фун Лам (Fung Lam)
Во-первых, это новые требования к качеству в Google Play.
Как часто вы посещаете Google Play Console?
Каждый месяц?
Каждую неделю?
Никогда?
Смех означает "да", верно?
Нет?
Нет.
Вы посетили раздел Android Vitals?
Да, да.
Да, так что вы точно знаете что-то, прежде чем я это сделаю.
Поэтому в Google Play появились новые требования к качеству, связанные с пороговыми значениями производительности.
Мы хотим, чтобы пользователи Android и Google Play получали только качественные приложения.
Поэтому мы стараемся сделать так, чтобы конечный пользователь получил наилучший результат.
Простыми словами, когда ваше приложение работает в фоновом режиме или в активном режиме, 90-й процентиль использования памяти не должен превышать этот порог.
В противном случае видимость вашего приложения в Google Play может быть снижена".
Новые правила начнут действовать в феврале 2027 года.
Превышение этого порога напрямую влияет на оценку работоспособности приложения.
Поэтому обязательно зайдите в Google Play Console и проверьте, каков ваш 90-й процентиль сегодня.
и внесите улучшения до указанного срока.
Это анимация.
"Если вы откроете Google Play Console, то увидите Android Vitals.
Это часть памяти.
и 90-й процентиль.
Если нажать на него, можно увидеть среднее потребление памяти за определенный период.
Также есть разные уровни по ОЗУ, типу устройства, плотности экрана и версии ОС, которые помогают изолировать узкие места в памяти на определенных устройствах начального уровня или в определенных выпусках приложений.
Теперь я знаю, что вы можете сказать: "Эй, спасибо, что сказали мне, что мои приложения превышают пороговое значение.
Но я правда не знаю, что не так.
Поможешь?"
Да, поэтому я здесь, чтобы помочь вам.
Я постараюсь объяснить это простыми словами. Представьте, что вы смотрите телевизор и вдруг пропадает изображение или звук. Что вы сделаете?
Вы достаёте инструкцию к телевизору, открываете последние страницы, где обычно находится руководство по устранению неполадок, и читаете: «Если у телевизора нет звука, нажмите эту кнопку или ту кнопку».
Да, сегодня я тоже столкнулся с распространенной проблемой.
Надеемся, что это руководство по устранению неполадок.
Обычно, когда мы читаем руководство к телевизору, мы должны начинать с первой страницы, на которой обычно находятся диагностические инструменты, которые действительно помогают понять, как работает телевизор.
Но теперь я знаю, что это менее интересно, поэтому сегодня, пожалуйста, позвольте мне сначала перейти к устранению неполадок, а мы вернемся позже.
И, честно говоря, именно так я читаю руководство к телевизору.
Чтобы выявить наиболее частые проблемы с выделением памяти для растровых изображений в экосистеме Android, я изучил около 500 популярных приложений.
Я запускаю приложение, вхожу в аккаунт, перехожу на разные страницы, изучаю их и возвращаюсь назад.
Я тестирую приложение на устройствах с разным объемом ОЗУ, например на устройстве Android Go с 2 ГБ памяти, на устройстве начального уровня с 4 ГБ, на устройстве среднего уровня с 4, 6 или 8 ГБ, а также на складном устройстве премиум-класса с очень большим экраном, очень хорошим качеством экрана и 16 ГБ памяти.
Мы измерили использование памяти на этом устройстве в разных состояниях приложения, зафиксировали дамп кучи и использовали разные инструменты, включая Perfetto AI Skill, чтобы проанализировать распределение и выявить распространенные антипаттерны, то есть распространенные ошибки, которые мы совершаем в больших масштабах.
Вот как меняется память в разных состояниях активности.
И с помощью этого анализа мы выявляем более 15 распространенных проблем, начиная от слишком большого размера изображения и заканчивая увеличением фонового кэша.
Это основная история успеха, которую мы сегодня рассмотрим.
Фун Лам (Fung Lam)
Да, все кейсы, которыми я сейчас делюсь, основаны на исследовании 500 приложений.
Раз вы здесь, значит, вы любите Android, верно?
Мы все любим Android.
Допустим, кто-то из нас тоже любит Android, но не является экспертом в разработке для Android.
Он создал приложение для Android, в котором можно заказать фигурку Android.
Но есть некоторые ошибки.
Давайте поговорим. Давайте посмотрим, может быть, это первая ошибка.
"На странице товара у нас есть макет с двумя столбцами, в которых показаны разные фигурки Android.
Размер изображения в каждом столбце составляет около 400 x 400 пикселей.
Однако на самом деле приложение скачивает с сервера одно изображение, независимо от размера экрана.
Скачивается изображение размером 3000 x 3000 пикселей.
Да, потому что разработчик говорит: "Android – это так здорово, так замечательно.
Я хочу показывать пользователям только лучшее".
Но на самом деле нет, потому что, например, эй, он ограничен размером представления изображения.
Поэтому даже если вы скачаете большое изображение, оно не будет выглядеть наилучшим образом.
Кроме того, это большое изображение занимало дополнительную память.
Даже если мы уменьшим разрешение, этот процесс также потребует дополнительной памяти.
Как помочь разработчику исправить проблему?
Если вы знаете, что такое CDN, то вам известно, что она может выполнять некоторые операции с изображениями, например кадрирование, изменение размера, поворот, применение фильтра, преобразование квадратного изображения в круглое.
CDN может обрабатывать HTTP-запросы.
Поэтому, даже если на вашем сервере размещено только одно изображение, вы можете использовать CDN для отправки на устройство изображения с измененным размером.
Итак, во-первых, да, когда вы запрашиваете изображение, попробуйте использовать CDN, чтобы вы запрашивали изображение в размере ImageView.
Но если это невозможно, например, если изображение иногда может быть немного больше, чем ImageView, не забудьте уменьшить его размер в коде.
Поэтому, когда оно загружается в память, загружается меньшее изображение, а не изображение с полным разрешением.
Ниже приведен пример кода для Coil.
// 1. Ask CDN for the exact UI card dimensions (e.g. 540x675)
val sizedUrl = "$cdnBaseUrl?w=$cardWidthPx&h=$cardHeightPx&fmt=webp"
// 2. Enforce exact downsampling + Hardware Bitmap config
AsyncImage(
model = ImageRequest.Builder(LocalContext.current)
.data(sizedUrl)
.size(cardWidthPx, cardHeightPx) // Never use Size.ORIGINAL!
.precision(Precision.EXACT) // Downsample to exact card size
.bitmapConfig(Bitmap.Config.HARDWARE)
.build(),
contentDescription = product.title
)Вы можете передать размер в виде текста, выделенного желтым цветом.
Да, вы можете передать размер в HTTP-запросе.
А затем в выделенном коде (вы видите выделение? Да, отлично) не используйте Size.ORIGINAL.
Да, используйте ширину и высоту, чтобы они передавались в качестве параметра.
Используйте точность, чтобы уменьшить изображение до нужного размера.
По возможности используйте Bitmap.Config.HARDWARE, так как они хранятся непосредственно в видеопамяти и занимают меньше места.
Аналогично, в этом приложении, скажем, они выпускают новую версию коллекционных фигурок Android, поэтому есть длинное изображение, которое пользователь может пролистать влево и вправо, чтобы увидеть другой дизайн новой фигурки Android.
Но есть и другая проблема.
Для длинного изображения большая его часть на самом деле находится за пределами экрана, что означает, что пользователь не может его увидеть, но изображение загружается в память.
Можно ли сделать лучше?
Да.
Мы можем разбить его на более мелкие изображения и показывать только то, что загружается на экране.
Те, что находятся за пределами экрана, не загружаются или выгружаются.
Это позволит снизить использование памяти.
Аналогично, в этом случае было сэкономлено около 60 % памяти.
Конечно, если вы обрежете его до меньшего прямоугольника, вы сэкономите больше.
Но если вы хотите предоставить пользователю возможность приблизить изображение, чтобы рассмотреть каждую деталь фигурки Android, то распространенной ошибкой будет загрузка очень большого изображения, которое можно приближать снова и снова.
Но нет, вы не обязаны это делать.
Лучше, когда пользователь увеличивает исходное уже загруженное изображение с низким разрешением, и когда пользователь перестает сжимать, вы вычисляете координату, а затем загружаете полное разрешение - загружаете только эту область, вычисленную область из изображения с высоким разрешением.
Поэтому в память загружается только видимая область.
Как это сделать?
О, есть API под названием BitmapRegionDecoder, который помогает загрузить определенную область изображения.
И, конечно, есть инструменты и библиотеки, которые могут вам помочь.
Вы можете найти его на GitHub.
Некоторые приложения с комиксами или картами уже используют эту функцию.
Вы можете использовать их в качестве примера.
Кроме того, мы люди, и мы совершаем ошибки.
ИИ также может ошибаться.
Чтобы избежать ошибок, мы можем добавить ограничение на загрузку изображений.
Например, в этом случае я мог бы сказать: "Эй, давайте использовать размер окна или размер экрана в качестве ограничения и поместим его в функцию min, min ширины запроса или ширины границы".
// Query active window bounds instead of hardcoding 1080 / 1920
val bounds = WindowMetricsCalculator.getOrCreate()
.computeCurrentWindowMetrics(activity).bounds
// Clamp target dimensions so Bitmap NEVER exceeds the active window
val safeWidth = minOf(requestedWidthPx, bounds.width())
val safeHeight = minOf(requestedHeightPx, bounds.height())
val bitmap = Bitmap.createBitmap(safeWidth, safeHeight, config)Итак, с этой функцией, когда бы я ни загружал изображение, оно никогда не будет выходить за границы.
Поэтому изображение не будет загружаться в разрешении, превышающем разрешение экрана.
Помните, что декодировать нужно только видимую область изображения. Это позволит сэкономить память.
Фун Лам (Fung Lam)
И снова, скажем, это приложение на самом деле не от одного разработчика.
На самом деле, это для нескольких участников, и они могут использовать разные инструменты.
Например, часть из них использует, ой, извините.
Попробую ещё раз.
То же самое и с этим приложением.
Я на странице товара, вижу список товаров и это изображение. Добавляю его в корзину.
Через несколько минут я перехожу к оформлению заказа, и снова появляется то же изображение.
Но действительно ли это одно и то же изображение или на самом деле это два разных объекта в памяти?
Да, одна из распространенных ошибок, которые мы обнаружили, заключается в том, что приложение загружает много изображений.
Однако в памяти также много дублирующихся изображений.
Если вы используете загрузчик изображений, то да, вы можете передать ключ для кеширования.
Поэтому, когда используется одно и то же изображение, вы можете использовать уже выделенный объект Bitmap, а не создавать новый.
В Android Studio можно использовать профилировщик памяти, чтобы узнать, есть ли в приложении дублирующиеся растровые изображения.
Я рассказывал об этом на конференции Google I/O, так что вы можете посмотреть видео на YouTube.
Если вы не используете Android Studio, то можете получить дамп кучи с помощью команды adb, а затем использовать язык SQL для графа кучи Perfetto, чтобы проверить, есть ли в приложении дублирующиеся растровые изображения.
Вот пример кода в Coil, показывающий, как добавить ключ для кеширования растровых изображений.
// Strip volatile query tokens (?from=list / ?sig=...) for cache key
val canonicalKey = MemoryCache.Key(productUrl.substringBefore("?"))
// On Checkout Screen: reuse the 200x200 Bitmap already loaded in the List!
AsyncImage(
model = ImageRequest.Builder(context)
.data(productUrl)
.placeholderMemoryCacheKey(canonicalKey) // Instant zero-decode reuse!
.memoryCacheKey(canonicalKey)
.diskCacheKey(productUrl.substringBefore("?"))
.build(),
contentDescription = product.title
)А теперь – да.
Вернемся к предыдущему примеру.
У моего приложения другой разработчик.
В некоторых местах используются представления, поэтому в качестве загрузчика изображений используется Glide.
А некоторые из них используют Coil, потому что они используют Compose.
Вы упомянули, что загрузчик изображений помогает выполнять кеширование. Будет ли проблемой, если у меня несколько загрузчиков?
Определенно, что-то не так.
По умолчанию загрузчик изображений использует X % от размера кучи в качестве размера кеша.
Если у вас один загрузчик изображений, он использует X % памяти.
Если у вас два загрузчика изображений, то объем памяти увеличится в два раза.
Если у вас три загрузчика изображений, то в три раза.
Если загрузчик изображений не использует тот же пул, произойдет то же самое.
Например, на странице товара я загружаю изображение с помощью Glide, а на странице оформления заказа то же изображение будет загружено с помощью Coil.
В воспоминании останется два изображения.
Это нехорошо.
Да, поэтому, чтобы исправить это, в идеале мы просто используем один загрузчик изображений или один общий пул для кэширования.
Но что, если это, например, много инженерной работы, я не смогу этого сделать.
О, более простое решение будет таким: я измеряю, сколько процентов изображения будет обработано этим загрузчиком изображений, сколько процентов изображения будет обработано другим загрузчиком изображений, и устанавливаю вес для разных загрузчиков изображений, как в этом коде.
val totalBudgetBytes = computeAdaptiveCacheBytes(context)
val numberOfLoaders = 2 // e.g., Team A (Glide) + Team B (Coil)
// Approach A (With Stats): Weight by image traffic (70% Team A / 30% Team B)
val loaderABudget = (totalBudgetBytes * 0.70).toInt()
val loaderBBudget = (totalBudgetBytes * 0.30).toInt()
// Approach B (Simple Fix without stats): Divide by number of loaders!
val simpleSplitBudget = totalBudgetBytes / numberOfLoaders
MemoryCache.Builder()
.maxSizeBytes(simpleSplitBudget)
.build()Можно задать loaderABudget равным 70 %, а loaderBBudget – 30 %.
Если вы не знаете, сколько изображений будет обрабатывать каждый загрузчик, лучше просто разделить их.
Если у меня два загрузчика изображений, я делю на 2, если три – на 3, а не использую значение по умолчанию, потому что это приведет к чрезмерному использованию памяти.
И, допустим, мы ожидаем, что на странице товара пользователь будет прокручивать страницу, чтобы увидеть больше информации о товаре.
Обычно мы кэшируем около 50 изображений или 50 МБ изображений.
Но что, если приложение перейдет в фоновый режим?
Если приложение работает в фоновом режиме, пользователь может не просматривать его.
Если пользователь не просматривает страницы, нужно ли хранить в памяти такое количество изображений из кеша?
Нет, мы должны его выпустить.
Да, мы должны учитывать обратный вызов onTrimMemory.
Да, на самом деле многие приложения сейчас не реализуют или не учитывают обратный вызов onTrimMemory.
В обратном вызове onTrimMemory нужно обратить внимание на два сигнала.
Первый – UI_HIDDEN. Он означает, что пользователь нажал кнопку главного экрана, но не сразу после этого, а через некоторое время. Это значит, что пользовательский интерфейс приложения не виден.
И мы могли бы ожидать: "Эй, пользователь не использует ваше приложение.
Освободите ресурсы для ОС".
Поэтому нам нужно уменьшить размер кеша.
Другой – TRIM_MEMORY_BACKGROUND. Он означает, что приложение уже долго находится в фоновом режиме и операционной системе нужны ресурсы.
На этом этапе вы должны освободить свою память как можно больше, почти на 100%.
Это также снижает риск того, что ОС закроет ваше приложение.
Потому что, например, "Эй, им нужна память, вы им даете, а потом ОС с этим справляется".
Если нет, то у ОС нет выбора, ей нужно что-то сделать.
Фун Лам (Fung Lam)
Ещё одна распространенная ошибка – когда приложение, работающее в фоновом режиме, использует слишком много памяти.
Однако я заметил, что приложения действительно используют больше памяти в фоновом режиме.
Почему?
Вот почему я провожу исследования.
Я попытался выяснить, почему.
И одна из причин – "В их приложении есть анимация или карусель.
Например, они скачивают новое изображение каждые 15 секунд и показывают его пользователю".
Однако разработчик не остановил анимацию, когда приложение переходит в фоновый режим.
То есть даже когда приложение работает в фоновом режиме, код продолжает выполняться, получая и декодируя новые изображения и используя больше памяти.
Это не то, что должно делать приложение.
Ага.
Когда приложение переходит в фоновый режим, необходимо остановить всю анимацию, кеширование и другие не связанные с ним процессы.
Если вы знакомы с жизненным циклом Activity, то знаете, что есть события onPause и onStop. Обязательно реализуйте onStop, onPause и onDestroyView, чтобы останавливать анимацию, pull-запросы на получение изображений и другие действия, не требующие внимания пользователя.
override fun onStop() {
super.onStop()
// 1. Stop animations so they don't decode next frames in background!
bannerAnimationView.cancelAnimation()
animatedImageDrawable?.stop()
// 2. Cancel next-image buffering / prefetch queues!
nextImagesBufferingJob?.cancel()
Glide.with(this).pauseRequests()
}
override fun onDestroyView() {
super.onDestroyView()
bannerAnimationView.clearAnimation() // Release resources!
}Фун Лам (Fung Lam)
Я знаю, что мы уже обсуждали много дел, но я сказал: "У меня больше 15 дел".
Если я расскажу вам обо всех случаях, вы можете все забыть.
Вы просто вернетесь домой и введете в Google: "Как решить проблемы с памятью?"
Поэтому я помогаю вам запомнить.
Ага.
Я свяжу его с очень известной структурой 3R, чтобы помочь вам запомнить их.
Думаю, все слышали о концепции 3R.
Первый – это уменьшение объема памяти, используемой при загрузке изображения.
Конечно, как я уже говорил, мы можем уменьшить разрешение и получить изображение нужного размера с сервера.
Мы оптимизируем глубину цвета или используем аппаратную битовую карту, если это возможно.
Если вы не используете CDN, попробуйте начать.
Поэтому, когда мы получаем изображение с сервера, мы просто получаем изображение, которое лучше всего подходит по размеру.
Повторное использование. Мы используем кеш, чтобы загруженное в память изображение использовалось повторно и в памяти не было дубликатов битовых карт.
И, конечно, старайтесь не использовать в коде несколько загрузчиков изображений, так как это менее эффективно.
Сокращайте, используйте повторно и… что ещё?
Знаете ли вы?
Переработка.
Повторить попытку?
Переработка, да, переработка.
Да, мы узнали о сокращении, повторном использовании и переработке в начальной школе, хотя мы не изучаем программирование в начальной школе.
Да, переработка, да.
Да, речь идет о том, как освободить память, чтобы другие приложения или ОС могли ее использовать.
Там есть вещи, которые можно переработать.
Во-первых, я упомянул, что есть событие onTrimMemory, которое сообщает, что ОС отправит вам два сигнала, когда ваш пользовательский интерфейс скрыт или ОС действительно нужны ресурсы.
Да, попробуй выполнить этот запрос.
И, конечно, если у вас есть анимация, загрузчик изображений или что-то ещё, что происходит на переднем плане, обязательно остановите это, когда происходит событие onPause, onStop.
Поэтому сокращайте, используйте повторно, перерабатывайте.
Легко запомнить, правда?
Фун Лам (Fung Lam)
Теперь вернемся к менее интересной части руководства по телевизору.
Как использовать разные инструменты для эффективного обнаружения и измерения этой проблемы с памятью?
Первый – это Google Play Console, Android Vitals и Reporting API.
Да, посмотрите 90-й процентиль в Google Play Console.
Что это за место?
Если вы не посещаете веб-страницу, используйте Reporting API.
Вы по-прежнему можете использовать API, чтобы получить значение.
Да, убедитесь, что пороговое значение составляет 200 МБ и 400 МБ.
Убедитесь, что вы не превысили пороговое значение.
Также есть команды ADB, которые помогают, например, создать дамп кучи или отправить событие, чтобы симулировать, что приложение находится на этапе UI_HIDDEN.
adb shell dumpsys meminfo <pkg> # watch Native Heap!
adb shell am send-trim-memory <pkg> UI_HIDDENЕсли вы используете Android Studio, отлично.
Но он всегда будет вашим хорошим другом.
Вы можете использовать профилировщик Android Studio, чтобы получить дамп кучи и увидеть в интерфейсе, как используется и распределяется память.
И, конечно, мы говорим об ИИ с Gemini в Android Studio или других доступных навыках Android.
Она помогает анализировать дамп кучи и использовать трассировки Perfetto SQL, чтобы найти битовую карту и определить владельца кеша.
Загружается ли мое изображение с помощью Glide или Coil?
Есть ли в моем приложении другие утечки памяти?
Я знаю, что некоторые из вас скажут: "Эй, на самом деле я больше не использую IDE.
Я специалист по составлению запросов.
Да, я просто отправляю запрос ИИ-агенту, чтобы он написал код".
Да, я знаю.
В свободное время я также занимаюсь вайб-кодингом.
Я делюсь этим с вами в качестве примера.
Да, это только часть запроса, который я использую для исследования 500 приложений.
Да, только часть.
Вы знаете, что экран не большой.
Если бы я добавил больше, вы бы его едва увидели.
Но это дает вам представление о том, как можно использовать ИИ для автоматизации, фиксации использования памяти, включая дамп кучи, а также для анализа и предоставления вам результатов.
И да, я могу сказать здесь, ещё в одном предложении, да, "Пожалуйста, также предоставьте мне исправление кода для моего кода".
Да, надеюсь, это поможет вам меньше бояться. Скажите себе: "Память – это большая область.
В университете я не изучал операционные системы и архитектуру компьютерных систем.
Но на самом деле это не так страшно и не так сложно".
Да, это упрощает анализ приложения.
Попробуйте.
Но, конечно, ИИ будет ошибаться, поэтому фундаментальные знания о том, как используется память, а также о том, как ОС распределяет память, по-прежнему важны.
Поэтому приходите на мое выступление и посмотрите наше видео на YouTube.
Фун Лам (Fung Lam)
Итак, подведем итоги.
Во-первых, сократите.
Убедитесь, что размер изображения правильный.
Вы разбиваете изображение на более мелкие, чтобы не загружать все в память, да, просто чтобы загрузить в память изображение, которое видно пользователю.
По возможности используйте аппаратные ключи bitmapConfig или RGB_565.
Повторное использование. Убедитесь, что у вас есть стратегия кеширования, и используйте загрузчик изображений, чтобы уменьшить количество дублирующихся битовых карт в памяти.
И, конечно, не используйте несколько растровых изображений, даже если вы пытались перезаписать размер кеша по умолчанию для этих загрузчиков изображений.
И мы не используем повторно, не перерабатываем.
Ага.
Управление памятью должно адаптироваться к состоянию приложения.
Если интерфейс скрыт, то да, памяти должно использоваться меньше.
Если ОС действительно запрашивает освобождение памяти, освободите как можно больше.
и перестать тратить ресурсы, будь то битовая карта, энергия или вычислительные ресурсы.
Да, остановите таймер баннера, анимацию и объединение изображений.
И наконец, не забывайте отслеживать результаты.
Регулярно заходите в Google Play Console.
Убедитесь, что это тренировка.
Да, это поможет вам понять, каково текущее состояние 90-го процентиля использования памяти вашего приложения среди всей пользовательской популяции.
Это также поможет вам выявить проблему на ранней стадии.
Ага.
Для локального профилирования можно использовать профилировщик памяти Android Studio.
Мы также можем использовать команду ADB.
У нас также есть навык Android.
Но если вы хотите посмотреть статистику по реальным пользователям, то вам нужна Google Play Console.
Также есть Google Play Reporting API, который позволяет получать данные без входа в Play Console.
Как всегда, если вы хотите узнать больше, посетите developer.android.com/memory.
Сокращайте потребление, используйте повторно и перерабатывайте.
Мы могли бы создать лучшую экосистему Android, чтобы преодолеть трудности, с которыми мы сталкиваемся.
Спасибо и удачи!
Скоро у меня будут часы приема на улице, так что если у вас есть вопросы, не стесняйтесь приходить.
До выступления следующего спикера ещё есть время, поэтому, пожалуйста, подождите. Большое спасибо.
// 1. Ask CDN for the exact UI card dimensions (e.g. 540x675)
val sizedUrl = "$cdnBaseUrl?w=$cardWidthPx&h=$cardHeightPx&fmt=webp"
// 2. Enforce exact downsampling + Hardware Bitmap config
AsyncImage(
model = ImageRequest.Builder(LocalContext.current)
.data(sizedUrl)
.size(cardWidthPx, cardHeightPx) // Never use Size.ORIGINAL!
.precision(Precision.EXACT) // Downsample to exact card size
.bitmapConfig(Bitmap.Config.HARDWARE)
.build(),
contentDescription = product.title
)// Query active window bounds instead of hardcoding 1080 / 1920
val bounds = WindowMetricsCalculator.getOrCreate()
.computeCurrentWindowMetrics(activity).bounds
// Clamp target dimensions so Bitmap NEVER exceeds the active window
val safeWidth = minOf(requestedWidthPx, bounds.width())
val safeHeight = minOf(requestedHeightPx, bounds.height())
val bitmap = Bitmap.createBitmap(safeWidth, safeHeight, config)// Strip volatile query tokens (?from=list / ?sig=...) for cache key
val canonicalKey = MemoryCache.Key(productUrl.substringBefore("?"))
// On Checkout Screen: reuse the 200x200 Bitmap already loaded in the List!
AsyncImage(
model = ImageRequest.Builder(context)
.data(productUrl)
.placeholderMemoryCacheKey(canonicalKey) // Instant zero-decode reuse!
.memoryCacheKey(canonicalKey)
.diskCacheKey(productUrl.substringBefore("?"))
.build(),
contentDescription = product.title
)val totalBudgetBytes = computeAdaptiveCacheBytes(context)
val numberOfLoaders = 2 // e.g., Team A (Glide) + Team B (Coil)
// Approach A (With Stats): Weight by image traffic (70% Team A / 30% Team B)
val loaderABudget = (totalBudgetBytes * 0.70).toInt()
val loaderBBudget = (totalBudgetBytes * 0.30).toInt()
// Approach B (Simple Fix without stats): Divide by number of loaders!
val simpleSplitBudget = totalBudgetBytes / numberOfLoaders
MemoryCache.Builder()
.maxSizeBytes(simpleSplitBudget)
.build()override fun onStop() {
super.onStop()
// 1. Stop animations so they don't decode next frames in background!
bannerAnimationView.cancelAnimation()
animatedImageDrawable?.stop()
// 2. Cancel next-image buffering / prefetch queues!
nextImagesBufferingJob?.cancel()
Glide.with(this).pauseRequests()
}
override fun onDestroyView() {
super.onDestroyView()
bannerAnimationView.clearAnimation() // Release resources!
}adb shell dumpsys meminfo <pkg> # watch Native Heap!
adb shell am send-trim-memory <pkg> UI_HIDDEN