在視覺效果豐富的 Android 應用程式中,管理點陣圖記憶體是降低記憶體不足 (OOM) 風險的關鍵。開發人員知道要縮減圖片大小,但要讓應用程式長期穩定運作,還必須掌握另外兩大要素:重複使用和回收。本課程將提供實用指南,說明如何使用現代工具、Glide 和 Coil 等開放原始碼程式庫,以及系統層級 API,將記憶體效能最佳化。
在本課程中,您將瞭解如何使用 Android Vitals 診斷實際的記憶體迴歸問題、使用 Glide 和 Coil 實作進階快取 (重複使用) 模式,以及使用 onTrimMemory 回呼主動釋出記憶體 (回收)。
BitmapRegionDecoder 等 API,而不是載入過大的全解析度圖片。onTrimMemory,在應用程式移至背景時釋出圖片記憶體。停止 onStop 中的背景輪詢和動畫,避免背景記憶體用量增加。轉錄稿
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 應用程式的個案研究,探討這些應用程式如何使用記憶體、使用方式是否正確,以及是否有值得學習的地方。
接著,我會說明如何最佳化點陣圖記憶體用量。
最後,我們會分享一些工具,協助您檢查自己的應用程式。
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!
}Fung Lam
我知道我已經跟你談過很多案件,但我說過:「我有超過 15 個案件。」
如果我把所有案例都告訴你,你可能會忘記。
你回家後,只要在 Google 搜尋「如何解決記憶體問題?」
所以我會幫你記住。
沒錯。
我會將這些原則與知名的 3R 架構連結,幫助您記住。
我想大家都聽過 3R 架構。
第一項是減少,也就是如何在載入圖片時減少記憶體用量。
當然,如前所述,我們可以在從伺服器取得圖片時,將圖片降取樣,取得正確的圖片尺寸。
我們會最佳化色彩深度,或盡可能使用硬體點陣圖。
如果未使用 CDN,請嘗試使用。
因此,當我們從伺服器取得圖片時,只會取得最適合的大小。
重複使用:我們使用快取,因此載入記憶體的圖片會重複使用,記憶體中不會有重複的點陣圖圖片。
當然,請盡量避免在程式碼中加入多個圖片載入器,因為這樣會降低效率。
減少使用、重複使用,還有一個是什麼?
你知道嗎?
回收。
要重試嗎?
回收,沒錯,回收。
沒錯,我們在小學時就學過減量、重複使用和回收,雖然我們在小學時不會學到程式設計。
沒錯,回收。
沒錯,這與如何釋出記憶體有關,這樣其他應用程式或 OS 才能使用記憶體。
還有回收相關資訊。
首先,我提到有 onTrimMemory 事件,也就是當 UI 隱藏或 OS 確實需要資源時,OS 會傳送兩個信號給您。
好的,我會嘗試處理這項要求。
當然,如果前景正在執行動畫、圖片載入器或其他工作,請務必在發生 onPause、onStop 事件時停止。
因此,請減少使用、重複使用、回收再利用。
很好記吧?
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 生態系統,克服目前面臨的挑戰。
謝謝,祝您應用程式事業蒸蒸日上!
我很快就會在外面提供諮詢服務,如有任何問題,歡迎前來。
下一位講者還需要一些時間,請稍候,謝謝。
// 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