不同Android版本,對(duì)一張圖片的內(nèi)存處理方式是不一樣的,使用不正確會(huì)導(dǎo)致OOM的發(fā)生,這篇文章帶你梳理內(nèi)存占用情況,選擇適合你的圖片加載模式,解決OOM問(wèn)題。
一、背景
你知道嗎
一張5.48MB,寬高像素為4896*6528的24位的靜態(tài)圖片,放在Android工程目錄下面的res/drawable-[density]/ 不同文件夾下面,占據(jù)的內(nèi)存是多少?
使用Glide加載一張5.48MB,寬高像素為4896*6528的24位的網(wǎng)絡(luò)圖片,占據(jù)內(nèi)存又是多少?
二、梳理概念
在正式分析下面的內(nèi)容前,先來(lái)看幾個(gè)概念。
1、屏幕尺寸
指屏幕的對(duì)角線的長(zhǎng)度,單位是英寸,1英寸=2.54厘米。這個(gè)值是利用手機(jī)屏幕的長(zhǎng)和寬,然后利用勾股定理,就可以算出斜邊的長(zhǎng)了。
2、屏幕像素密度
即每英寸屏幕所擁有的像素?cái)?shù),英文簡(jiǎn)稱ppi, 屏幕像素密度與屏幕尺寸和屏幕分辨率有關(guān),屏幕密度越低在給定物理區(qū)域的像素就會(huì)較少。Android 將所有屏幕密度分為六組通用密度:ldpi( 低)、mdpi(中)、hdpi(高)、xhdpi(超高)、xxhdpi(超超高)和xxxhdpi(超超超高)。
3、屏幕分辨率
屏幕分辨率是指在橫縱向上的像素點(diǎn)數(shù),單位是px,1px=1個(gè)像素點(diǎn),比如我們經(jīng)常說(shuō)的寬高像素為:4896*6528。
上面三個(gè)概念模糊嗎?我們可以看一下下面這兩張圖,就可以理清上面三個(gè)概念了:
(圖:分辨率計(jì)算公式)
下面的分析,重要了解的是屏幕像素密度。
三、屏幕密度(dpi)對(duì)應(yīng)關(guān)系
屏幕物理區(qū)域中的像素量,通常稱為 dpi(每英寸點(diǎn)數(shù))。屏幕密度越低在給定物理區(qū)域的像素就會(huì)較少。Android 將所有屏幕密度分為六組通用密度:ldpi( 低)、mdpi(中)、hdpi(高)、xhdpi(超高)、xxhdpi(超超高)和xxxhdpi(超超超高)。
六種通用密度之間遵循 3:4:6:8:12:16 的縮放比率。
四、代碼驗(yàn)證
代碼很簡(jiǎn)單,就是用一個(gè)ImageView包含一張背景圖片,然后通過(guò)轉(zhuǎn)換為Bitmap查看占用內(nèi)存大小。
布局文件,就是一個(gè)ImageView控件,包含一張背景圖。
MainAcivity.java
Android有一個(gè)特殊的文件夾res/drawable-nodpi/,放在里面的資源,不會(huì)被放大或者壓縮,按照原大小展示,我們這里也把測(cè)試資源放在這個(gè)文件夾。
五、圖片的內(nèi)存占用
1、靜態(tài)圖片不區(qū)分文件夾內(nèi)存占用
仍然以寬高像素為:4896*6528=31961088的圖片舉例,圖片原始大小為5.48M,圖片資源放在res/drawable-nodpi/下面,這時(shí)候找一個(gè)vivo X21手機(jī),加載這張圖片,占據(jù)內(nèi)存情況為127844352byte:
而圖片的原始圖片像素總數(shù)為31961088,跟內(nèi)存大小127844352byte好像沒(méi)什么關(guān)系,但是真相是31961088* 4 = 127844352(Byte),原始圖片尺寸大小與最終的內(nèi)存占用大小呈倍數(shù)的關(guān)系,所以在這里與內(nèi)存占用大小有直接關(guān)系的就是原始圖片尺寸大小(例如:480x800),道理我都懂,但是倍數(shù)關(guān)系是從哪里來(lái)的呢,這就要談?wù)摰紹itmap的像素格式了。
Android系統(tǒng)支持4種格式的像素格式,源碼在Bitmap.Config中:
為了保證圖片質(zhì)量,官方默認(rèn)使用ARGB_8888格式,導(dǎo)致圖片的每個(gè)像素會(huì)占用4個(gè)Byte大小,所以demo里面的圖片占用內(nèi)存大小就是像素總數(shù)*像素格式,就是384000 * 4 = 1536000(Byte),這個(gè)時(shí)候應(yīng)該有點(diǎn)成就感了,可以幫助你解決一部分實(shí)際項(xiàng)目問(wèn)題了。
2、靜態(tài)圖片區(qū)分文件夾內(nèi)存占用現(xiàn)象
(1) 靜態(tài)圖片區(qū)分文件夾在X21(Android 8.0)上的內(nèi)存占用
那么問(wèn)題又來(lái)了,放在res/drawable-nodpi/文件夾下沒(méi)問(wèn)題,放在其他文件夾下呢?因?yàn)槲覀円m配不同的機(jī)器。
仍然以vivo X21舉例,x21的目標(biāo)圖片文件夾是res/drawable-xxdpi/,屏幕密度480dpi。
看一下這個(gè)圖片放在不同的文件夾下面,內(nèi)存占用情況,單位:M。
可以看到,
對(duì)于分辨率為res/drawable-hdpi/、res/drawable-xhdpi/、res/drawable-xxdpi/三個(gè)分辨率來(lái)說(shuō),圖片占據(jù)內(nèi)存基本是一致的,Java層內(nèi)存沒(méi)有消耗,而是消耗了native內(nèi)存。
res/drawable-xxxdpi/分辨率下面的圖片,占據(jù)內(nèi)存是最高的,native占據(jù)了200M。
(2) 所有的機(jī)器,內(nèi)存占用都是這個(gè)規(guī)律嗎
或許你有這個(gè)疑問(wèn):
為什么在不同的文件夾下面,圖片占據(jù)的內(nèi)存資源基本一致,有的時(shí)候卻發(fā)現(xiàn)不同文件夾下面,內(nèi)存占據(jù)又是不一樣的?
在回答這個(gè)問(wèn)題前,你要搞清楚,google在圖片加載時(shí)候,不同的Android版本,做了native堆棧和Java堆棧的區(qū)分。
這里也有個(gè)有意思的現(xiàn)象,在Android4.4到Android 8.0以下的機(jī)器,當(dāng)你把這個(gè)圖片放在不同的文件夾下面時(shí),圖片占據(jù)的內(nèi)存是不一樣的,那是因?yàn)閳D片內(nèi)存的加載,是在Java 堆棧,所以你可能會(huì)遇到 Java 層面的OOM。
1AndroidRuntime: java.lang.RuntimeException: Canvas: trying to draw too large(127844352bytes) bitmap.
8.0之后的內(nèi)存分配是在native,Java層的bitmap創(chuàng)建之后,實(shí)際上像素內(nèi)存的分配是在native層直接調(diào)用calloc,所以其像素分配的是在native heap上, 這也是為什么8.0之后的Bitmap消耗內(nèi)存可以無(wú)限增長(zhǎng),直到耗盡系統(tǒng)內(nèi)存,也不會(huì)提示Java OOM的原因。
3、網(wǎng)絡(luò)圖片加載內(nèi)存占用現(xiàn)象
(1) Glide加載圖片的方法
glide加載圖片資源的方式有兩個(gè):
無(wú)回調(diào),使用如下方式加載
Glide.with(context)
.load(url)
.apply(requestOptions.override(width, height))
.into(imageView);
有回調(diào),使用下面加載方式,區(qū)別在into傳入simpleTarget,而不是imageview
Glide.with(context)
.asBitmap()
.load(url)
.apply(requestOptions)
.into(simpleTarget);
其中的simpleTarget有兩種定義方式:
傳入寬、高參數(shù),且大于0
1simpleTarget = new SimpleTarget(width, height) {}
寬、高都為0
1simpleTarget = new SimpleTarget() {}
(2)SimpleTarget使用錯(cuò)誤帶來(lái)的問(wèn)題
A和B的區(qū)別
區(qū)別就在于,當(dāng)你傳入了寬高的時(shí)候,圖片就按照你傳入的大小,緩存到了內(nèi)存(Glide更多級(jí)存儲(chǔ)大小此處不討論)。當(dāng)你不設(shè)置寬、高的時(shí)候,圖片就按照原始的像素大小進(jìn)行了緩存。
這是因?yàn)榧虞d網(wǎng)絡(luò)圖片的時(shí)候,我們經(jīng)常不知道寬、高是多少,我們?cè)O(shè)置本地資源imageview像素的時(shí)候,使用了wrap_content或者match_content,不確定最終的寬高,所以我們選擇傳入width = 0,height = 0,使用glide下載好圖片后,再去做對(duì)應(yīng)的設(shè)置。
為什么我們一般情況下感受不到A、B的差異
這是因?yàn)椋W(wǎng)絡(luò)圖片也好、本地圖片也好,像素都不會(huì)太大,以像素類(lèi)型為RGB_8888為例,一個(gè)1920*1080的圖片,在內(nèi)存占據(jù)內(nèi)存為1920*1080*4Byte = 829440Byte = 7.9M。
此時(shí)設(shè)置寬、高(正常也就設(shè)置個(gè)幾十dp)與不設(shè)置寬高,區(qū)別并不大。
崩潰來(lái)了
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”
為什么崩潰?
因?yàn)楸镜氐囊粡垐D片大小雖然為5.48M,像素為width = 4896 height = 6528,但是在內(nèi)存占據(jù)大小為 4896 * 6528 * 4 = 127844352byte = 120M。這個(gè)內(nèi)存足以使官網(wǎng)app在本來(lái)使用內(nèi)存就高的情況下閃退。
看一下加載這個(gè)本地圖片時(shí)的內(nèi)存情況,從 320M 到 548M,飆升228M(還有后臺(tái)事件帶來(lái)內(nèi)存波動(dòng),引起閃退的根本原因是Graphics的內(nèi)存飆升)。
怎么解決崩潰?
想辦法去掉simpleTarget的B定義方法
如果你不知道需要現(xiàn)實(shí)的資源寬高是多少,設(shè)置下面這個(gè)參數(shù),這樣就以當(dāng)前屏幕寬、高作為最高顯示像素,downsample設(shè)置為DownsampleStrategy.AT_MOST。
這個(gè)表示:
當(dāng)你的資源原始尺寸大于width * height(屏幕寬、高像素)時(shí),以width * height為準(zhǔn)。
當(dāng)你的資源原始尺寸小于width * height時(shí),以原始尺寸為準(zhǔn)。
width * height作為圖片保存到內(nèi)存時(shí)的最大像素值。
閃退問(wèn)題同樣解決,此時(shí)內(nèi)存使用情況從 290M 到 340M,增加50M(還有后臺(tái)事件帶來(lái)內(nèi)存波動(dòng))。
六、總結(jié)
不同分辨率的靜態(tài)資源圖片放在不同的文件夾下面,不要隨便放,會(huì)引起內(nèi)存的異常。
編輯:hfy
評(píng)論
查看更多