ツールとパフォーマンス

アプリのビットマップ メモリ使用量を削減するための実用的な戦略

スピーカー

概要

講演について

ビットマップ メモリの管理は、視覚的に豊かな Android アプリでメモリ不足(OOM)のリスクを軽減するために重要です。デベロッパーは画像サイズを小さくする必要があることを知っていますが、アプリの長期的な安定性を実現するには、他の 2 つの重要な柱である「再利用」と「リサイクル」をマスターする必要があります。このセッションでは、最新のツール、Glide や Coil などのオープンソース ライブラリ、システムレベルの API を使用してメモリ パフォーマンスを最適化するための実践的なプレイブックを紹介します。

このセッションでは、Android Vitals を使用して実際のメモリ回帰を診断する方法、Glide と Coil を使用して高度なキャッシュ(再利用)パターンを実装する方法、onTrimMemory コールバックを使用してメモリを事前に解放(リサイクル)する方法について説明します。

概要

  • Google Play では、2027 年 2 月にメモリ パフォーマンスのしきい値が導入されます。90 パーセンタイルの上限を超えると、アプリの健全性評価に直接影響し、Google Play ストアでの表示が低下する可能性があります。
  • ビットマップはデコード時にサイズと色深度が拡大されるため、ほとんどのアプリでメモリ消費量が最大になります。フル解像度の画像が数枚あるだけで、数百メガバイトになることがあります。
  • 画像をダウンサンプリングし、表示領域のみをデコードして、メモリ消費量を削減します。CDN を介して一致する画像サイズをリクエストするか、BitmapRegionDecoder などの API を使用して、大きすぎるフル解像度の画像を読み込まないようにします。
  • 複数の画像ローダを使用すると、デフォルトのメモリヒープ キャッシュの使用量が増加し、保存されたビットマップが重複します。1 つのローダーに標準化するか、既存のローダー全体に明示的なメモリ予算を設定します。
  • onTrimMemory を処理して、アプリがバックグラウンドに移動したときに画像メモリを解放します。onStop でバックグラウンドのポーリングとアニメーションを停止して、バックグラウンドのメモリの増加を防ぎます。

リソース

Gemini Notebook

トピック

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

文字起こし

