1. 程式人生 > 實用技巧 >Android 載入圖片佔用記憶體分析

Android 載入圖片佔用記憶體分析

本文首發於 vivo網際網路技術 微信公眾號
連結:https://mp.weixin.qq.com/s/aRDzmMlkqB14Ty67GJs9vg
作者:Xu Jie

不同Android版本,對一張圖片的記憶體處理方式是不一樣的,使用不正確會導致OOM的發生,這篇文章帶你梳理記憶體佔用情況,選擇適合你的圖片載入模式,解決OOM問題。

一、背景

你知道嗎

  1. 一張5.48MB,寬高畫素為4896*6528的24位的靜態圖片,放在Android工程目錄下面的res/drawable-[density]/ 不同資料夾下面,佔據的記憶體是多少?

  2. 使用Glide載入一張5.48MB,寬高畫素為4896*6528的24位的網路圖片,佔據記憶體又是多少?

(圖:畫素為4896*6528的圖片)

二、梳理概念

在正式分析下面的內容前,先來看幾個概念。

1、螢幕尺寸

指螢幕的對角線的長度,單位是英寸,1英寸=2.54釐米。這個值是利用手機螢幕的長和寬,然後利用勾股定理,就可以算出斜邊的長了。

2、螢幕畫素密度

即每英寸螢幕所擁有的畫素數,英文簡稱ppi, 螢幕畫素密度與螢幕尺寸和螢幕解析度有關,螢幕密度越低在給定物理區域的畫素就會較少。Android 將所有螢幕密度分為六組通用密度:ldpi( 低)、mdpi(中)、hdpi(高)、xhdpi(超高)、xxhdpi(超超高)和xxxhdpi(超超超高)。

3、螢幕解析度

螢幕解析度是指在橫縱向上的畫素點數,單位是px,1px=1個畫素點,比如我們經常說的寬高畫素為:4896*6528。

上面三個概念模糊嗎?我們可以看一下下面這兩張圖,就可以理清上面三個概念了:

(圖:解析度計算公式)

下面的分析,重要了解的是螢幕畫素密度。

三、螢幕密度(dpi)對應關係

螢幕物理區域中的畫素量,通常稱為 dpi(每英寸點數)。螢幕密度越低在給定物理區域的畫素就會較少。Android 將所有螢幕密度分為六組通用密度:ldpi( 低)、mdpi(中)、hdpi(高)、xhdpi(超高)、xxhdpi(超超高)和xxxhdpi(超超超高)。

六種通用密度之間遵循 3:4:6:8:12:16 的縮放比率。

四、程式碼驗證

程式碼很簡單,就是用一個ImageView包含一張背景圖片,然後通過轉換為Bitmap檢視佔用記憶體大小。

佈局檔案activity_main.xml

佈局檔案,就是一個ImageView控制元件,包含一張背景圖。

MainAcivity.java

Android有一個特殊的資料夾res/drawable-nodpi/,放在裡面的資源,不會被放大或者壓縮,按照原大小展示,我們這裡也把測試資源放在這個資料夾。

五、圖片的記憶體佔用

1、靜態圖片不區分資料夾記憶體佔用

仍然以寬高畫素為:4896*6528=31961088的圖片舉例,圖片原始大小為5.48M,圖片資源放在res/drawable-nodpi/下面,這時候找一個vivo X21手機,載入這張圖片,佔據記憶體情況為127844352byte:

而圖片的原始圖片畫素總數為31961088,跟記憶體大小127844352byte好像沒什麼關係,但是真相是31961088* 4 = 127844352(Byte),原始圖片尺寸大小與最終的記憶體佔用大小呈倍數的關係,所以在這裡與記憶體佔用大小有直接關係的就是原始圖片尺寸大小(例如:480x800),道理我都懂,但是倍數關係是從哪裡來的呢,這就要談論到Bitmap的畫素格式了。

Android系統支援4種格式的畫素格式,原始碼在Bitmap.Config中:

為了保證圖片質量,官方預設使用ARGB_8888格式,導致圖片的每個畫素會佔用4個Byte大小,所以demo裡面的圖片佔用記憶體大小就是畫素總數*畫素格式,就是384000 * 4 = 1536000(Byte),這個時候應該有點成就感了,可以幫助你解決一部分實際專案問題了。

2、靜態圖片區分資料夾記憶體佔用現象

(1)靜態圖片區分資料夾在X21(Android 8.0)上的記憶體佔用

那麼問題又來了,放在res/drawable-nodpi/資料夾下沒問題,放在其他資料夾下呢?因為我們要適配不同的機器。

仍然以vivo X21舉例,x21的目標圖片資料夾是res/drawable-xxdpi/,螢幕密度480dpi。

看一下這個圖片放在不同的資料夾下面,記憶體佔用情況,單位:M。

可以看到,

  1. 對於解析度為res/drawable-hdpi/、res/drawable-xhdpi/、res/drawable-xxdpi/三個解析度來說,圖片佔據記憶體基本是一致的,Java層記憶體沒有消耗,而是消耗了native記憶體。

  2. res/drawable-xxxdpi/解析度下面的圖片,佔據記憶體是最高的,native佔據了200M。

