工具和性能

有助于减少应用位图内存占用的实用策略

扬声器

关于

演讲简介

管理位图内存对于降低视觉效果丰富的 Android 应用中的内存不足 (OOM) 风险至关重要。虽然开发者知道要缩减图片大小,但要实现应用的长期稳定性,还需要掌握另外两个关键要素:重用和回收。本讲座将提供一份实用的指南,介绍如何使用现代工具、Glide 和 Coil 等开源库以及系统级 API 来优化内存性能。

在本课程中,您将学习如何使用 Android Vitals 诊断实际内存回归问题,如何使用 Glide 和 Coil 实现高级缓存(重用)模式,以及如何使用 onTrimMemory 回调主动释放内存(回收)。

摘要

  • Google Play 将于 2027 年 2 月开始强制执行内存性能阈值。如果超出第 90 百分位限制,会直接影响应用的健康状况评估,并可能降低其在 Play 商店中的曝光度。
  • 位图在解码时会根据尺寸和色深进行扩展,因此在大多数应用中,位图是最大的内存消耗者。只需几张全分辨率图片,即可占用数百兆字节。
  • 对图片进行降采样,并仅解码可见区域,以减少内存消耗。通过 CDN 请求匹配的图片尺寸,或使用 BitmapRegionDecoder 等 API,而不是加载尺寸过大的全分辨率图片。
  • 多个图片加载器会使默认内存堆缓存用量成倍增加,并复制存储的位图。标准化使用一个加载器,或在现有加载器中设置明确的内存预算。
  • 通过处理 onTrimMemory,在应用移至后台时释放图片内存。在 onStop 中停止后台轮询和动画,以防止后台内存增长。

资源

Gemini Notebook

主题

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

转写文稿

简介和位图内存概览

  • Fung Lam

    是的,距离午餐时间还有大约 1 小时,请大家坚持一下。

  • 我是林楚峰,是 Android 开发者关系工程师,常驻伦敦。

  • 好的,谢谢。

  • 在开始之前,我可以问一个问题吗?

  • 今年有多少人买了新手机?

  • 哇

  • 哇

  • 说实话,如果你不买 Pixel,我也不会怪你。

  • 新手机的价格更高,对吧?

  • 是的,这就是为什么有些人没有购买新手机。

  • 为什么价格更高?

  • 您知道吗?

  • 否。

  • RAM。

  • RAM,对,对。

  • 是的,RAM 确实是 Android、我们以及开发者面临的挑战。

  • 因此,今天我将在这里告诉大家,我们如何克服内存 (RAM) 方面的挑战。

  • 在之前的 Google I/O 和 Google I/O Connect 大会上,我们举办了一些关于内存的专题演讲,内容涵盖操作系统如何分配内存、如何分析应用性能、如何使用 R8 优化代码,当然,当时我也稍微提及了位图。

  • 您可以在屏幕截图中看到我的照片。

  • 内存优化是一个很大的领域,涉及 Android 运行时、垃圾回收、性能工具、对象内存分配、编译器等不同方面。在本次 30 分钟的讲座中,我将向大家介绍如何优化位图内存。

  • 您可能会问,为什么?为什么是 Bitmap?

  • 为什么我们应该关注位图内存?

  • 因为一张图片胜过千言万语。

  • 的确如此,我来向您解释一下原因。

  • 在手机屏幕上,比如 Pixel 7、Pixel 9 或 Pixel 11 Pro 版本,屏幕分辨率约为 2,500 x 1,200 或 1,400。

  • 因此,对于每个像素,我们都会携带 32 位的颜色深度。

  • 因此,对于全屏图片,哦,我的手指不够用,但它会占用大约 15 MB 的内存,50 MB。

  • 因此,操作系统中的一个字长(例如 64 位)意味着 8 个字节。

  • 一千个字只有 8 MB。

  • 是的,8K。

  • 因此,一张图片绝对胜过千言万语。

  • 是的,这很重要。

  • 一张图片的大小就已达到 15 MB。

  • 如果您的应用有 10 张、100 张图片,该怎么办?

  • 这个数字很大。

  • 因此,实际上 Bitmap 通常是应用中最大的内存消耗者。

  • 正如我所提到的,当图片解码到内存中时,它会根据尺寸和颜色深度进行扩展。

  • 这与文件大小无关,而是与文件格式有关,是 JPEG 还是 PNG 格式。

  • 不会,它会解码并使用内存。

  • 因此,只需几张全分辨率图片,就会轻松占用设备上数百兆字节的内存。

  • 其次,当内存非常紧张时,操作系统将不得不消耗更多电量来处理内存消耗。

  • 这会导致界面出现卡顿。

  • 或者,操作系统也可能会终止您的应用以释放内存。

  • 否则,应用也会遇到内存不足错误、内存不足异常。

  • 好消息是,这个阶段通常只需稍加努力,就能取得巨大成效。

  • 大多数内存峰值都源于常见错误,这些错误本可以避免,而且我们可以在不牺牲图片质量的情况下修复这些错误。

  • 现在您知道了,位图内存对应用至关重要,不仅能让应用流畅运行,还关乎稳定性和用户留存率。

  • 因此,今天我们将通过五个简单的部分来帮助您确保应用顺畅运行。

  • 首先,我们将探讨 Google Play 技术质量,即新的技术质量要求,包括即将实施的内存限制,以及如何轻松查看该限制。

  • 其次,我将分享一个关于 500 个 Android 应用的案例研究,探讨它们如何使用内存,是否使用了正确的方式,或者我们是否可以从中吸取一些经验教训。

  • 之后,我将向您展示如何优化位图内存用量。

  • 最后,我们将分享一些可帮助您检查自己应用的工具。