概要とビットマップ メモリの概要

  • Fung Lam

    はい。昼食まであと 1 時間ほどですので、引き続きご参加ください。

  • ロンドンを拠点とする Android のデベロッパー リレーションズ エンジニア、Fung Lam です。

  • はい、ありがとうございます。

  • 始める前に、質問してもよろしいでしょうか?

  • 今年、新しいスマートフォンを購入した方はどれくらいいますか?

  • ワオ

  • ワオ

  • 正直に言って、Google Pixel を購入しないとしても、私はあなたを責めません。

  • 新しいスマートフォンは、より高価ですよね。

  • そう、だから新しいスマートフォンを買わなかった人もいるんだ。

  • なぜ料金が高くなるのですか?

  • 耳寄りな情報

  • いいえ。

  • RAM。

  • RAM、そうそう。

  • そうですね。RAM は Android にとっても、私たち開発者にとっても、本当に課題です。

  • そこで本日は、RAM のメモリに関する課題をどのように克服できるかについてお話しします。

  • 以前の Google I/O と Google I/O Connect では、メモリに関するセッションがいくつかありました。オペレーティング システムがメモリを割り当てる方法、アプリケーションをプロファイリングする方法、R8 を使用してコードを最適化する方法などです。もちろん、そのときもビットマップについて少し触れました。

  • スクリーンショットに私の写真が写っています。

  • メモリの最適化は、Android ランタイム、ガベージ コレクション、パフォーマンス ツール、オブジェクトのメモリ割り当て、コンパイラなど、さまざまな領域にまたがる大きな分野の一つです。このセッションでは、30 分間でビットマップ メモリを最適化する方法について説明します。

  • なぜ Bitmap なのか、疑問に思われるかもしれません。

  • ビットマップ メモリに注目すべき理由

  • 1 枚の画像は千の言葉に値するからです。

  • そのとおりです。その理由を説明します。

  • スマートフォンの画面(Google Pixel 7、Google Pixel 9、Google Pixel 11 Pro など)の画面解像度は、2,500 x 1,200、1,400 程度です。

  • つまり、各ピクセルに 32 ビットの色深度が割り当てられます。

  • 全画面の画像の場合、指が足りませんが、15 MB、50 MB ほどのメモリが必要になります。

  • OS システムのワードは 64 ビット、つまり 8 バイトです。

  • 1, 000 語はわずか 8 MB です。

  • そう、8K。

  • つまり、1 枚の画像が 1, 000 語以上の価値を持つことは間違いありません。

  • はい、これは重要です。

  • 1 枚の画像がすでに 15 MB に達しています。

  • アプリに 10 枚、100 枚の画像がある場合はどうすればよいでしょうか。

  • 大きな数字です。

  • 実際、ビットマップは通常、アプリ内で最大のメモリ消費源です。

  • 先ほど説明したように、画像がメモリにデコードされると、サイズと色深度に応じて展開されます。

  • ファイルサイズが JPEG 形式か PNG 形式かということではありません。

  • いいえ。デコードされてメモリが使用されます。

  • そのため、フル解像度の画像が数枚あるだけで、デバイスのメモリが数百メガバイトも消費されてしまいます。

  • 2 つ目は、メモリが非常に逼迫している場合、オペレーティング システムはメモリ消費を処理するためにより多くの電力を消費する必要があることです。

  • これにより、UI がジャンクになる可能性があります。

  • または、OS がアプリを強制終了してメモリを解放することもあります。

  • また、アプリでメモリ不足エラーやメモリ不足例外が発生することもあります。

  • 幸いなことに、このフェーズは通常、少しの努力で大きな成果を得ることができます。

  • ほとんどのメモリの急増は、回避可能な一般的なミスに起因しており、画質を犠牲にすることなく修正できます。

  • ビットマップ メモリは、アプリケーションをスムーズに動作させるだけでなく、安定性とユーザー維持率の向上にも重要であることがわかりました。

  • そこで、本日はアプリをスムーズに運用するための 5 つの簡単なステップをご紹介します。

  • まず、Google Play の技術的な品質、新しい技術的な品質要件について説明します。これには、今後導入されるメモリ上限と、その上限を簡単に確認できる場所が含まれます。

  • 次に、500 個の Android アプリケーションに関するケーススタディを紹介します。それらのアプリケーションがメモリをどのように使用しているか、適切な方法で使用しているか、そこから学べることはあるか、といった点について説明します。

  • その後、ビットマップのメモリ使用量を最適化する方法の原則について説明します。

  • 最後に、アプリの確認に役立つツールをいくつかご紹介します。

