工具與成效

減少應用程式點陣圖記憶體用量的實用策略

喇叭

關於

講座簡介

在視覺效果豐富的 Android 應用程式中,管理點陣圖記憶體是降低記憶體不足 (OOM) 風險的關鍵。開發人員知道要縮減圖片大小,但要讓應用程式長期穩定運作,還必須掌握另外兩大要素:重複使用和回收。本課程將提供實用指南,說明如何使用現代工具、Glide 和 Coil 等開放原始碼程式庫,以及系統層級 API,將記憶體效能最佳化。

在本課程中,您將瞭解如何使用 Android Vitals 診斷實際的記憶體迴歸問題、使用 Glide 和 Coil 實作進階快取 (重複使用) 模式,以及使用 onTrimMemory 回呼主動釋出記憶體 (回收)。

摘要

  • Google Play 將於 2027 年 2 月強制實施記憶體效能門檻。如果超出第 90 百分位數限制,會直接影響應用程式的健康評估,並可能降低在 Play 商店的曝光度。
  • 點陣圖在解碼時會依維度和色彩深度展開,因此在大多數應用程式中,點陣圖是記憶體用量最大的部分。只要幾張全解析度圖片,就會占用數百 MB 的空間。
  • 對圖片進行降取樣,並只解碼可見區域,以減少記憶體消耗。透過 CDN 要求相符的圖片尺寸,或使用 BitmapRegionDecoder 等 API,而不是載入過大的全解析度圖片。
  • 多個圖片載入器會使預設記憶體堆積快取用量倍增,並重複儲存點陣圖。請統一使用一個載入器,或為現有載入器設定明確的記憶體預算。
  • 處理 onTrimMemory,在應用程式移至背景時釋出圖片記憶體。停止 onStop 中的背景輪詢和動畫,避免背景記憶體用量增加。

資源

Gemini Notebook

主題

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

轉錄稿

簡介和點陣圖記憶體總覽

  • Fung Lam

    沒錯,距離午餐時間還有大約 1 小時,請繼續聽我說明。

  • 我是 Fung Lam,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 的記憶體。

  • 不過,OS 系統中的字詞就像 64 位元,也就是 8 個位元組。

  • 一千個字只有 8 MB。

  • 沒錯,8K。

  • 因此,一張圖片絕對勝過千言萬語。

  • 沒錯,這很重要。

  • 一張圖片的大小已達 15 MB。

  • 如果應用程式有 10、100 張圖片,該怎麼辦?

  • 這是一個很大的數字。

  • 因此,點陣圖通常是應用程式中耗用最多記憶體的項目。

  • 如前所述,圖片解碼至記憶體時,會根據尺寸和色彩深度展開。

  • 這不是指檔案大小,而是指檔案格式,例如 JPEG 或 PNG。

  • 不會,系統會解碼並使用記憶體。

  • 因此,只要幾張全解析度圖片,就會輕鬆佔用裝置上數百 MB 的記憶體。

  • 其次,當記憶體非常吃緊時,作業系統就必須耗費更多電力來處理記憶體消耗。

  • 這會導致 UI 卡頓。

  • 或者,OS 也可能終止應用程式,釋出記憶體。

  • 否則應用程式會發生記憶體不足錯誤或記憶體不足例外狀況。

  • 幸好,這個階段通常只要稍加努力,就能獲得巨大成效。

  • 大多數記憶體用量激增的情況都是由常見錯誤造成,這些錯誤可以避免,而且我們可以在不犧牲圖片品質的情況下修正。

  • 現在您知道,點陣圖記憶體對應用程式很重要,不僅能讓應用程式順暢運作,還能提升穩定性和使用者留存率。

  • 今天,我們將介紹五個簡單步驟,協助您確保應用程式順暢運作。

  • 首先,我們將探討 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、板型規格、螢幕密度和 OS 版本區分不同層級,協助您找出特定入門級裝置或特定應用程式版本的記憶體瓶頸。

  • 我知道您可能會說:「謝謝您告訴我應用程式超過門檻,

  • 但我真的不知道哪裡出了問題。

  • 可以幫忙嗎?」

  • 是的,所以我才在這裡協助你。

  • 我會盡量讓您輕鬆理解,就像是,如果您在看電視,然後電視沒有影像或聲音,您會怎麼做?

  • 你拿出電視使用手冊,翻到最後幾頁,通常是疑難排解指南,上面寫著:「如果電視沒有聲音,請試試這個按鈕,或試試那個按鈕。」

  • 沒錯,同樣地,我今天也發現了一些常見問題。

  • 希望這份疑難排解指南能幫助您。

  • 一般來說,閱讀電視使用手冊時,應該從第一頁開始,通常是診斷工具包,協助你瞭解電視的運作方式。

  • 不過,我知道這些內容比較無趣,所以今天請允許我先從疑難排解開始,稍後再回頭說明。

  • 老實說,我就是這樣看電視使用手冊的。

  • 為了找出 Android 生態系統中最常見的點陣圖分配記憶體問題,我研究了約 500 個熱門 Android 應用程式。

  • 我會實際執行應用程式、啟動應用程式、登入、前往不同頁面、深入瀏覽,然後返回。

  • 我也會在不同 RAM 的裝置上測試應用程式,例如 2 GB 記憶體的 Android Go 裝置、4 GB 的入門級裝置、4 GB 到 8 GB 的中階裝置,以及 16 GB 記憶體的高階摺疊式裝置,這類裝置的螢幕很大,螢幕品質也很好。

  • 我們測量了這部裝置在不同應用程式狀態下的記憶體用量、擷取記憶體快照資料,並使用 Perfetto AI Skill 等不同工具分析分配情形,找出常見的反模式,也就是我們常犯的錯誤。

  • 這就是記憶體在不同活動狀態下的變化。

  • 透過這項分析,我們找出超過 15 個常見問題,包括圖片過大、背景快取增加等。

  • 這是我們今天要分享和探討的核心個案研究。