Google Play 质量标准和常见问题

  • Fung Lam

    首先,Google Play 的新质量要求。

  • 您访问 Google Play 管理中心的频率如何?

  • 每月?

  • 每周?

  • 从未?

  • 笑声代表同意,对吧?

  • 否

  • 否。

  • 您是否访问了 Android Vitals 部分?

  • 是的,是的。

  • 是的,所以在我制作之前,你肯定已经知道了一些事情。

  • 因此,Google Play 针对性能阈值提出了新的质量要求。

  • Android 和 Play 团队希望用户能够使用优质应用。

  • 因此,这个阈值也是我们努力为最终用户提供最佳体验的一个方面。

  • 简单来说,就是“嘿,当您的应用在后台或可观察的前台界面中运行时,其内存用量的第 90 百分位数不应超过此阈值。

  • 否则,您的应用在 Play 商店中的曝光度可能会降低,诸如此类。”

  • 自 2027 年 2 月起,我们将开始对应用和游戏强制执行这些阈值。

  • 超出此阈值会直接影响您的应用健康状况评估。

  • 因此,请务必前往 Google Play 管理中心,查看“今天我的第 90 百分位值是多少?”

  • 并在截止日期前完成改进。

  • 这就是动画。

  • “嘿,如果你前往 Google Play 管理中心,就会看到 Android Vitals。

  • 有一部分记忆

  • 您可以看到第 90 百分位。

  • 点击它,您就可以看到一段时间内的平均内存消耗量。

  • 此外,还有按 RAM、设备规格、屏幕密度、操作系统版本划分的不同层级,可帮助您找出特定入门级设备或特定应用版本上的内存瓶颈。

  • 现在,我知道您可能会说:“嘿,谢谢您告诉我我的应用超出了阈值。

  • 但说真的,我不知道哪里出了问题。

  • 您愿意帮忙吗?”

  • 是的,所以我在这里为您提供帮助。

  • 我来举个简单的例子,比如,您正在看电视,突然电视没有画面或没有声音了,您会怎么做?

  • 您拿出电视手册,翻到最后几页,通常是问题排查指南,上面写着:“如果电视没有声音,请尝试按这个按钮或那个按钮。”

  • 是的,同样,今天我也发现了一些常见问题。

  • 希望本指南能帮助您排查问题。

  • 通常,当我们阅读电视手册时,应该从第一页开始,这通常是诊断工具包,可以真正帮助您了解电视的工作原理。

  • 但现在,我知道这些内容不那么有趣,所以今天请允许我先从问题排查开始,稍后再回来。

  • 说实话,我就是这样阅读电视手册的。

  • 为了找出 Android 生态系统中最常见的位图分配位图内存问题,我研究了 Android 中大约 500 个热门应用。

  • 我真的会运行应用、启动应用、登录、访问不同的页面、深入探索,然后返回。

  • 我还在不同 RAM、不同设备上测试应用,例如,2 GB 内存的 Android Go 设备、一些 4 GB 的入门级设备、一些 4 GB、6 GB 到 8 GB 的中端设备,或一些具有超大屏幕、超高屏幕质量和 16 GB 内存的高端可折叠设备。

  • 我们测量了该设备在不同应用状态下的内存用量,捕获了堆转储,并使用 Perfetto AI Skill 等不同工具分析了分配情况,以识别常见的反模式,也就是我们经常犯的错误。

  • 这就是内存使用量在不同 activity 状态下的变化情况。

  • 通过这项分析,我们发现了 15 个以上的常见问题,从图片过大到后台缓存增加,不一而足。

  • 这就是我们今天要分享和探讨的核心案例研究。