Google Play の品質基準とよくある問題

  • Fung Lam

    1 つ目は、Google Play の新しい品質要件です。

  • Google Play Console にアクセスする頻度はどのくらいですか?

  • 毎月、

  • 毎週?

  • 一度もない?

  • 笑っているということは、賛成ということですね?

  • 実行できない?

  • いいえ。

  • Android Vitals のセクションにアクセスしましたか?

  • はい、はい。

  • そう、だから私がこれを作る前に、あなたは何かを知っているはずです。

  • そこで、Google Play では、パフォーマンスのしきい値に関する新しい品質要件が導入されました。

  • Android と Google Play は、ユーザーに質の高いアプリを使用してほしいと考えています。

  • このしきい値も、エンドユーザーに最適なものを提供するためのものです。

  • 簡単に言うと、「アプリがバックグラウンドまたは監視可能なフォアグラウンド サーフェスで実行されている場合、メモリ使用量の 90 パーセンタイルがこのしきい値を超えてはならない」ということです。

  • そうしないと、Google Play ストアでのアプリの表示が制限される可能性があります。」

  • このポリシーは、2027 年 2 月よりアプリとゲームに適用されます。

  • このしきい値を超えると、アプリの健全性評価に直接影響します。

  • Google Play Console で、今日の 90 パーセンタイルを確認してください。

  • 期限までに改善してください。

  • これがアニメーションです。

  • 「Google Play Console にアクセスすると、Android Vitals が表示されます。

  • メモリの一部があります。

  • 90 パーセンタイルを確認できます。

  • クリックすると、メモリ消費量の平均が時系列で表示されます。

  • また、RAM、フォーム ファクタ、画面密度、OS バージョン別の階層もあり、特定のエントリーレベルのデバイスや特定のアプリ リリースでメモリのボトルネックを特定するのに役立ちます。

  • 「アプリがしきい値を超えていることを教えてくれてありがとう。

  • でも、何が悪いのかわからない。

  • サポートをお願いできますか?」

  • はい、そのために私がサポートさせていただきます。

  • わかりやすく説明します。たとえば、テレビを見ているときに、テレビの映像や音声が出なくなったら、どうしますか?

  • テレビのマニュアルを取り出し、最後の数ページを開きます。通常、そこにはトラブルシューティング ガイドが記載されており、「テレビから音が出ない場合は、このボタンまたはあのボタンを試してください」と書かれています。

  • はい。同様に、今日も一般的な問題がいくつか見つかりました。

  • このトラブルシューティング ガイドがお役に立てば幸いです。

  • 通常、テレビのマニュアルを読むときは、1 ページ目から読みます。1 ページ目には、通常、テレビの仕組みを理解するのに役立つ診断ツールキットが記載されています。

  • しかし、それらはあまり興味深いものではないことがわかったので、今日はまずトラブルシューティングから始めて、後で戻ることにします。

  • 正直に言うと、私はテレビのマニュアルをこのように読んでいます。

  • Android エコシステムで最も頻繁に発生するビットマップ割り当てのビットマップ メモリの問題を明らかにするため、Android で人気の高い約 500 個のアプリを調査しました。

  • 実際にアプリを実行し、起動して、ログインし、さまざまなページに移動して、深く掘り下げてから戻ります。

  • また、さまざまな RAM やデバイスでアプリをテストします。たとえば、2 GB のメモリを搭載した Android Go デバイス、4 GB のエントリーレベル、4 GB のミドルレベル、6 GB ~ 8 GB、非常に大きな画面、優れた画面品質、16 GB のメモリを搭載したプレミアムな折りたたみ式デバイスなどです。

  • このデバイスのメモリ使用量を、さまざまなアプリのステータスで測定し、ヒープダンプをキャプチャして、Perfetto AI Skill などのさまざまなツールを使用して割り当てを分析し、一般的なアンチパターン、つまり、私たちが犯す一般的な間違いを大規模に特定しました。

  • このように、アクティビティの状態によってメモリが変化します。

  • この分析により、サイズの大きすぎる画像からバックグラウンド キャッシュの増加まで、15 を超える一般的な問題を特定しました。

  • これが、本日ご紹介するコアとなるケーススタディです。

