不同Android版本,對一張圖片的內存處理方式是不一樣的,使用不正確會導致OOM的發生,這篇文章帶你梳理內存占用情況,選擇適合你的圖片加載模式,解決OOM問題。
一、背景
你知道嗎
一張5.48MB,寬高像素為4896*6528的24位的靜態圖片,放在Android工程目錄下面的res/drawable-[density]/ 不同文件夾下面,占據的內存是多少?
使用Glide加載一張5.48MB,寬高像素為4896*6528的24位的網絡圖片,占據內存又是多少?
二、梳理概念
在正式分析下面的內容前,先來看幾個概念。
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查看占用內存大小。
布局文件,就是一個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。
可以看到,
對于分辨率為res/drawable-hdpi/、res/drawable-xhdpi/、res/drawable-xxdpi/三個分辨率來說,圖片占據內存基本是一致的,Java層內存沒有消耗,而是消耗了native內存。
res/drawable-xxxdpi/分辨率下面的圖片,占據內存是最高的,native占據了200M。
(2) 所有的機器,內存占用都是這個規律嗎
或許你有這個疑問:
為什么在不同的文件夾下面,圖片占據的內存資源基本一致,有的時候卻發現不同文件夾下面,內存占據又是不一樣的?
在回答這個問題前,你要搞清楚,google在圖片加載時候,不同的Android版本,做了native堆棧和Java堆棧的區分。
這里也有個有意思的現象,在Android4.4到Android 8.0以下的機器,當你把這個圖片放在不同的文件夾下面時,圖片占據的內存是不一樣的,那是因為圖片內存的加載,是在Java 堆棧,所以你可能會遇到 Java 層面的OOM。
1AndroidRuntime: 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
1simpleTarget = new SimpleTarget(width, height) {}
寬、高都為0
1simpleTarget = new SimpleTarget() {}
(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)與不設置寬高,區別并不大。
崩潰來了
104-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(還有后臺事件帶來內存波動)。
六、總結
不同分辨率的靜態資源圖片放在不同的文件夾下面,不要隨便放,會引起內存的異常。
編輯:hfy
評論
查看更多