案例研究:调整位图大小和解码位图

  • Fung Lam

    是的,我正在分享的所有案例研究都是关于 500 款应用的研究。

  • 既然大家来到了这里,我想我们都热爱 Android,对吧?

  • 我们都喜欢 Android。

  • 假设我们当中有人也喜欢 Android,但他/她并不是 Android 开发专家。

  • 因此,他制作了一款 Android 应用,用于订购 Android 玩偶。

  • 但存在一些错误。

  • 我们来聊聊,看看这是否是第一个错误。

  • “嘿,在商品页面上,我们有一个两列布局,展示了不同的 Android 玩偶。

  • 对于每列,图片大小约为 400 x 400 像素。

  • 不过,无论屏幕有多大,应用实际上都会从服务器下载一张图片。

  • 正在下载 3000 x 3000 的图片。

  • 没错,因为开发者会说:“Android 太棒了,太惊艳了。

  • 我想向用户展示最好的内容。”

  • 但实际上,并不会,因为图片视图的尺寸会限制图片的大小。

  • 因此,即使您下载了大图片,也无法向用户呈现最佳效果。

  • 此外,这张大图还占用了额外的内存。

  • 即使我们对其进行降采样,降采样过程也会占用额外的内存。

  • 我们应如何帮助此开发者解决问题?

  • 如果您了解 CDN,就会知道 CDN 实际上可以处理一些图片操作,例如剪裁、调整大小、旋转、应用滤镜、将图片从方形变为圆形。

  • 通过 HTTP 请求,CDN 可以处理一些内容。

  • 因此,即使您的服务器端只托管一张图片,您也可以使用 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 请求中以 HTTP 形式传递大小。

  • 然后,在这里,在突出显示的代码中,您能看到突出显示的内容吗?是的,没错,请勿使用 Size.ORIGINAL。

  • 是的,使用宽度和高度,这样它们就会作为参数传递。

  • 使用精确度将图片降采样到确切大小。

  • 如果可能,请使用 Bitmap.Config.HARDWARE,它也会使用更少的内存,因为图片将直接存储在 VRAM 中。

  • 同样,在这个应用中,假设他们发布了新版 Android 收藏品,那么就有一张长图,用户可以左右滑动来查看新 Android 玩偶的不同设计。

  • 但这里还存在另一个问题。

  • 对于长图片,实际上有很大一部分超出了屏幕范围,这意味着用户实际上看不到它,但图片已加载到内存中。

  • 我们能做得更好吗?

  • 是。

  • 我们可以将其切成更小的图片,并且仅加载屏幕上显示的图片。

  • 屏幕之外的图片将不会加载或会被卸载。

  • 因此,它将使用更少的内存。

  • 同样,在本例中,它节省了大约 60% 的内存。

  • 当然,如果您剪裁成更小的矩形,则可以节省更多。

  • 但同样,如果想为用户提供一项功能,比如放大再放大,以便查看 Android 玩偶的每个细节,常见的错误是,他们真的会再次加载一张非常大的图片,以便可以放大再放大。

  • 但实际上,您不必这样做。

  • 更好的方法是,当用户放大已加载的原始低分辨率图片时,在用户停止捏合后,计算坐标,然后仅加载该区域,即从高分辨率图片中计算出的区域。

  • 因此,只有这些可见区域会被加载到内存中。

  • 如何做到这一点?

  • 哦,有一个名为 BitmapRegionDecoder 的 API,它可以帮助您加载图片的特定区域。

  • 当然,还有一些工具和库可以为您提供帮助。

  • 您可以在 GitHub 上搜索该工具。

  • 一些漫画应用或地图应用已经在使用这项功能。

  • 您可以将其用作参考。

  • 此外,我们是人,难免会犯错。

  • AI 也会犯错。

  • 因此,为了防止我们出错,我们还可以在加载图片时添加一些限制。

  • 例如,在这种情况下,我可以这样说:“嘿,让我们使用窗口大小或屏幕尺寸作为限制,并将其放入 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 命令来捕获堆转储,然后使用 Perfetto 堆图 SQL 语言来查询应用是否存在重复的位图。

  • 这是一个 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
    )
  • 现在,是的。

  • 我回到上一个示例。

  • 您好,我的应用有不同的开发者。

  • 其中一些使用 View,因此使用 Glide 作为图片加载器。

  • 其中一些开发者因为使用 Compose,所以也使用了 Coil。

  • 你刚才提到图片加载器可以帮助我们进行缓存,那么如果我有多个图片加载器,会有什么问题吗?

  • Definitely, there's something wrong.

  • 因此,对于图片加载器,默认情况下,它们会使用堆内存大小的 X% 作为默认缓存大小。

  • 因此,如果您有一个图片加载器,它会使用 X% 的内存。

  • 如果您有两个图片加载器,则内存用量将增加一倍。

  • 如果您有三个图片加载器,则为 3 倍。

  • 如果图片加载器不共享同一个池,也会发生同样的情况。

  • 例如,在商品页面中,我使用 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 MB 的图片。

  • 但如果应用进入后台,会发生什么情况?

  • 如果应用在后台运行,用户可能不会浏览。

  • 如果用户没有浏览,我是否仍应在内存中保留此数量的缓存图片?

  • 不,我们应该发布它。

  • 是的,我们应该遵循 onTrimMemory 回调。

  • 是的,实际上,目前很多应用都没有实现或不遵循 onTrimMemory 回调。

  • 对于 onTrimMemory 回调,我们应该注意两个信号。

  • 一个是 UI_HIDDEN,这就像用户按下了主屏幕按钮,但不是在他们按下主屏幕按钮后立即发生,而是在他们按下主屏幕按钮后的一段时间内发生,这意味着“哦,您的应用实际上处于用户看不到界面的状态”。

  • 我们可以预期,“用户没有使用您的应用。

  • 您应将资源释放给操作系统。”

  • 因此,我们应该缩减缓存大小。

  • 另一个是 TRIM_MEMORY_BACKGROUND,这意味着应用已在后台运行很长时间,现在操作系统确实需要一些资源。

  • 在此阶段,您应尽可能释放内存,几乎达到 100%。

  • 这样一来,应用被操作系统终止的几率和风险也会降低。

  • 因为“嘿,他们想要内存,你给他们,然后操作系统会处理。”

  • 如果不是,那么操作系统别无选择,必须采取行动。