(2)所有的機器,記憶體佔用都是這個規律嗎

或許你有這個疑問:

為什麼在不同的資料夾下面,圖片佔據的記憶體資源基本一致,有的時候卻發現不同資料夾下面,記憶體佔據又是不一樣的?

在回答這個問題前,你要搞清楚,google在圖片載入時候,不同的Android版本,做了native堆疊和Java堆疊的區分。

這裡也有個有意思的現象,在Android4.4到Android 8.0以下的機器,當你把這個圖片放在不同的資料夾下面時,圖片佔據的記憶體是不一樣的,那是因為圖片記憶體的載入,是在Java 堆疊,所以你可能會遇到 Java 層面的OOM。

AndroidRuntime: java.lang.RuntimeException: Canvas: trying to draw too large(127844352bytes) bitmap.

8.0之後的記憶體分配是在native,Java層的bitmap建立之後,實際上畫素記憶體的分配是在native層直接呼叫calloc,所以其畫素分配的是在native heap上, 這也是為什麼8.0之後的Bitmap消耗記憶體可以無限增長,直到耗盡系統記憶體,也不會提示Java OOM的原因。

3、網路圖片載入記憶體佔用現象

(1)Glide載入圖片的方法

glide載入圖片資源的方式有兩個:

  • 無回撥,使用如下方式載入
Glide.with(context)
        .load(url)
        .apply(requestOptions.override(width, height))
        .into(imageView);
  • 有回撥,使用下面載入方式,區別在into傳入simpleTarget,而不是imageview
Glide.with(context)
        .asBitmap()
        .load(url)
        .apply(requestOptions)
        .into(simpleTarget);

其中的simpleTarget有兩種定義方式:

  • 傳入寬、高參數,且大於0
simpleTarget = new SimpleTarget<Bitmap>(width, height) {}
  • 寬、高都為0
simpleTarget = new SimpleTarget<Bitmap>() {} 

(2)SimpleTarget使用錯誤帶來的問題

  • A和B的區別

區別就在於,當你傳入了寬高的時候,圖片就按照你傳入的大小,快取到了記憶體(Glide更多級儲存大小此處不討論)。當你不設定寬、高的時候,圖片就按照原始的畫素大小進行了快取。

  • 但是我們經常不傳入寬、高

這是因為載入網路圖片的時候,我們經常不知道寬、高是多少,我們設定本地資源imageview畫素的時候,使用了wrap_content或者match_content,不確定最終的寬高,所以我們選擇傳入width = 0,height = 0,使用glide下載好圖片後,再去做對應的設定。

  • 為什麼我們一般情況下感受不到A、B的差異

這是因為,網路圖片也好、本地圖片也好,畫素都不會太大,以畫素型別為RGB_8888為例,一個1920*1080的圖片,在記憶體佔據記憶體為1920*1080*4Byte = 829440Byte = 7.9M。

此時設定寬、高(正常也就設定個幾十dp)與不設定寬高,區別並不大。

  • 崩潰來了

04-27 17:39:53.154 31269-31269/? E/art: Throwing OutOfMemoryError "Failed to allocate a 227278860 byte allocation with 1048576 free bytes and 126MB until OOM"
  • 為什麼崩潰?

因為本地的一張圖片大小雖然為5.48M,畫素為width = 4896 height = 6528,但是在記憶體佔據大小為 4896 * 6528 * 4 = 127844352byte = 120M。這個記憶體足以使官網app在本來使用記憶體就高的情況下閃退。

看一下載入這個本地圖片時的記憶體情況,從 320M 到 548M,飆升228M(還有後臺事件帶來記憶體波動,引起閃退的根本原因是Graphics的記憶體飆升)。

  • 怎麼解決崩潰?

想辦法去掉simpleTarget的B定義方法

如果你不知道需要現實的資源寬高是多少,設定下面這個引數,這樣就以當前螢幕寬、高作為最高顯示畫素,downsample設定為DownsampleStrategy.AT_MOST。

這個表示:

當你的資源原始尺寸大於width * height(螢幕寬、高畫素)時,以width * height為準。

當你的資源原始尺寸小於width * height時,以原始尺寸為準。

width * height作為圖片儲存到記憶體時的最大畫素值。

閃退問題同樣解決,此時記憶體使用情況從 290M 到 340M,增加50M(還有後臺事件帶來記憶體波動)。

六、總結

  1. 不同解析度的靜態資源圖片放在不同的資料夾下面,不要隨便放,會引起記憶體的異常。

  2. 網路載入框架Glide等,最好根據螢幕寬、高設定需要載入的圖片寬、高,不要使用圖片原始大小載入,否則容易出現崩潰。

其他:如果你有興趣,可以驗證 Android 8.0以下圖片記憶體佔用情況,會發現不一樣的天地。

更多內容敬請關注vivo 網際網路技術微信公眾號

注:轉載文章請先與微訊號:Labs2020聯絡