個案研究:調整點陣圖大小及解碼

  • Fung Lam

    沒錯,我現在分享的所有個案研究,都是關於 500 個應用程式的研究。

  • 既然各位來到這裡,我想大家都喜歡 Android,對吧?

  • 我們都喜歡 Android。

  • 假設我們之中有人也喜歡 Android,但並非 Android 開發人員。

  • 因此,他製作了一個 Android 應用程式,用於訂購 Android 公仔。

  • 但有些錯誤。

  • 我們來看看,這可能是第一個錯誤。

  • 「嘿,在產品頁面中,我們採用雙欄版面配置,顯示不同的 Android 公仔。

  • 每欄的圖片大小約為 400 x 400 像素。

  • 不過,無論螢幕大小為何,應用程式實際上都會從伺服器下載一張圖片。

  • 系統正在下載 3000 x 3000 的圖片。

  • 沒錯,因為開發人員會說:「Android 太棒了,

  • 我想向使用者顯示最合適的內容。」

  • 但實際上並非如此,因為它會受到圖片檢視畫面的尺寸限制。

  • 因此,即使您下載大型圖片,也無法向使用者呈現最佳效果。

  • 此外,這張大型圖片會耗用額外記憶體。

  • 即使我們降低取樣率,這個過程也會使用額外的記憶體。

  • 我們該如何協助這位開發人員修正問題?

  • 如果你瞭解 CDN,就會知道 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 要求中以 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

    再次強調,這個應用程式並非由單一開發人員開發。

  • 實際上,這項功能是為多位貢獻者設計,他們可能使用不同的工具。

  • 例如,部分內容是使用,抱歉。

  • 我再重說一遍。

  • 這個應用程式也一樣。

  • 我在產品頁面看到產品清單和這張圖片,於是將產品加入購物車。

  • 幾分鐘後,我前往結帳,嘿,同一張圖片再次出現。

  • 但這真的是同一張圖片,還是記憶體中的兩個不同物件?

  • 沒錯,我們發現的常見錯誤之一,就是應用程式載入大量圖片。

  • 不過,記憶體中也有許多重複的圖片。

  • 如果您使用圖片載入器,實際上可以傳遞鍵來執行快取。

  • 因此,使用相同圖片時,您可以使用已分配的點陣圖物件,而不是建立新物件。

  • 在 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。

  • 你剛才提到圖片載入器可協助快取,如果有多個圖片載入器,會有問題嗎?

  • 顯然有問題。

  • 因此,圖片載入器預設會使用 X% 的堆積記憶體大小,做為預設快取大小。

  • 因此,如果有一個圖片載入器,會使用 X% 的記憶體。

  • 如果有兩個圖片載入器,記憶體用量就會增加 2 倍。

  • 如果有三個圖片載入器,則會是 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,這就像使用者按下主畫面按鈕,但不是在按下主畫面按鈕後立即發生,而是在按下主畫面按鈕後的一段時間,這表示「您的應用程式實際上處於使用者看不到 UI 的狀態」。

  • 我們預期「使用者沒有使用你的應用程式,

  • 請將資源釋出給 OS。」

  • 因此,我們應該縮減快取大小。

  • 另一個是 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 架構。

  • 第一項是減少,也就是如何在載入圖片時減少記憶體用量。

  • 當然,如前所述,我們可以在從伺服器取得圖片時,將圖片降取樣,取得正確的圖片尺寸。

  • 我們會最佳化色彩深度,或盡可能使用硬體點陣圖。

  • 如果未使用 CDN,請嘗試使用。

  • 因此,當我們從伺服器取得圖片時,只會取得最適合的大小。

  • 重複使用:我們使用快取,因此載入記憶體的圖片會重複使用,記憶體中不會有重複的點陣圖圖片。

  • 當然,請盡量避免在程式碼中加入多個圖片載入器,因為這樣會降低效率。

  • 減少使用、重複使用,還有一個是什麼?

  • 你知道嗎?

  • 回收。

  • 要重試嗎?

  • 回收,沒錯,回收。

  • 沒錯,我們在小學時就學過減量、重複使用和回收,雖然我們在小學時不會學到程式設計。

  • 沒錯,回收。

  • 沒錯,這與如何釋出記憶體有關,這樣其他應用程式或 OS 才能使用記憶體。

  • 還有回收相關資訊。

  • 首先,我提到有 onTrimMemory 事件,也就是當 UI 隱藏或 OS 確實需要資源時,OS 會傳送兩個信號給您。

  • 好的,我會嘗試處理這項要求。

  • 當然,如果前景正在執行動畫、圖片載入器或其他工作,請務必在發生 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 自動擷取記憶體用量 (包括記憶體快照資料),並分析及提供結果。

  • 沒錯,我可以在這裡再說一句:「請一併提供程式碼修正建議。」

  • 希望這能減輕你的恐懼,你可以說:「記憶體是個很大的領域,

  • 我在大學時並未修習電腦科學中的作業系統或電腦系統架構。

  • 但其實這並不可怕,也不困難。」

  • 沒錯,這能讓您輕鬆分析應用程式。

  • 請試試看。

  • 不過,AI 當然也會出錯,因此瞭解記憶體用量和 OS 分配記憶體的基本知識仍很重要。

  • 歡迎參加我的講座,並觀看我們的 YouTube 影片。

重點回顧和摘要

  • Fung Lam

    總而言之,我們有幾項重點。

  • 首先,減少用量。

  • 請務必取得正確大小的圖片。

  • 您將圖片分割成較小的圖片,不將所有內容載入記憶體,只將使用者可見的圖片載入記憶體。

  • 請盡可能使用硬體 bitmapConfig 或 RGB_565。

  • 重複使用,因此請務必制定快取策略,並使用圖片載入器,減少記憶體中的重複點陣圖數量。

  • 當然,請勿使用多個點陣圖圖片,即使您嘗試覆寫這些圖片載入器的預設快取大小也一樣。

  • 我們不會重複使用或回收。

  • 沒錯。

  • 應用程式的記憶體管理應根據應用程式狀態調整。

  • 如果 UI 隱藏,沒錯,您應該使用較少的記憶體。

  • 如果 OS 確實要求您釋出記憶體,請盡可能釋出。

  • 並停止耗用資源,無論是點陣圖、電力或運算資源。

  • 沒錯,停止橫幅計時器、停止動畫、停止圖片集區。

  • 最後,請務必監控。

  • 定期前往 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
)
「Case studies:Sizing and decoding Bitmaps」影片 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