事例紹介: ビットマップのサイズ変更とデコード

  • Fung Lam

    はい。今お話ししているケーススタディはすべて、500 個のアプリを対象とした調査に関するものです。

  • ここにいらっしゃる皆さんは、Android が大好きですよね。

  • 私たちは皆、Android が大好きです。

  • たとえば、Android が好きなメンバーがいたとしても、そのメンバーが Android 開発のエキスパートとは限りません。

  • そこで、Android フィギュアを注文するための Android アプリを作成しました。

  • しかし、いくつか間違いがあります。

  • これが最初の間違いかもしれません。

  • 「商品ページには、さまざまな Android フィギュアを表示する 2 列のレイアウトがあります。

  • 各列の画像サイズは 400 x 400 ピクセル程度です。

  • しかし、実際には、画面のサイズに関係なく、アプリはサーバーから 1 つの画像をダウンロードしています。

  • 3,000 x 3,000 の画像がダウンロードされます。

  • そうですね。デベロッパーは「Android はとても美しく、素晴らしい。

  • ユーザーに最適なものを表示したいと考えています。」

  • しかし、実際にはそうではありません。なぜなら、ImageView のサイズによって制限されるからです。

  • そのため、大きな画像をダウンロードしても、ユーザーに最適な画像が表示されるわけではありません。

  • さらに、この大きな画像により、追加のメモリが必要になりました。

  • また、ダウンサンプリングを行う場合でも、ダウンサンプリングのプロセスで追加のメモリが使用されます。

  • このデベロッパーが問題を解決できるよう、どのようにサポートすればよいですか?

  • CDN についてご存じであれば、CDN が画像の切り抜き、サイズ変更、回転、フィルタの適用、正方形から円形への変換などの画像操作を処理できることをご存じでしょう。

  • HTTP リクエストによって、CDN が何らかの処理を行うことができます。

  • そのため、サーバーサイドでホストしている画像が 1 つだけの場合でも、CDN を使用してサイズ変更された画像をデバイスに送信できます。

  • 1 つ目は、画像をリクエストするときに 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

    このアプリは、実際には 1 人のデベロッパーが作成したものではありません。

  • 実際には、複数のコントリビューターがいて、それぞれ異なるツールを使用している可能性があります。

  • たとえば、その一部は、

  • もう一度始めましょう。

  • このアプリも同様です。

  • 商品ページに商品のリストがあり、この画像が表示されたので、ショッピング カートに追加しました。

  • 数分後、購入手続きに進むと、同じ画像が再び表示されます。

  • しかし、これは本当に同じ画像なのでしょうか。それとも、実際にはメモリ内の 2 つの異なるオブジェクトなのでしょうか。

  • よくある間違いの一つは、アプリが大量の画像を読み込んでいることです。

  • しかし、メモリ内に重複する画像も多数存在します。

  • 画像ローダを使用している場合は、キャッシュ保存を行うためのキーを渡すことができます。

  • そのため、同じ画像を使用する場合は、新しいオブジェクトを作成するのではなく、すでに割り当てられている Bitmap オブジェクトを使用できます。

  • Android Studio では、Memory Profiler を使用して、アプリに重複するビットマップがあるかどうかを確認できます。

  • このトピックは 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
    )
  • そして今、

  • 前の例に戻ります。

  • アプリのデベロッパーが異なる。

  • 一部はビューを使用しているため、画像ローダーとして Glide を使用しています。

  • Compose を使用しているため、Coil を使用しているものもあります。

  • 先ほど、画像ローダはキャッシュに役立つとおっしゃいましたが、画像ローダが複数あると何か問題があるのでしょうか?

  • 確かに、何か問題があります。

  • そのため、画像ローダでは、デフォルトでヒープメモリ サイズの X% がデフォルトのキャッシュ サイズとして使用されます。

  • たとえば、1 つの画像ローダを使用している場合、メモリの X% が使用されます。

  • 画像ローダが 2 つある場合、メモリは 2 倍になります。

  • 画像ローダが 3 つある場合は 3 倍になります。

  • また、イメージ ローダが同じプールを共有していない場合も、同じことが起こります。

  • 商品ページと同様に、Glide を使用して画像を読み込み、購入手続きページでは、Coil を使用して同じ画像を読み込みます。

  • ただし、メモリには 2 つの画像が残ります。

  • それはよくありません。

  • はい。これを修正するには、理想的には、1 つの画像ローダーまたは 1 つの共有プールをキャッシュに使用します。

  • しかし、エンジニアリング作業が多すぎて、自分にはできないと感じるかもしれません。

  • もっと簡単な修正方法は、この画像ローダーで処理される画像の割合と、別の画像ローダーで処理される画像の割合を測定し、このコードのように、異なる画像ローダーに重みを設定することです。

    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 つの画像ローダがある場合は 2 で割り、3 つの画像ローダがある場合は 3 で割ります。デフォルト値を使用すると、メモリが過剰に使用されるためです。

  • 商品ページで、ユーザーがスクロールして商品の詳細を確認することを期待しているとします。

  • 通常は、50 枚の画像または 50 MB の画像をキャッシュします。

  • しかし、アプリがバックグラウンドに移動した場合はどうなるでしょうか。

  • アプリがバックグラウンドにある場合、ユーザーはブラウジングしていない可能性があります。

  • ユーザーがブラウジングしていない場合でも、この量のキャッシュ画像をメモリに保持する必要があるでしょうか。

  • いいえ、リリースすべきです。

  • はい、onTrimMemory コールバックを尊重する必要があります。

  • はい。実際、現在多くのアプリが onTrimMemory コールバックを実装していないか、尊重していません。

  • onTrimMemory コールバックでは、2 つのシグナルに注意する必要があります。

  • 1 つは UI_HIDDEN です。これは、ユーザーがホームボタンを押したときのような状態です。ホームボタンを押した直後ではなく、ホームボタンを押してから一定時間が経過した後の状態です。つまり、「アプリは実際には、ユーザーに表示される UI がない状態」であることを意味します。

  • 「ユーザーがアプリを使用していない。

  • リソースを OS に解放する必要があります。」

  • そのため、キャッシュ サイズをトリミングする必要があります。

  • もう 1 つは TRIM_MEMORY_BACKGROUND です。これは、アプリがすでに長時間バックグラウンドにあり、OS がリソースを必要としていることを意味します。

  • このステージでは、メモリを可能な限り(ほぼ 100%)解放する必要があります。

  • これにより、アプリが OS によって強制終了されるリスクも軽減できます。

  • 「メモリが必要な場合は、メモリを割り当てて、OS が処理する」というようなものです。

  • そうでない場合、OS は何かを行う必要があります。