防止后台内存增长

  • Fung Lam

    我看到的另一个重大错误是,应用在进入后台后,应该使用更少的内存。

  • 不过,我确实看到应用在后台运行时占用了更多内存。

  • 为什么?

  • 所以我才要做研究。

  • 我试图找出原因。

  • 其中一个原因是:“在他们的应用中,有一些动画或轮播界面。

  • 例如,它们每 15 秒下载一张新图片,并向用户展示一张新图片。”

  • 不过,开发者未在应用进入后台时停止动画。

  • 因此,即使应用在后台运行,代码仍在运行,仍在获取新图片、解码新图片,并使用更多内存。

  • 因此,这实际上是应用不应执行的操作。

  • 没错。

  • 当应用进入后台时,我们应停止所有动画、缓存或不相关的工作。

  • 因此,如果您了解 activity 生命周期,就会知道有 onPause、onStop 事件,请务必实现 onStop、onPause、onDestroyView 来停止动画,是的,停止图片拉取请求,或与不需要用户注意的工作相关的其他操作。

    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!
    }

记忆健康 3R 框架

  • Fung Lam

    我知道我已经和您讨论过很多案例,但我说过,“我有超过 15 个案例。”

  • 我知道,如果我把所有案例都告诉您,您可能会忘记所有内容。

  • 当你回到家,你就会在 Google 上搜索:“嘿,我该如何解决内存问题?”

  • 所以我让您更容易记住。

  • 没错。

  • 我会将其与一个非常著名的 3R 框架联系起来,帮助您记住这些原则。

  • 这个 3R 框架,我想大家都应该听说过。

  • 第一种是减少,即如何在加载图片时减少内存。

  • 当然,正如我所提到的,我们可以进行降采样,在从服务器获取图片时获取正确的图片尺寸。

  • 我们会优化色深,或尽可能使用硬件位图。

  • 如果您没有使用 CDN,请尝试使用 CDN。

  • 因此,当我们从服务器获取图片时,只会获取最适合该尺寸的图片。

  • 重复使用,即使用缓存,以便重复使用加载到内存中的图片,避免内存中出现重复的位图图片。

  • 当然,尽量不要在代码中使用多个图片加载器,因为这样效率较低。

  • 减量化、再利用,还有一项是什么?

  • 您知道吗?

  • 回收。

  • 是否重试?

  • 回收,是的,回收。

  • 是的,我们在小学就学过减少、重复使用和回收,虽然我们在小学不学编程。

  • 是的,回收,没错。

  • 是的,这涉及如何释放内存,以便其他应用或操作系统可以使用该内存。

  • 还有一些关于回收的内容。

  • 首先,我提到过 onTrimMemory 事件,当界面隐藏或操作系统确实需要资源时,操作系统会向您发送两个信号。

  • 是的,请尝试处理此请求。

  • 当然,如果您有动画、图片加载器或一些其他在前台运行的工作,请确保在发生 onPause、onStop 事件时停止这些工作。

  • 因此,请减少使用、重复使用和回收利用。

  • 很容易记住,对吧?

