Android圖片佔用內存探索

轉自:請叫我大蘇android

連接:算法

https://www.jianshu.com/p/3c5ac5fdb62aapi


最近封裝了個高斯模糊組件,正好將圖片相關的理論基礎也梳理了下,因此,此次就來說講,在 Android 中,怎麼計算一張圖片在內存中佔據的大小,若是要優化,能夠從哪些方向着手。緩存


1
提問



閱讀本篇以前,先來想一些問題:bash


Q1:一張 png 格式的圖片,圖片文件大小爲 55.8KB,那麼它加載進內存時所佔的大小是多少?網絡


Q2:爲何有時候,同一個 app,app 內的同個界面,界面上同張圖片,但在不一樣設備上所耗內存卻不同?app


Q3:同一張圖片,在界面上顯示的控件大小不一樣時,它的內存大小也會跟隨着改變嗎?ide


Q4:圖片佔用的內存大小公式:圖片分辨率 * 每一個像素點大小,這種說法正確嗎,或者嚴謹嗎?測試


Q5:優化圖片的內存大小有哪些方向能夠着手?優化


2
正文


在 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 上那麼多強大的圖片開源庫,而不一樣的圖片開源庫,內部對於圖片的加載處理,緩存策略,複用策略都是不同的。


因此,若是使用了某個圖片開源庫,那麼對於加載一張圖片到內存中佔據了多大的空間,就須要你深刻這個圖片開源庫中去分析它的處理了。


由於基本全部的圖片開源庫,都會對圖片操做進行優化,那麼下面就繼續來說講圖片的優化處理吧。


3
圖片優化



有了上述的理論基礎,如今再來想一想若是圖片佔用內存空間太多,要進行優化,能夠着手的一些方向,也比較有眉目了吧。


圖片佔據內存大小的公式也就是:分辨率*像素點大小,只是在某些場景下,好比圖片的來源是 res 的話,可能最終圖片的分辨率並非原圖的分辨率而已,但歸根結底,對於計算機來講,確實是按照這個公式計算。


因此,若是單從圖片自己考慮優化的話,也就只有兩個方向:


  • 下降分辨率

  • 減小每一個像素點大小


除了從圖片自己考慮外,其餘方面能夠像內存預警時,手動清理,圖片弱引用等等之類的操做。


減小像素點大小


第二個方向很好操做,畢竟系統默認是以 ARGB_8888 格式進行處理,那麼每一個像素點就要佔據 4B 的大小,改變這個格式天然就能下降圖片佔據內存的大小。


常見的是,將 ARGB_8888 換成 RGB_565 格式,但後者不支持透明度,因此此方案並不通用,取決於你 app 中圖片的透明度需求,固然也能夠換成 ARGB_4444,但會大大下降圖片質量,Google 官方並不推薦。


因爲基本是使用圖片開源庫了,如下列舉一些圖片開源庫修改像素點格式的處理:


//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() 方法。


4
總結


最後,來稍微總結一下:


  • 一張圖片佔用的內存大小的計算公式:分辨率 * 像素點大小;但分辨率不必定是原圖的分辨率,須要結合一些場景來討論,像素點大小就幾種狀況:ARGB_8888(4B)、RGB_565(2B) 等等。

  • 若是不對圖片進行優化處理,如壓縮、裁剪之類的操做,那麼 Android 系統會根據圖片的不一樣來源決定是否須要對原圖的分辨率進行轉換後再加載進內存。

  • 圖片來源是 res 內的不一樣資源目錄時,系統會根據設備當前的 dpi 值以及資源目錄所對應的 dpi 值,作一次分辨率轉換,規則以下:新分辨率 = 原圖橫向分辨率 * (設備的 dpi / 目錄對應的 dpi ) * 原圖縱向分辨率 * (設備的 dpi / 目錄對應的 dpi )。

  • 其餘圖片的來源,如磁盤,文件,流等,均按照原圖的分辨率來進行計算圖片的內存大小。

  • jpg、png 只是圖片的容器,圖片文件自己的大小與它所佔用的內存大小沒有什麼關係,固然它們的壓縮算法並不同,在解碼時所耗的內存與效率此時就會有些區別。

  • 基於以上理論,如下場景的出現是合理的:

  • 同個 app,在不一樣 dpi 設備中,同個界面的相同圖片所佔的內存大小有可能不同。

  • 同個 app,同一張圖片,但圖片放於不一樣的 res 內的資源目錄裏時,所佔的內存大小有可能不同。

  • 以上場景之所說有可能,是由於,一旦使用某個熱門的圖片開源庫,那麼,以上理論基本就不適用了。

  • 由於系統支持對圖片進行優化處理,容許先將圖片壓縮,下降分辨率後再加載進內存,以達到下降佔用內存大小的目的

  • 而熱門的開源圖片庫,內部基本都會有一些圖片的優化處理操做:

  • 當使用 fresco 時,無論圖片來源是哪裏,即便是 res,圖片佔用的內存大小仍舊以原圖的分辨率計算。

  • 當使用 Glide 時,若是有設置圖片顯示的控件,那麼會自動按照控件的大小,下降圖片的分辨率加載。圖片來源是 res 的分辨率轉換規則對它也無效。


本篇所梳理出的理論、基本都是經過總結別人的博客內容,以及本身作相關實驗驗證後,得出來的結論,正確性相比閱讀源碼自己梳理結論天然要弱一些,因此,若是有錯誤的地方,歡迎指點一下。有時間,也能夠去看看相關源碼,來確認一下看看。

相關文章
相關標籤/搜索