バックグラウンドでのメモリ増加の防止

  • Fung Lam

    よくある間違いとして、アプリがバックグラウンドに移行したときにメモリ使用量を減らす必要があるという話をしましたが、

  • しかし、アプリがバックグラウンドで実際に使用するメモリが増えていることがわかりました。

  • 理由

  • だからこそ、私は調査を行っています。

  • その理由を調べてみました。

  • その理由の一つは、「アプリにアニメーションやカルーセルがある。

  • たとえば、15 秒ごとに新しい画像をダウンロードし、新しい画像をユーザーに表示します。」

  • しかし、アプリがバックグラウンドに移行したときに、デベロッパーがアニメーションを停止していませんでした。

  • そのため、アプリがバックグラウンドにあっても、コードは実行され、新しい画像を取得してデコードし、より多くのメモリを使用します。

  • これは、アプリが行うべきではないことです。

  • はい。

  • アプリがバックグラウンドに移行するときは、すべてのアニメーション、キャッシュ、または関連のない作業を停止する必要があります。

  • アクティビティのライフサイクルを把握している場合は、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 フレームワークは、誰もが聞いたことがあると思います。

  • 1 つ目は、画像の読み込み時にメモリを削減する方法です。

  • もちろん、前述のように、サーバーから画像を取得するときにダウンサンプリングして適切なサイズにすることもできます。

  • 色深度を最適化するか、可能であればハードウェア ビットマップを使用します。

  • CDN を使用していない場合は、CDN を使用してみてください。

  • サーバーから画像を取得する際、サイズに最適な画像のみが取得されます。

  • 再利用。キャッシュを使用することで、メモリに読み込まれた画像が再利用され、メモリ内でビットマップ画像が重複しないようにします。

  • また、コード内に複数の画像ローダを配置しないようにしてください。効率が低下するためです。

  • リデュース、リユース、残りの 1 つは何ですか?

  • 耳寄りな情報

  • リサイクル。

  • 再試行しますか?

  • リサイクル、そう、リサイクル。

  • はい。小学校ではコーディングは学びませんが、リデュース、リユース、リサイクルは学びました。

  • リサイクル、リサイクル。

  • はい。他のアプリケーションや OS がメモリを使用できるように、メモリを解放する方法についてです。

  • リサイクルに関するものもあります。

  • まず、onTrimMemory イベントについて説明しました。UI が非表示になったとき、または OS が本当にリソースを必要としているときに、OS が送信するシグナルが 2 つあります。

  • はい、このリクエストを処理してみてください。

  • もちろん、アニメーション、画像ローダ、その他の処理がフォアグラウンドで実行されている場合は、onStop イベントの で停止するようにしてください。

  • リデュース、リユース、リサイクルを心がけましょう。

  • 覚えやすいですよね。