开发者诊断工具包和 AI 分析

  • Fung Lam

    现在,我们回到电视手册中不那么有趣的部分。

  • 我们如何使用不同的工具包来检测和有效衡量此内存问题?

  • 第一种是 Google Play 管理中心、Android Vitals 和 Reporting API。

  • 是的,请在 Google Play 管理中心内查看第 90 百分位。

  • 您目前处于哪个阶段?

  • 如果您不访问该网页,也可以使用 Reporting API。

  • 您仍然可以使用 API 获取该值。

  • 是的,请确保阈值为 200 MB 和 400 MB。

  • 确保您未超出阈值。

  • 此外,还有一些 ADB 命令可以帮助您,例如捕获堆转储或发送事件来模拟“我的应用现在处于 UI_HIDDEN 阶段”。

    adb shell dumpsys meminfo <pkg>  # watch Native Heap!
    adb shell am send-trim-memory <pkg> UI_HIDDEN
  • 如果您使用的是 Android Studio,那就太好了。

  • 不过,它始终是你的好朋友。

  • 您可以使用 Android Studio 性能分析器来捕获堆转储,通过界面查看实际的内存用量和内存分配情况。

  • 当然,我们还会讨论 Android Studio 中的 Gemini AI 或其他可用的 Android 技能。

  • 它可帮助您分析堆转储,并使用 Perfetto SQL 跟踪记录来帮助您找到位图,从而通常可以确定缓存所有者。

  • 我的图片是由 Glide 加载还是由 Coil 加载?

  • 我的应用中是否存在其他内存泄漏?

  • 我知道有些人会说:“嘿,其实我不再使用 IDE 了。

  • 我是一名提示工程师。

  • 我只是向 AI 智能体发送提示,让它来编写代码。”

  • 是的,我知道。

  • 我也会在空闲时间进行一些氛围编程。

  • 所以,我只是分享一个示例。

  • 是的,这只是我用于 500 个应用研究的提示的一部分。

  • 是的,只是其中一部分。

  • 您知道屏幕并不大。

  • 如果我放更多,你就几乎看不到了。

  • 但它能让您了解如何使用 AI 实现自动化,捕获内存用量(包括堆转储),并进行分析,为您提供分析结果。

  • 是的,我可以在这里再加一句:“Please also provide me the code fix for my code.”

  • 是的,我希望这能减轻你的恐惧,比如,“嘿,记忆是一个很大的领域。

  • 我在大学里没有学习过计算机科学中的操作系统或计算机系统架构。

  • 但实际上,这并没有那么可怕,也没有那么困难。”

  • 是的,这让您能够轻松分析应用。

  • 请试用。

  • 但 AI 难免会出错,因此,掌握有关内存用量以及操作系统如何分配内存的基础知识仍然很重要。

  • 因此,请来听我的演讲,并在 YouTube 上观看我们的视频。

