Инструменты и эффективность

Стратегии, которые помогут уменьшить объем памяти, занимаемый битовыми картами в приложении.

Колонки

О приложении

О выступлении

Управление памятью битовых карт критически важно для снижения риска нехватки памяти в приложениях Android с большим количеством визуальных элементов. Разработчики знают, что нужно уменьшать размер изображений, но для долгосрочной стабильности приложения необходимо освоить ещё два ключевых принципа: повторное использование и утилизацию. На этом занятии вы узнаете, как оптимизировать использование памяти с помощью современных инструментов, библиотек с открытым исходным кодом, таких как Glide и Coil, и системных API.

На этом занятии вы узнаете, как диагностировать реальные регрессии памяти с помощью Android Vitals, реализовать расширенные шаблоны кеширования (повторного использования) с помощью Glide и Coil, а также упреждающе освобождать память (повторно использовать) с помощью обратного вызова onTrimMemory.

Сводка

  • В феврале 2027 года в Google Play начнут действовать пороговые значения для использования памяти. Превышение 90-го процентиля напрямую влияет на оценку качества приложения и может снизить его видимость в Google Play.
  • При декодировании битовые карты расширяются по размерам и глубине цвета, поэтому в большинстве приложений они занимают больше всего памяти. Всего несколько изображений в полном разрешении могут занимать сотни мегабайт.
  • Уменьшите разрешение изображений и декодируйте только видимые области, чтобы снизить потребление памяти. Запрашивайте изображения подходящего размера через CDN или используйте API, например BitmapRegionDecoder, вместо того чтобы загружать слишком большие изображения в полном разрешении.
  • Если использовать несколько загрузчиков изображений, то кеш кучи памяти будет расходоваться быстрее, а битовые карты будут дублироваться. Используйте один загрузчик или задайте явные ограничения памяти для существующих загрузчиков.
  • Освобождайте память, занятую изображениями, когда приложение переходит в фоновый режим, с помощью onTrimMemory. Остановите фоновые опросы и анимации в onStop, чтобы предотвратить рост объема памяти, используемой в фоновом режиме.

Ресурсы

Gemini Notebook

Темы

  • bitmap-optimization
  • android-memory
  • android-vitals
  • image-caching
  • memory-profiler

Расшифровка

Введение и обзор использования памяти битовыми картами

  • Фун Лам (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, в котором я изучал, как они используют память, правильно ли они это делают и можем ли мы чему-то у них научиться.

  • Затем я покажу вам, как оптимизировать использование памяти битовыми картами.

  • В конце мы расскажем об инструментах, которые помогут вам проверить свое приложение.

Стандарты качества Google Play и распространенные проблемы

  • Фун Лам (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, чтобы преодолеть трудности, с которыми мы сталкиваемся.

  • Спасибо и удачи!

  • Скоро у меня будут часы приема на улице, так что если у вас есть вопросы, не стесняйтесь приходить.

  • До выступления следующего спикера ещё есть время, поэтому, пожалуйста, подождите. Большое спасибо.

Код

Фрагменты кода

Фрагмент кода Kotlin на 11:50 в видео "Примеры из практики: определение размера и декодирование растровых изображений"
// 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
)
Фрагмент кода на Kotlin в разделе "Примеры: изменение размера и декодирование растровых изображений"
// 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)
Фрагмент кода на Kotlin в видео "Управление памятью, дубликатами и кешем"
// 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
)
Фрагмент кода на Kotlin в разделе "Управление памятью, дубликатами и кешем"
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()
Фрагмент кода на Kotlin в разделе "Предотвращение увеличения объема памяти, используемой в фоновом режиме"
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!
}
Фрагмент кода Shell на 25:17 в видео "Инструменты диагностики для разработчиков и анализ с помощью ИИ"
adb shell dumpsys meminfo <pkg>  # watch Native Heap!
adb shell am send-trim-memory <pkg> UI_HIDDEN