デベロッパー向け診断ツールキットと AI 分析

  • Fung Lam

    テレビのマニュアルの、あまり面白くない部分に戻ります。

  • このメモリの問題を効果的に検出して測定するには、さまざまなツールキットをどのように使用すればよいですか?

  • 1 つ目は、Google Play Console、Android Vitals、Reporting API です。

  • はい。Google Play Console で 90 パーセンタイルを確認してください。

  • 今 どこにいるの?

  • ウェブページにアクセスしない場合は、Reporting API を使用します。

  • API を使用して値を取得することは引き続き可能です。

  • はい、しきい値が 200 MB と 400 MB であることを確認してください。

  • しきい値を超えていないことを確認してください。

  • また、ヒープダンプをキャプチャしたり、イベントを送信してアプリが UI_HIDDEN ステージにあることをシミュレートしたりするのに役立つ ADB コマンドもあります。

    adb shell dumpsys meminfo <pkg>  # watch Native Heap!
    adb shell am send-trim-memory <pkg> UI_HIDDEN
  • Android Studio を使用している場合は、

  • それでも、この友人はいつもあなたのそばにいます。

  • Android Studio Profiler を使用してヒープダンプをキャプチャし、UI で実際のメモリ使用量やメモリ割り当てを確認できます。

  • もちろん、Android Studio の Gemini やその他の Android スキルで利用できる AI についても説明します。

  • ヒープダンプを分析し、Perfetto SQL トレースを使用してビットマップを見つけ、通常はキャッシュ オーナーを特定するのに役立ちます。

  • 画像の読み込みは Glide によるものか、Coil によるものか。

  • アプリケーションに他のメモリリークはありますか?

  • 「もう IDE は使っていない」という方もいらっしゃるでしょう。

  • 私はプロンプト エンジニアです。

  • AI エージェントにプロンプトを送信してコードを生成しています。」

  • はい、知っています。

  • また、空き時間にバイブ コーディングも行っています。

  • これはあくまでも一例です。

  • これは、500 個のアプリを調査するために使用するプロンプトの一部です。

  • はい、一部だけです。

  • 画面が大きくないことはわかっています。

  • これ以上入れると、ほとんど見えなくなってしまいます。

  • しかし、AI を使用して自動化し、ヒープダンプを含むメモリ使用量をキャプチャして分析し、結果を提供する方法を理解できます。

  • そして、ここで「コードの修正も提供してください」という文を追加することもできます。

  • このセッションで、メモリは広大な領域であり、

  • 大学のコンピュータ サイエンスでオペレーティング システムやコンピュータ システム アーキテクチャを学んでいません。

  • でも実際には、それほど怖くも難しくもありません。」

  • はい。これにより、アプリを簡単に分析できます。

  • ぜひお試しください。

  • もちろん、AI は間違いを犯すこともあるため、メモリ使用量や OS によるメモリの割り当て方法に関する基本的な知識は依然として重要です。

  • ぜひ私の講演を聴き、YouTube で動画をご覧ください。

重要なポイントとまとめ

  • Fung Lam

    まとめると、いくつかのポイントがあります。

  • 1 つ目は、削減です。

  • 画像のサイズが適切であることを確認します。

  • 画像を小さな画像に分割するのは、すべてをメモリに読み込むのではなく、ユーザーに表示される画像のみをメモリに読み込むためです。

  • 可能な限り、ハードウェア bitmapConfig または RGB_565 を使用します。

  • 再利用。キャッシュ戦略を立て、画像ローダを使用してメモリ内の重複するビットマップの数を減らします。

  • もちろん、複数のビットマップ画像を使用しないでください。これらの画像ローダのデフォルトのキャッシュ サイズを上書きしようとしている場合でも同様です。

  • 再利用やリサイクルもしていません。

  • はい。

  • アプリのメモリ管理は、アプリの状態に適応する必要があります。

  • UI が非表示になっている場合は、メモリの使用量を減らす必要があります。

  • OS からメモリの解放を求められた場合は、できるだけ多くのメモリを解放します。

  • ビットマップ、電力、コンピューティング リソースなど、リソースの消耗を止めます。

  • バナータイマー、アニメーション、画像プーリングを停止します。

  • 最後に、必ずモニタリングしてください。

  • Google Play Console に定期的にアクセスします。

  • これは練習です。

  • これにより、アプリのメモリ使用量の 90 パーセンタイルが、ユーザー全体の中でどのような状況にあるかを把握できます。

  • また、問題を早期に特定するのにも役立ちます。

  • はい。

  • ローカルでプロファイリングするには、Android Studio の Memory Profiler を使用します。

  • また、ADB コマンドもいくつかあります。

  • Android のスキルもあります。

  • ただし、実際のユーザーの統計情報を確認したい場合は、Google Play Console を使用してください。

  • また、Google Play Reporting API を使用すると、Google Play Console にログインしなくても API を使用してデータを取得できます。

  • 詳細については、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 のシェル スニペット
adb shell dumpsys meminfo <pkg>  # watch Native Heap!
adb shell am send-trim-memory <pkg> UI_HIDDEN