要点总结

  • Fung Lam

    总结一下,我们有一些要点。

  • 首先,减少。

  • 请确保您获取的图片尺寸正确。

  • 您将图片分割成较小的图片,而不是将所有内容都加载到内存中,是的,只需将用户可见的图片加载到内存中即可。

  • 尽可能使用硬件 bitmapConfig 或 RGB_565。

  • 重复使用,因此请确保您有缓存策略,使用图片加载器来帮助您减少内存中重复位图的数量。

  • 当然,请勿使用多个位图图片,即使您尝试覆盖这些图片加载器的默认缓存大小也是如此。

  • 我们不重复利用,也不回收。

  • 没错。

  • 应用的内存管理应适应应用状态。

  • 如果界面处于隐藏状态,那么您应该使用更少的内存。

  • 如果操作系统确实要求您释放内存,请尽可能多地释放。

  • 并停止消耗资源,无论是位图、电量还是计算资源。

  • 是的,停止横幅计时器、停止动画、停止图片池。

  • 最后,请务必进行监控。

  • 定期访问 Google Play 管理中心。

  • 请确保这是您的惯常做法。

  • 是的,这有助于您了解,在整个用户群体中,应用内存用量的第 90 百分位数的当前状态是什么?

  • 这也有助于您及早发现问题。

  • 没错。

  • 如需在本地进行性能分析,您可以使用 Android Studio 内存分析器。

  • 我们还可以使用一些 ADB 命令。

  • 我们还拥有 Android 技能。

  • 但如果您想查看真实用户的统计信息,Google Play 管理中心是您的不二之选。

  • 此外,您还可以使用 Google Play Reporting API,无需登录 Play 管理中心即可获取这些数据。

  • 一如既往,如果您想了解更多信息,请访问 developer.android.com/memory。

  • 用它来减量化、再利用和再循环。

  • 我们可以打造一个更好的…我希望我们能共同打造一个更好的 Android 生态系统,以克服我们面临的挑战。

  • 谢谢,祝您编码愉快!

  • 我很快就会在外面进行答疑,所以如果您有任何问题,欢迎随时前来咨询。

  • 下一位演讲者还需要一些时间,请耐心等待下一位演讲者上台,非常感谢。

代码

代码段

“案例研究:调整位图大小和解码位图”中 11:50 处的 Kotlin 代码段
// 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
)
“案例研究:调整和解码位图”中 14:45 处的 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)
“管理内存、重复项和缓存”中 16:51 处的 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
)
“管理内存、重复项和缓存”中 18:17 处的 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()
“防止后台内存增长”中 22:05 处的 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!
}
“开发者诊断工具包和 AI 分析”中 25:17 处的 Shell 代码段
adb shell dumpsys meminfo <pkg>  # watch Native Heap!
adb shell am send-trim-memory <pkg> UI_HIDDEN