最近封裝了個高斯模糊元件,正好將圖片相關的理論基礎也梳理了下,所以,這次就來講講,在 Android 中,怎麼計算一張圖片在記憶體中佔據的大小,如果要優化,可以從哪些方向著手。
提問
閱讀本篇之前,先來想一些問題:
Q1:一張 png 格式的圖片,圖片檔案大小為 55.8KB,那麼它載入進記憶體時所佔的大小是多少?
Q2:為什麼有時候,同一個 app,app 內的同個介面,介面上同張圖片,但在不同裝置上所耗記憶體卻不一樣?
Q3:同一張圖片,在介面上顯示的控制元件大小不同時,它的記憶體大小也會跟隨著改變嗎?
Q4:圖片佔用的記憶體大小公式:圖片解析度 * 每個畫素點大小,這種說法正確嗎,或者嚴謹嗎?
Q5:優化圖片的記憶體大小有哪些方向可以著手?
正文
在 Android 開發中,經常需要對圖片進行優化,因為圖片很容易耗盡記憶體。那麼,就需要知道,一張圖片的大小是如何計算的,當載入進記憶體中時,佔用的空間又是多少?
先來看張圖片:
這是一張普通的 png 圖片,來看看它的具體資訊:
圖片的解析度是 1080*452,而我們在電腦上看到的這張 png 圖片大小僅有 55.8KB,那麼問題來了:
我們看到的一張大小為 55.8KB 的 png 圖片,它在記憶體中佔有的大小也是 55.8KB 嗎?
理清這點蠻重要的,因為碰到過有人說,我一張圖片就幾 KB,雖然介面上顯示了上百張,但為什麼記憶體佔用卻這麼高?
所以,我們需要搞清楚一個概念:我們在電腦上看到的 png 格式或者 jpg 格式的圖片,png(jpg) 只是這張圖片的容器,它們是經過相對應的壓縮演算法將原圖每個畫素點資訊轉換用另一種資料格式表示,以此達到壓縮目的,減少圖片檔案大小。
而當我們通過程式碼,將這張圖片載入進記憶體時,會先解析圖片檔案本身的資料格式,然後還原為點陣圖,也就是 Bitmap 物件,Bitmap 的大小取決於畫素點的資料格式以及解析度兩者了。
所以,**一張 png 或者 jpg 格式的圖片大小,跟這張圖片載入進記憶體所佔用的大小完全是兩回事。**你不能說,我 jpg 圖片也就 10KB,那它就只佔用 10KB 的記憶體空間,這是不對的。
那麼,一張圖片佔用的記憶體空間大小究竟該如何計算?
末尾附上的一篇大神文章裡講得特別詳細,感興趣可以看一看。這裡不打算講這麼專業,還是按照我粗坯的理解來給大夥講講。
圖片記憶體大小
網上很多文章都會介紹說,計算一張圖片佔用的記憶體大小公式:解析度 * 每個畫素點的大小。
這句話,說對也對,說不對也不對,我只是覺得,不結合場景來說的話,直接就這樣表達有點不嚴謹。
在 Android 原生的 Bitmap 操作中,某些場景下,圖片被載入進記憶體時的解析度會經過一層轉換,所以,雖然最終圖片大小的計算公式仍舊是解析度*畫素點大小,但此時的解析度已不是圖片本身的解析度了。
我們來做個實驗,分別從如下的幾種考慮點相互組合的場景中,載入同一張圖片,看一下佔用的記憶體空間大小分別是多少:
- 圖片的不同來源:磁碟、res 資原始檔
- 圖片檔案的不同格式:png、jpg
- 圖片顯示的不同大小的控制元件
- 不同的 Android 系統裝置
測試程式碼模板如下:
private void loadResImage(ImageView imageView) {
BitmapFactory.Options options = new BitmapFactory.Options();
Bitmap bitmap = BitmapFactory.decodeResource(getResources(), R.drawable.weixin, options);
//Bitmap bitmap = BitmapFactory.decodeFile("mnt/sdcard/weixin.png", options);
imageView.setImageBitmap(bitmap);
Log.i("!!!!!!", "bitmap:ByteCount = " + bitmap.getByteCount() + ":::bitmap:AllocationByteCount = " + bitmap.getAllocationByteCount());
Log.i("!!!!!!", "width:" + bitmap.getWidth() + ":::height:" + bitmap.getHeight());
Log.i("!!!!!!", "inDensity:" + options.inDensity + ":::inTargetDensity:" + options.inTargetDensity);
Log.i("!!!!!!", "imageview.width:" + imageView.getWidth() + ":::imageview.height:" + imageView.getHeight());
}
複製程式碼
ps:這裡提一下,使用 Bitmap 的 getByteCount()
方法可以獲取當前圖片佔用的記憶體大小,當然在 api 19 之後有另外一個方法,而且當 bitmap 是複用時獲取的大小含義也有些變化,這些特殊場景就不細說,感興趣自行查閱。反正這裡知道,大部分場景可以通過 getByteCount()
列印圖片佔用的記憶體大小來驗證我們的實驗即可。
圖片就是上圖那張:解析度為 1080*452 的 png 格式的圖片,圖片檔案本身大小 56KB
序號 | 前提 | Bitmap記憶體大小 |
---|---|---|
1 | 圖片位於res/drawable,裝置dpi=240,裝置1dp=1.5px,控制元件寬高=50dp | 4393440B(4.19MB) |
2 | 圖片位於res/drawable,裝置dpi=240,裝置1dp=1.5px,控制元件寬高=500dp | 4393440B(4.19MB) |
3 | 圖片位於res/drawable-hdpi,裝置dpi=240,裝置1dp=1.5px | 1952640B(1.86MB) |
4 | 圖片位於res/drawable-xhdpi,裝置dpi=240,裝置1dp=1.5px | 1098360B(1.05MB) |
5 | 圖片位於res/drawable-xhdpi,**裝置dpi=160,**裝置1dp=1px | 488160B(476.7KB) |
6 | 圖片位於res/drawable-hdpi,裝置dpi=160,裝置1dp=1px | 866880(846.5KB) |
7 | 圖片位於res/drawable,裝置dpi=160,裝置1dp=1px | 1952640B(1.86MB) |
8 | 圖片位於磁碟中,裝置dpi=160,裝置1dp=1px | 1952640B(1.86MB) |
9 | 圖片位於磁碟中,裝置dpi=240,裝置1dp=1.5px | 1952640B(1.86MB) |
看見沒有,明明都是同一張圖片,但在不同場景下,所佔用的記憶體大小卻是有可能不一樣的,具體稍後分析。以上場景中列出了圖片的不同來源,不同 Android 裝置,顯示控制元件的不同大小這幾種考慮點下的場景。我們繼續來看一種場景:同一張圖片,儲存成不同格式的檔案(不是重新命名,可藉助ps);
圖片:解析度 1080*452 的 jpg 格式的圖片,圖片檔案本身大小 85.2KB
ps:還是同樣上面那張圖片,只是通過 PhotoShop 儲存為 jpg 格式
序號 | 前提 | Bitmap記憶體大小 | 比較物件 |
---|---|---|---|
10 | 圖片位於res/drawable,裝置dpi=240,裝置1dp=1.5px | 4393440B(4.19MB) | 序號1 |
11 | 圖片位於res/drawable-hdpi,裝置dpi=240,裝置1dp=1.5px | 1952640B(1.86MB) | 序號3 |
12 | 圖片位於res/drawable-xhdpi,裝置dpi=240,裝置1dp=1.5px | 1098360B(1.05MB) | 序號4 |
13 | 圖片位於磁碟中,裝置dpi=240,裝置1dp=1.5px | 1952640B(1.86MB) | 序號9 |
這裡列出的幾種場景,每個場景比較的實驗物件序號也寫在每行最後了,大夥可以自己比對確認一下,是不是發現,資料都是一樣的,所以這裡可以先得到一點結論:
圖片的不同格式:png 或者 jpg 對於圖片所佔用的記憶體大小其實並沒有影響
好了,我們開始來分析這些實驗資料:
首先,如果按照圖片大小的計算公式:解析度 * 畫素點大小
那麼,這張圖片的大小按照這個公式應該是:1080 * 452 * 4B = 1952640B ≈ 1.86MB
ps: 這裡畫素點大小以 4B 來計算是因為,當沒有特別指定時,系統預設為 ARGB_8888 作為畫素點的資料格式,其他的格式如下:
- ALPHA_8 -- (1B)
- RGB_565 -- (2B)
- ARGB_4444 -- (2B)
- ARGB_8888 -- (4B)
- RGBA_F16 -- (8B)
上述實驗中,按理就應該都是這個大小,那,為什麼還會出現一些其他大小的資料呢?所以,具體我們就一條條來分析下:
分析點1
先看序號 1,2 的實驗,這兩者的區別僅在於圖片顯示的空間的大小上面。做這個測試是因為,有些人會認為,圖片佔據記憶體空間大小與圖片在介面上顯示的大小會有關係,顯示控制元件越大佔用記憶體越多。顯然,這種理解是錯誤的。
想想,圖片肯定是先載入進記憶體後,才繪製到控制元件上,那麼當圖片要申請記憶體空間時,它此時還不知道要顯示的控制元件大小的,怎麼可能控制元件的大小會影響到圖片佔用的記憶體空間呢,除非提前告知,手動參與圖片載入過程。
分析點2
再來看看序號 2,3,4 的實驗,這三個的區別,僅僅在於圖片在 res 內的不同資源目錄中。當圖片放在 res 內的不同目錄中時,為什麼最終圖片載入進記憶體所佔據的大小會不一樣呢?
如果你們去看下 Bitmap.decodeResource()
原始碼,你們會發現,系統在載入 res 目錄下的資源圖片時,會根據圖片存放的不同目錄做一次解析度的轉換,而轉換的規則是:
新圖的高度 = 原圖高度 * (裝置的 dpi / 目錄對應的 dpi )
新圖的寬度 = 原圖寬度 * (裝置的 dpi / 目錄對應的 dpi )
目錄名稱與 dpi 的對應關係如下,drawable 沒帶字尾對應 160 dpi:
所以,我們來看下序號 2 的實驗,按照上述理論的話,我們來計算看看這張圖片的記憶體大小:
轉換後的解析度:1080 * (240/160) * 452 * (240/160) = 1620 * 678
顯然,此時的解析度已不是原圖的解析度了,經過一層轉換,最後計算圖片大小:
1620 * 678 * 4B = 4393440B ≈ 4.19MB
這下知道序號 2 的實驗結果怎麼來的了吧,同樣的道理,序號 3 資源目的是 hdpi 對應的是 240,而裝置的 dpi 剛好也是 240,所以轉換後的解析度還是原圖本身,結果也才會是 1.86MB。
小結一下:
位於 res 內的不同資源目錄中的圖片,當載入進記憶體時,會先經過一次解析度的轉換,然後再計算大小,轉換的影響因素是裝置的 dpi 和不同的資源目錄。
分析點3
基於分析點 2 的理論,看下序號 5,6,7 的實驗,這三個實驗其實是用於跟序號 2,3,4 的實驗進行對比的,也就是這 6 個實驗我們可以得出的結論是:
- 同一圖片,在同一臺裝置中,如果圖片放在 res 內的不同資源目錄下,那麼圖片佔用的記憶體空間是會不一樣的
- 同一圖片,放在 res 內相同的資源目錄下,但在不同 dpi 的裝置中,圖片佔用的記憶體空間也是會不一樣的
所以,有可能出現這種情況,同一個 app,但跑在不同 dpi 裝置上,同樣的介面,但所耗的記憶體有可能是不一樣的。
為什麼這裡還要說是有可能不一樣呢?按照上面的理論,同圖片,同目錄,但不同 dpi 裝置,那顯然解析度轉換就不一樣,所耗記憶體應該是肯定不一樣的啊,為什麼還要用有可能這種說辭?
emmm,繼續看下面的分析點吧。
分析點4
序號 8,9 的實驗,其實是想驗證是不是隻有當圖片的來源是 res 內才會存在解析度的轉換,結果也確實證明了,當圖片在磁碟中,SD 卡也好,assert 目錄也好,網路也好(網路上的圖片其實最終也是下載到磁碟),只要不是在 res 目錄內,那麼圖片佔據記憶體大小的計算公式,就是按原圖的解析度 * 畫素點大小來。
其實,有空去看看 BitmapFactory 的原始碼,確實也只有 decodeResource()
方法內部會根據 dpi 進行解析度的轉換,其他 decodeXXX()
就沒有了。
那麼,為什麼在上個小節中,要特別說明,即使同一個 app,但跑在不同 dpi 裝置上,同樣的介面,但所耗的記憶體有可能是不一樣的。這裡為什麼要特別用有可能這個詞呢?
是吧,大夥想想。明明按照我們梳理後的理論,圖片的記憶體大小計算公式是:解析度*畫素點大小,然後如果圖片的來源是在 res 的話,就需要注意,圖片是放於哪個資源目錄下的,以及裝置本身的 dpi 值,因為系統取 res 內的資源圖片會根據這兩點做一次解析度轉換,這樣的話,圖片的記憶體大小不是肯定就不一樣了嗎?
emmm,這就取決於你本人的因素了,如果你開發的 app,圖片的相關操作都是通過 BitmapFactory 來操作,那麼上述問題就可以換成肯定的表述。但現在,哪還有人自己寫原生,Github 上那麼多強大的圖片開源庫,而不同的圖片開源庫,內部對於圖片的載入處理,快取策略,複用策略都是不一樣的。
所以,如果使用了某個圖片開源庫,那麼對於載入一張圖片到記憶體中佔據了多大的空間,就需要你深入這個圖片開源庫中去分析它的處理了。
因為基本所有的圖片開源庫,都會對圖片操作進行優化,那麼下面就繼續來講講圖片的優化處理吧。
圖片優化
有了上述的理論基礎,現在再來想想如果圖片佔用記憶體空間太多,要進行優化,可以著手的一些方向,也比較有眉目了吧。
圖片佔據記憶體大小的公式也就是:解析度*畫素點大小,只是在某些場景下,比如圖片的來源是 res 的話,可能最終圖片的解析度並不是原圖的解析度而已,但歸根結底,對於計算機來說,確實是按照這個公式計算。
所以,如果單從圖片本身考慮優化的話,也就只有兩個方向:
- 降低解析度
- 減少每個畫素點大小
除了從圖片本身考慮外,其他方面可以像記憶體預警時,手動清理,圖片弱引用等等之類的操作。
減少畫素點大小
第二個方向很好操作,畢竟系統預設是以 ARGB_8888 格式進行處理,那麼每個畫素點就要佔據 4B 的大小,改變這個格式自然就能降低圖片佔據記憶體的大小。
常見的是,將 ARGB_8888 換成 RGB_565 格式,但後者不支援透明度,所以此方案並不通用,取決於你 app 中圖片的透明度需求,當然也可以快取 ARGB_4444,但會降低質量。
由於基本是使用圖片開源庫了,以下列舉一些圖片開源庫的處理方式:
//fresco,預設使用ARGB_8888
Fresco.initialize(context, ImagePipelineConfig.newBuilder(context).setBitmapsConfig(Bitmap.Config.RGB_565).build());
//Glide,不同版本,畫素點格式不一樣
public class GlideConfiguration implements GlideModule {
@Override
public void applyOptions(Context context, GlideBuilder builder) {
builder.setDecodeFormat(DecodeFormat.PREFER_ARGB_8888);
}
@Override
public void registerComponents(Context context, Glide glide) {
}
}
//在AndroidManifest.xml中將GlideModule定義為meta-data
<meta-data android:name="com.inthecheesefactory.lab.glidepicasso.GlideConfiguration" android:value="GlideModule"/>
//Picasso,預設 ARGB_8888
Picasso.with(imageView.getContext()).load(url).config(Bitmap.Config.RGB_565).into(imageView);
複製程式碼
以上程式碼摘抄自網路,正確性應該可信,沒驗證過,感興趣自行去相關原始碼確認一下。
降低解析度
如果能夠讓系統在載入圖片時,不以原圖解析度為準,而是降低一定的比例,那麼,自然也就能夠達到減少圖片記憶體的效果。
同樣的,系統提供了相關的 API:
BitmapFactory.Options.inSampleSize
複製程式碼
設定 inSampleSize 之後,Bitmap 的寬、高都會縮小 inSampleSize 倍。例如:一張寬高為 2048x1536 的圖片,設定 inSampleSize 為 4 之後,實際載入到記憶體中的圖片寬高是 512x384。佔有的記憶體就是 0.75M而不是 12M,足足節省了 15 倍
上面這段話摘抄自末尾給的連結那篇文章中,網上也有很多關於如何操作的講解文章,這裡就不細說了。我還沒去看那些開源圖片庫的內部處理,但我猜想,它們對於圖片的優化處理,應該也都是通過這個 API 來操作。
其實,不管哪個圖片開源庫,在載入圖片時,內部肯定就有對圖片進行了優化處理,即使我們沒手動說明要進行圖片壓縮處理。這也就是我在上面講的,為什麼當你使用了開源圖片庫後,就不能再按照圖片記憶體大小一節中所講的理論來計算圖片佔據記憶體大小的原因。
我們可以來做個實驗,先看下 fresco 的實驗:
開源庫 | 前提 | Bitmap記憶體大小 |
---|---|---|
fresco | 圖片位於res/drawable,裝置dpi=240,裝置1dp=1.5px | 1952640B(1.86MB) |
fresco | 圖片位於res/drawable-hdpi,裝置dpi=240,裝置1dp=1.5px | 1952640B(1.86MB) |
fresco | 圖片位於res/drawable-xhdpi,裝置dpi=240,裝置1dp=1.5px | 1952640B(1.86MB) |
fresco | 圖片位於磁碟中,裝置dpi=240,裝置1dp=1.5px | 1952640B(1.86MB) |
如果使用 fresco,那麼不管圖片來源是哪裡,解析度都是已原圖的解析度進行計算的了,從得到的資料也能夠證實,fresco 對於畫素點的大小預設以 ARGB_8888 格式處理。
我猜想,fresco 內部對於載入 res 的圖片時,應該先以它自己的方式獲取圖片檔案物件,最後有可能是通過 BitmapFactory 的 decodeFile()
或者 decodeByteArray()
等等之類的方式載入圖片,反正就是不通過 decodeResource()
來載入圖片,這樣才能說明,為什麼不管放於哪個 res 目錄內,圖片的大小都是以原圖解析度來進行計算。有時間可以去看看原始碼驗證一下。
再來看看 Glide 的實驗:
開源庫 | 前提 | Bitmap記憶體大小 |
---|---|---|
Glide | 圖片位於res/drawable,裝置dpi=240,裝置1dp=1.5px,顯示到寬高500dp的控制元件 | 94200B(91.99KB) |
Glide | 圖片位於res/drawable-hdpi,裝置dpi=240,裝置1dp=1.5px,顯示到寬高500dp的控制元件 | 94200B(91.99KB) |
Glide | 圖片位於res/drawable-hdpi,裝置dpi=240,裝置1dp=1.5px,不顯示到控制元件,只獲取 Bitmap 物件 | 1952640B(1.86MB) |
Glide | 圖片位於磁碟中,裝置dpi=240,裝置1dp=1.5px,不顯示到控制元件,只獲取 Bitmap 物件 | 1952640B(1.86MB) |
Glide | 圖片位於磁碟中,裝置dpi=240,裝置1dp=1.5px,顯示到全屏控制元件(1920*984) | 7557120B(7.21MB) |
可以看到,Glide 的處理與 fresco 又有很大的不同:
如果只獲取 bitmap 物件,那麼圖片佔據的記憶體大小就是按原圖的解析度進行計算。但如果有通過 into(imageView)
將圖片載入到某個控制元件上,那麼解析度會按照控制元件的大小進行壓縮。
比如第一個,顯示的控制元件寬高均為 500dp = 750px,而原圖解析度 1080*452,最後轉換後的解析度為:750 * 314,所以圖片記憶體大小:750 * 314 * 4B = 94200B;
比如最後一個,顯示的控制元件寬高為 1920*984,原圖解析度轉換後為:1920 * 984,所以圖片記憶體大小:1920 * 984 * 4B = 7557120B;
至於這個轉換的規則是什麼,我不清楚,有時間可以去原始碼看一下,但就是說,Glide 會自動根據顯示的控制元件的大小來先進行解析度的轉換,然後才載入進記憶體。
但不管是 Glide,fresco,都不管圖片的來源是否在 res 內,也不管裝置的 dpi 是多少,是否需要和來源的 res 目錄進行一次解析度轉換。
所以,我在圖片記憶體大小這一章節中,才會說到,如果你使用了某個開源庫圖片,那麼,那麼理論就不適用了,因為系統開放了 inSampleSize 介面設定,允許我們對需要載入進記憶體的圖片先進行一定比例的壓縮,以減少記憶體佔用。
而這些圖片開源庫,內部自然會利用系統的這些支援,做一些記憶體優化,可能還涉及其他圖片裁剪等等之類的優化處理,但不管怎麼說,此時,系統原生的計算圖片記憶體大小的理論基礎自然就不適用了。
降低解析度這點,除了圖片開源庫內部預設的優化處理外,它們自然也會提供相關的介面來給我們使用,比如:
//fresco
ImageRequestBuilder.newBuilderWithSource(uri)
.setResizeOptions(new ResizeOptions(500, 500)).build()
複製程式碼
對於 fresco 來說,可以通過這種方式,手動降低解析度,這樣圖片佔用的記憶體大小也會跟著減少,但具體這個介面內部對於傳入的 (500, 500) 是如何處理,我也還不清楚,因為我們知道,系統開放的 API 只支援解析度按一定比例壓縮,那麼 fresco 內部肯定會進行一層的處理轉換了。
需要注意一點,我使用的 fresco 是 0.14.1 版本,高版本我不清楚,此版本的 setResizeOptions()
介面只支援對 jpg 格式的圖片有效,如果需要對 png 圖片的處理,網上很多,自行查閱。
Glide 的話,本身就已經根據控制元件大小做了一次處理,如果還要手動處理,可以使用它的 override()
方法。
總結
最後,來稍微總結一下:
- 一張圖片佔用的記憶體大小的計算公式:解析度 * 畫素點大小;但解析度不一定是原圖的解析度,需要結合一些場景來討論,畫素點大小就幾種情況:ARGB_8888(4B)、RGB_565(2B) 等等。
- 如果不對圖片進行優化處理,如壓縮、裁剪之類的操作,那麼 Android 系統會根據圖片的不同來源決定是否需要對原圖的解析度進行轉換後再載入進記憶體。
- 圖片來源是 res 內的不同資源目錄時,系統會根據裝置當前的 dpi 值以及資源目錄所對應的 dpi 值,做一次解析度轉換,規則如下:新解析度 = 原圖橫向解析度 * (裝置的 dpi / 目錄對應的 dpi ) * 原圖縱向解析度 * (裝置的 dpi / 目錄對應的 dpi )。
- 其他圖片的來源,如磁碟,檔案,流等,均按照原圖的解析度來進行計算圖片的記憶體大小。
- jpg、png 只是圖片的容器,圖片檔案本身的大小與它所佔用的記憶體大小沒有什麼關係。
- 基於以上理論,以下場景的出現是合理的:
- 同個 app,在不同 dpi 裝置中,同個介面的相同圖片所佔的記憶體大小有可能不一樣。
- 同個 app,同一張圖片,但圖片放於不同的 res 內的資源目錄裡時,所佔的記憶體大小有可能不一樣。
- 以上場景之所說有可能,是因為,一旦使用某個熱門的圖片開源庫,那麼,以上理論基本就不適用了。
- 因為系統支援對圖片進行優化處理,允許先將圖片壓縮,降低解析度後再載入進記憶體,以達到降低佔用記憶體大小的目的
- 而熱門的開源圖片庫,內部基本都會有一些圖片的優化處理操作:
- 當使用 fresco 時,不管圖片來源是哪裡,即使是 res,圖片佔用的記憶體大小仍舊以原圖的解析度計算。
- 當使用 Glide 時,如果有設定圖片顯示的控制元件,那麼會自動按照控制元件的大小,降低圖片的解析度載入。圖片來源是 res 的解析度轉換規則對它也無效。
本篇所梳理出的理論、基本都是通過總結別人的部落格內容,以及自己做相關實驗驗證後,得出來的結論,正確性相比閱讀原始碼本身梳理結論自然要弱一些,所以,如果有錯誤的地方,歡迎指點一下。有時間,也可以去看看相關原始碼,來確認一下看看。
推薦閱讀
大家好,我是 dasu,歡迎關注我的公眾號(dasuAndroidTv),如果你覺得本篇內容有幫助到你,可以轉載但記得要關注,要標明原文哦,謝謝支援~