最近封裝了個高斯模糊組件,正好將圖片相關的理論基礎也梳理了下,所以,這次就來講講,在 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_8888Fresco.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_8888Picasso.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 接口設置,允許我們對需要加載進內存的圖片先進行一定比例的壓縮,以減少內存占用。
而這些圖片開源庫,內部自然會利用系統的這些支持,做一些內存優化,可能還涉及其他圖片裁剪等等之類的優化處理,但不管怎么說,此時,系統原生的計算圖片內存大小的理論基礎自然就不適用了。
降低分辨率這點,除了圖片開源庫內部默認的優化處理外,它們自然也會提供相關的接口來給我們使用,比如:
//frescoImageRequestBuilder.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 的分辨率轉換規則對它也無效。
本篇所梳理出的理論、基本都是通過總結別人的博客內容,以及自己做相關實驗驗證后,得出來的結論,正確性相比閱讀源碼本身梳理結論自然要弱一些,所以,如果有錯誤的地方,歡迎指點一下。有時間,也可以去看看相關源碼,來確認一下看看。
本文在開源項目:https://github.com/Android-Alvin/Android-LearningNotes 中已收錄,里面包含不同方向的自學編程路線、面試題集合/面經、及系列技術文章等,資源持續更新中…