探索Bitmap使用姿式

前言

早些時候對Android下GC調用時機比較好奇,因此寫了一些case測試各類狀況下Android GC調用時機與現象,感興趣的話能夠跳過去瞅瞅 : 《Android GC機制實踐調研》android

在這個過程當中發現一個讓人很是震驚的問題:從資源文件中加載一張110kb的圖片建立Bitmap對象,佔用的內存高達40MB! 爲何爲何爲何??git

因而這篇博客便產生了,我但願能夠經過一系列測試case,來了解Bitmap在各類場景下的各類使用姿式將會在內存佔用和加載速度兩方面都有哪些表現,從而從中探索可能的優化點和最佳實踐。github

各類場景下建立Bitmap內存佔用

從資源文件建立Bitmap

1.不一樣分辨率的drawable文件夾下加載相同素材,Bitmap的內存佔用大小

這裏咱們準備了一張117.16kb 1200*900的jpg圖片放到了res/各類分辨率的drawabe目錄下。對他們進行分別加載而後輸出各類值進行對比,須要說明一下這裏加載的意思能夠是:執行bitmapFactory.decodeResource 。 與給ImageView設置Resource 、給佈局設置背景等建立建立Bitmap或進行圖片顯示的操做相同。web

看下實驗數據編程

【努比亞Z9 Nubia NX508J】 分辨率1080 * 1920 像素密度:424ppi數組

文件夾 getByteCount getRowBytes getHeight getWidth
drawable 38880000b ≈ 37mb 14400b 2700 3600
mdip 38880000b ≈ 37mb 14400b 2700 3600
xhdip 9720000b ≈ 9mb 7200b 1350 1800
xxhdip 4320000b ≈ 4mb 4800b 900 1200

38880000b是什麼概念?37MB!! 想一下,你的應用還啥都沒幹呢,就僅是加載了一張圖片將近40MB的內存就被佔用了,再加上其餘一些操做,內存妥妥的就跳到臨界值了,若是再有一些不當的溢出,OOM指日可待!緩存

彷佛,圖片放在分辨率越高的文件夾下,內存佔用越小性能優化

2.不一樣格式的圖片建立Bitmap內存佔用大小

上面測試用的是jpg,而一般咱們開發中使用的都是png,看到這麼大的內存佔用,我有想過是不是由於圖片格式的問題,因而把這張圖片丟到美圖秀秀裏(美圖秀秀真好用),而後分別導出了長寬同樣的jpg和png兩張圖片,放到資源文件夾中進行加載。bash

【努比亞Z9  Nubia NX508J】
drawable_jpg_1.jpg 1200*900  135.76kb
drawable_png_1.png 1200*900  1.64mb

jpg getByteCount : 38880000 getRowBytes:14400 getHeight:2700 getWidth:3600
png getByteCount : 38880000 getRowBytes:14400 getHeight:2700 getWidth:3600
複製代碼

內存佔用和以前同樣,而且雖然png的圖片自己高達1.64mb,但內存佔用依然只是37mb。網絡

從資源文件中加載圖片的內存佔用與圖片格式、圖片佔硬盤大小無關!(但和apk包體積有關)

3.不一樣的分辨率的設備加載同一張素材,Bitmap內存佔用大小

Android存在着不少分辨率適配問題,不一樣drawable文件夾也是爲了適配而存在的,因此咱們還要挑幾個分辨率不同的手機看一下:

【榮耀暢玩4X】 分辨率:1280 * 720 像素密度:267ppi

文件夾 getByteCount getRowBytes getHeight getWidth
drawable 17280000b ≈ 16mb 9600b 1800 2400
mdip 17280000b ≈ 16mb 9600b 1800 2400
xhdip 4320000b ≈ 4mb 4800b 900 1200
xxhdip 1920000b ≈ 2mb 3200b 600 800

誒?很明顯啊,選一個分辨率低一點的手機,果真相同條件的圖片加載內存佔用是不同的。我這正好還有一個和努比亞分辨率同樣的手機,用這個也測一下:

【樂視 le x620】 分辨率:1080 * 1920 像素密度:401ppi

文件夾 getByteCount getRowBytes getHeight getWidth
drawable 29773800b ≈ 28mb 12600b 2363 3150
mdip 29773800b ≈ 28mb 12600b 2363 3150
xhdip 7440300b ≈ 7mb 6300b 1181 1575
xxhdip 3309600b ≈ 3mb 4200b 788 1050

問題來了,雖然分辨率是同樣的,可是內存佔用卻不一樣,關鍵因素不在分辨率,那在什麼呢?

咱們都知道咱們的應用程序在不一樣的設備上,Android系統會從不一樣的資源文件夾下獲取圖片資源,而其選擇的本質不是屏幕的長寬比,是像素密度。

因此這裏的關鍵在於像素密度!從資源文件中加載圖片的內存佔用與像素密度有關!

OK,上面的結論都是經過數據推理出來的一些表象現狀。這裏先進行一個小總結:

  • 從資源文件中建立Bitmap,圖片所在分辨率越高的drawable文件夾,Bitmap佔用內存越小。(單從內存的角度能夠這樣考量,但從實際應用過程當中,全部素材都放到分辨率最高的文件夾並非合適的作法)
  • 從資源文件中建立Bitmap,Bitmap佔用內存大小與圖片寬高極爲有關,與圖片自己格式以及佔硬盤大小無關。
  • 從資源文件中建立Bitmap,Bitmap佔用內存大小與手機像素密度極爲有關。

從網絡或本地存儲建立Bitmap

經過資源文件建立Bitmap,Android系統會爲了適配不一樣屏幕,而對圖片進行一些調整,致使不一樣狀況下內存佔用區別很大。那麼若是是從網絡或本地存儲中建立的Bitmap也會由於設備的像素密度而有很大差別嗎?

咱們來實驗一下,我從網絡下載一張圖片,而後觀察內存狀況。

我選了一張216932b ≈ 212kb 1600 *1280 的jpg圖片下載,並建立一個Bitmap

【努比亞Z9  Nubia NX508J 分辨率1080 * 1920  像素密度:424ppi 】
網絡下載:
byte[] size : 216932 ≈ 212kb
bitmap size : 8192000 ≈ 7.8125mb
同一張圖片放到資源文件中加載:
drawable getByteCount : 73728000 ≈ 70mb getRowBytes:19200 getHeight:3840 getWidth:4800
mdip getByteCount : 73728000 ≈ 70mb getRowBytes:19200 getHeight:3840 getWidth:4800
xhdip getByteCount : 18432000 ≈ 17.5mb getRowBytes:9600 getHeight:1920 getWidth:2400
xxhdip getByteCount : 8192000 ≈ 8mb getRowBytes:6400 getHeight:1280 getWidth:1600
複製代碼

Bitmap大小仍是要比圖片自己大出好多,並且彷佛和從xxhdip文件夾下加載大小是同樣的,這一個示例不足以證實是否和手機分辨率有關,咱們換個手機再看看:

【魅族MX6 分辨率1080 * 1920  像素密度:401ppi 】
網絡下載:
byte[] size : 216932 ≈ 212kb
bitmap size : 8192000 ≈ 7.8125mb
資源文件加載:
drawable getByteCount : 73728000 ≈ 70mb getRowBytes:19200 getHeight:3840 getWidth:4800
mdip getByteCount : 73728000 ≈ 70mb getRowBytes:19200 getHeight:3840 getWidth:4800
xhdip getByteCount : 18432000 ≈ 17.5mb getRowBytes:9600 getHeight:1920 getWidth:2400
xxhdip getByteCount : 8192000 ≈ 8mb getRowBytes:6400 getHeight:1280 getWidth:1600
複製代碼

好像看起來同樣,不過這兩臺設備分辨率同樣,像素密度也差不太多,仍是不足以說明問題,咱們找個像素密度更低一點的看一下:

【虛擬機-5.4FWVGA 分辨率480 * 584  像素密度:mdpi 】
網絡下載:
byte[] size : 216932 ≈ 212kb
bitmap size : 8192000 ≈ 7.8125mb
資源文件加載:
drawable getByteCount : 8192000 ≈ 8mb getRowBytes:6400 getHeight:1280 getWidth:1600
mdip getByteCount : 8192000 ≈ 8mb getRowBytes:6400 getHeight:1280 getWidth:1600
xhdip getByteCount : 2048000 ≈ 2mb getRowBytes:3200 getHeight:640 getWidth:800
xxhdip getByteCount : 910364 ≈ 1mb getRowBytes:2132 getHeight:427 getWidth:533
複製代碼

哦~ 這回有點說明性了,即便在像素密度不一樣狀況下,從網絡下載的圖片建立的Bitmap大小都是固定的,從資源文件中加載則由於像素密度不一樣會產生不少變化。

從網絡直接下載獲得的byte數組大小等同於原圖片大小,不經處理,直接用byte建立獲得Bitmap寬高會以原圖片寬高建立,獲得的Bitmap所佔內存遠大於原圖在硬盤上的大小。

作個小總結:

  • 從網絡或本地存儲加載圖片建立Bitmap,內存佔用僅與圖片自身寬高有關,與設備像素密度無關。
  • 從網絡或本地讀取的byte數組大小等同於圖片大小,未經處理建立Bitmap內存佔用遠大於byte數組大小。

Bitmap佔用內存的大小是如何計算的?

上一節的測試case,幫助咱們大概的瞭解了Bitmap不一樣場景下建立的一些特性,看起來頗有道理,但case覆蓋不夠充足的概括法並不足以服人。

但他確實已經激起了咱們很濃厚的興趣,因此下一步咱們要經過源碼來了解其中真正的原理。

Bitmap的源碼解析的細節比較繁瑣,有興趣能夠一層層追下去,這裏就直接放結果了。

仍是由於有適配的問題,因此咱們還要從兩個方面去說明:從網絡或本地加載,和從資源文件中加載。

從網絡或本地存儲加載圖片

從網絡或本地加載圖片不會受到設備像素密度影響,其內存佔用的大小能夠用下面的公式描述:

**size = 實際顯示的寬 * 實際顯示的高 * Bitmap.Config **

說到Bitmap.Config,這個又要老生常談了,Android爲圖片提供了4種解碼格式,不一樣的解碼格式佔用的內存大小不一樣,固然顯示效果也不一樣。

Format byte 說明
ARGB_8888 4b 此配置很是靈活,提供最好的質量。應儘量使用。
RGB_565 2b 此配置可能會根據源的配置產生輕微的視覺僞影。例如,沒有抖動,結果可能會顯示綠色的色調。爲了得到更好的效果,應該應用抖動。當使用不須要高色彩保真度的不透明位圖時,此配置可能頗有用。
ARGB_4444 2b 若是應用程序須要存儲半透明信息,並且還須要節省內存,則此配置最爲有用。(已廢棄)
ALPHA_8 1b 每一個像素存儲爲單透明(alpha)通道。這對於有效地存儲掩碼是很是有用的。沒有存儲顏色信息。經過這種配置,每一個像素須要1個字節的存儲器。

默認是ARGB_8888,雖然一直都在說建議不一樣狀況使用不一樣的解碼格式,但每每由於一些「不可抗拒」的因素,任什麼時候候咱們都在使用默認的解碼格式。後面第三節會對不一樣的解碼格式進行case測試。

從資源文件中加載圖片

從資源文件中加載圖片會受到drawble文件夾不一樣、設備像素密度影響,公式略微複雜一點:

scaledWidth = int(width * targetDensity / density + 0.5f) scaledHeight = int(height * targetDensity / density + 0.5f) size = scaledWidth * scaledHeight * Bitmap.Config

width和height是原素材大小; targetDensity 是設備像素密度; density 是素材所在drawable文件夾大小;

這裏要說明一下targetDensity 和 density 的值是怎麼來的。給一個表來講明:

名稱 density 像素密度範圍:targetDensity
mdpi 160dp 120dp ~ 160dp
hdpi 240dp 160dp ~ 240dp
xhdpi 320dp 240dp ~ 320dp
xxhdpi 480dp 320dp ~ 480dp
xxxhdpi 640dp 480dp ~ 640dp

圖片放到了哪一個文件夾,density的值就是多少,若是每一個文件夾都放了,Android會根據設備的像素密度自動選擇對應的文件夾。

而設備的像素密度每每並不會只有160、240、320、480、640這幾個,咱們能夠看到第一節測試數據的幾個設備像素密度都是 【努比亞Z9 像素密度:424ppi】 【榮耀暢玩4X 像素密度:267ppi】 【樂視 le x620 像素密度:401ppi】

這些像素密度值是硬件的實際參數,但在系統運行時,硬件須要給Android系統提供一個準確的整數值,一般你能夠粗略的將硬件實際像素密度套入上表中,去像素密度範文的最大值。但仍是會有一些特殊的設備不會取標準值,好比樂視le x620的像素密度並非標準的320dp或480dp,而是420dp。

因此設備像素密度在系統運行中的值咱們能夠經過下面的代碼獲取:

DisplayMetrics metric = new DisplayMetrics();
int densityDpi = metric.densityDpi;  // 屏幕密度DPI(120 / 160 / 240)
複製代碼

系統運行中取得的像素密度以下 【努比亞Z9 像素密度:480dp】 【榮耀暢玩4X 像素密度:320dp】 【樂視 le x620 像素密度:420dp】 若是素材在每一個文件夾都放了圖片,那麼會經過上表的像素密度範圍中尋找最佳的素材進行加載。

簡單總結一下:

  • Bitmap消耗內存大小主要取決於實際顯示的大小和每一個像素所佔的字節數
  • 從資源文件加載Bitmap時,還受設備像素密度與圖片所在文件夾表明的像素密度之比的影響

減小Bitmap的內存佔用

吶,如今要進入本文的重頭戲了,你固然不會看到如今網上大同小異的什麼不實際加載先獲取尺寸啊,各類壓縮方法啊什麼的說教類條目。

從公式引出的優化策略

第二節咱們介紹了Bitmap加載佔用內存的計算公式,經過公式咱們能夠很容易的得出一些減小Bitmap內存佔用的方法。

減少圖片實際顯示的長寬

一般來講咱們要顯示的圖片會大於控件自己的大小,這是一種很明顯的浪費,對圖片作適當的壓縮,貼近控件自己的大小能夠有效的減小內存佔用。主要用到的技術是 BitmapFactory.Options.inSampleSize屬性,這個屬性在Bitmap優化上已經被講過無數次了,咱們就很少介紹了。關鍵點:按照控件自己大小加載圖片

使用更合適的解碼格式加載Bitmap

Android提供了四種Bitmap解碼格式,每種格式佔用內存的大小不同,在合適的場景下選擇合適解碼格式能夠有效的減小內存佔用。這個雖然也是老生常談,但裏面會有一些不符合咱們默認觀念的東西,下面會詳細介紹。

爲應用提供知足當前設備像素密度的素材

Bitmap內存計算公式中除長、寬、解碼格式三者的乘積之外,還要乘以targetDensity與density比的平方。這是什麼概念呢?

若是咱們只提供了低像素密度的素材,那麼在高像素密度的設備上將佔用更大的內存。 反之,若是咱們只提供了高像素密度的素材,那麼在低像素密度的設備上將佔用更小的內存。

誒???好像發現了什麼??是否是咱們只要在xxhdpi甚至xxxxxxxhdpi中放素材,內存佔用將會變得很是很是小??這簡直新大陸啊。

若是問題真的這麼簡單,Android系統自己也不會提供那麼多像素密度的文件夾了,口說無憑,咱們寫個Demo看看效果。

設備信息:【虛擬機-5.4FWVGA 分辨率480 * 584 像素密度:mdpi 】 我將同一張圖片分別copy在和xxhdpi文件夾下和mhdpi文件夾下,而後進行顯示: (上面xxhdpi 下面 mhdpi)

很明顯的能夠看出來與設備像素密度相同的mhdpi文件夾下素材顯示正常,xxhdpi已經很是模糊了。

將素材放到高像素密度文件下,以求減小內存佔用是一個愚蠢的行爲。

那問題來了,爲了減小apk包大小(或者是懶),大多數開發者都只會在項目中存放一套素材放到某個像素密度的文件夾下。 這樣將引發的問題是:若放到低像素密度文件夾下,遇到高像素密度設備時將佔用多餘的內存;若放到高像素密度文件夾下,遇到低像素密度設備,素材將會變的模糊。

很痛苦對不對?因此若是對包的大小要求並無那麼嚴格,設定多套像素密度素材,讓targetDensity與density比爲1,保證顯示效果與內存佔用保持在最恰當的平衡纔是正道。 但若是就只能用一套呢?要想辦法走歪路了……

素材大部分的應用都是一些尺寸較小控件,小尺寸控件即便圖片較爲模糊也不會特別明顯,因此這些小素材咱們能夠選擇性忽略,是否是有點不放心?咱們再跑下Demo看看效果。

設備信息:【虛擬機-5.4FWVGA 分辨率480 * 584 像素密度:mdpi 】 下面是長寬150dp的控件,上面是xxhdpi下的素材,下面是mhdpi的素材。

相同的設備相同的素材,縮小了控件大小後模糊的是否是不那麼明顯了?

那麼對於大尺寸的控件呢?這裏個人建議是放到assets或res/raw、中,從assets中加載圖片等同於從網絡或本地加載,從raw中經過InputStream加載也能夠實現一樣的效果,不會受到像素密度干擾。咱們能夠在assets中放一張相對尺寸較大的圖片,而後依照控件大小加載Bitmap,在保證以最優內存佔用的同時保證圖片不會模糊。

固然若是圖片放到了src/drawable文件夾下,經過代碼BitmapFactory.decodeStream(getResources().openRawResource(R.drawable.example)); 效果等同於放到res/raw,但這時編譯器會提示這裏指望的是raw類型,一條紅色的波浪線老是讓人難以接受且這樣的圖片容易被直接使用而致使上面提到問題。

將上面的代碼封裝到一個方法裏能夠避免這條紅線,但仍是不能避免會有其餘的小夥伴直接當作資源使用這張圖片。你們本身選擇吧

下面咱們就看看分別放到xxhdpi、assets下面的對比圖。 設備信息:【虛擬機-5.4FWVGA 分辨率480 * 584 像素密度:mdpi 】 上面是xxhdpi下的素材,下面是assets的素材。

又見清晰的屁股。 固然內存佔用上上面模糊的圖會更小,畢竟targetDensity與density比爲0.25,至關於除以4。 不過這是一種在內存佔用、展現效果、Apk包大小三者間較爲平和的加載方式。

此類方法適用於:全屏類型的展現素材(Splash、引導圖等)、大尺寸的示例圖片等。

不一樣解碼格式的效果

上面咱們遺留一個問題,如何使用更合適的解碼格式加載Bitmap?下面就好好聊聊。

一直以來,Bitmap優化老生常談的一個問題:使用不一樣的Bitmap解碼格式,以下降Bitmap內存佔用。但實際過程當中咱們都但願圖片以最優的情況展現給用戶,因此用的最多的是ARGB_8888.

這裏我好奇的是他們之間究竟有多少差別,分別適應什麼場景,我作了一些測試。

奧~測試以前,再把四種解碼格式的介紹列一下吧:

  • ALPHA_8模式 ALPHA_8模式表示的圖片信息中只包含Alpha透明度信息,不包含任何顏色信息,因此ALPHA_8模式只能用在一些特殊場景。

  • RGB_565模式 顯然RGB_565模式不能表示全部的RGB顏色,它能表示的顏色數只有32 × 64 × 32 = 65536種,遠遠小於24位真彩色所能表示的顏色數(256 × 257 × 256 = 16677216)。當圖片中某個像素的顏色不在RGB_565模式表示的顏色範圍內時,會使用相近的顏色來表示。

  • ARGB_4444模式 ARGB_4444已被Android標記爲@Deprecated,Android推薦使用ARGB_8888來代替ARGB_4444,緣由是ARGB_4444表示出來的圖片質量太差。

  • ARGB_8888模式 ARGB_8888模式用8位來表示透明度,有256個透明度等級,用24位來表示R,G,B三個顏色通道,可以徹底表示32位真彩色,但同時這種模式佔用的內存空間也最大,是RGB_565模式的兩倍,是ALPHA_8模式的4倍。

介紹是這麼寫的,但真實使用狀況是怎麼樣的?咱們來測試一下:

我準備了一張圖片而後分別使用不一樣的解碼格式進行解碼,而後進行展現並輸出Bitmap的大小:

最好的解碼方式展現最優的效果,固然內存佔用也是最大的:1038000 ≈ 0.98mb。

果真是要放棄的解碼格式,大腿都花掉了,雖然內存佔用小了將近一半,但也不能再用你了。

誒?這個看起來好像很不錯的樣子,內存佔用僅有ARGB_8888的四分之一,但現實上幾乎看不出什麼不一樣,仍是細膩的大腿。贊贊贊。(理論上size的大小不該該只有ARGB_4444的一半,應該是相等的,這個不能理解)

誒誒誒??ALPHA_8這麼強大嗎???一樣的幾乎無損圖,按照說明它應該是顯示最差的啊,不是說不包含顏色的嗎?。size的大小和RGB_565同樣又是怎麼回事???

好了,這裏簡單解釋一下,前面三張圖重複的展現ARGB_888八、ARGB_444四、RGB_565三種解碼格式在內存佔用上的不一樣。ARGB_4444展會效果太差已是不用質疑的了,但RGB_565內存佔用僅有ARGB_8888的四分之一,顯示上卻沒有明顯的區別,難道說能夠用RGB_565徹底的代替ARGB_8888嗎?

不不不,固然不是這樣的,咱們看下RGB_565的解釋:當圖片中某個像素的顏色不在RGB_565模式表示的顏色範圍內時,會使用相近的顏色來表示。 之因此咱們沒有感受到特別大的區別,緣由在與圖片自己色調過於單一(滿眼黃黃的大腿),RGB_565所能表示的顏色已經夠用或者代替的顏色色差足夠小。若是你須要展現色彩特別豐富的圖片仍是會看出區別的。

而後咱們再解釋一下ALPHA_8的問題。當你設置op1.inPreferredConfig = Bitmap.Config.ALPHA_8爲某個屬性時,並非說Bitmap解碼器必然使用這種解碼格式,僅是優先使用這種解碼格式。不包含顏色信息的ALPHA_8怎麼能解碼出來黃黃的大腿呢?ALPHA_8不能夠,RGB_565能夠。因此解碼器使用了RGB_565,具體其內部的優先級和使用策略尚未具體研究。

Bitmap解碼器最終使用的解碼格式在很大程度上取決於圖片自己。

既然上面的圖片ALPHA_8無法解碼,那黑白的二維碼圖片ALPHA_8能夠解碼嗎?試一下:

挺好的……

簡單的二維碼圖片,ARGB_4444也挑不出啥毛病來……

這回size的大小合理了,和ARGB_4444同樣。

更小的size,顯示效果也無不一樣。贊!

簡單總結一下:

  • 設置圖片解碼格式並不必定會使用這種解碼格式,關鍵取決與圖片自己。
  • ALPHA_8適合相似二維碼一類的簡單黑白圖
  • RGB_565彷佛能夠知足大多數要求不高的展現場景

Bitmap內存複用

一般來講咱們在須要使用一張新的圖片時,都會爲這個從新分配一塊內存,而後建立一個新的Bitmap對象,一個兩個不會存在太大的問題,但當有大量的零時Bitmap對象被頻繁建立時,將會引發頻繁的GC。因此Google在很早以前發佈的性能優化典範中推薦開發者使用inBitmap屬性來對Bitmap作內存複用,經過該屬性告知解碼器嘗試使用已經存在的內存區域,從而避免內存的從新分配。

固然inBitmap是有較大限制的,有着必定的場景依賴,因此一般被使用的頻率不是很高,具體限制咱們後面會有簡單提到。這裏咱們先經過Demo測試一下inBitmap的複用效果。

首先我用下面的方法測試未複用Bitmap內存的狀況下,在一個ImageView依次顯示三張圖片時內存佔用狀況:

private void unRecycle() {
        byte[] welcome1 = Tool.readFile(this, bitmapPaths[index++]);
        imageView.setImageBitmap(BitmapFactory.decodeByteArray(welcome1, 0, welcome1.length));
        if (index >= 3) {
            index = 0;
        }
    }
複製代碼

經過內存監控可知,三張圖片依次加載時,內存成階梯狀上升,執行GC後,內存成斷崖式下跌。在實際使用過程當中,極可能由於內存沒法即時回收而致使OOM,或由於大量內存須要回收而引發卡頓。

而後咱們在用下面的方法測試複用Bitmap內存的狀況下,在一個ImageView依次顯示三張圖片時內存佔用狀況:

byte[] welcome1 = Tool.readFile(this, bitmapPaths[index++]);
        if (bitmap == null) {
            BitmapFactory.Options option1 = new BitmapFactory.Options();
            option1.inMutable = true;
            bitmap = BitmapFactory.decodeByteArray(welcome1, 0, welcome1.length, option1);
        } else {
            BitmapFactory.Options option1 = new BitmapFactory.Options();
            option1.inBitmap = bitmap;
            option1.inMutable = true;
            bitmap = BitmapFactory.decodeByteArray(welcome1, 0, welcome1.length, option1);
        }
        imageView.setImageBitmap(bitmap);
        if (index >= 3) {
            index = 0;
        }
複製代碼

經過內存監控可知,僅在第一張圖片加載時,系統分配了一塊內存給Bitmap,後面兩張圖沒用再從新進行內存分配。避免了大塊內存的從新分配和GC回收。

Bitmap複用場景實操 - 拍照後圖片加載與顯示的優化對比

這裏介紹一個最簡單的適合使用inBitmap屬性的場景:拍照!

設備:【努比亞Z9 像素密度:480dp】 Demo的界面很簡單,一個ImageView用來展現圖片,初次進入默認展現示例圖片,點擊拍照按鈕調用系統相機進入拍照界面,成功拍照後將照片展現到ImageView上,可屢次拍照,ImageView僅展現最新照片。

這裏咱們考察的點是,進入Activity後進行屢次拍照,而後觀察內存變化。主要關注示例圖片的內存佔用與拍照後的內存佔用。 下面是Demo的界面展現,優化先後界面展現保持不變。考慮篇幅問題,這裏再也不貼代碼,僅以文字描述,詳細代碼能夠查看Demo代碼

老的拍照操做

先說咱們一般最普通的作法,僅作了簡單的拍照後圖片壓縮顯示。

1.示例圖片放在src/xhdpi文件夾下,經過photoImg.setImageResource(R.drawable.example);設置。 2.拍照後將圖片保存爲本地文件,在onActivityResult回調方法中。以默認長寬1024x768爲標準進行壓縮,經過BitmapFactory.decodeStream建立Bitmap。(默認長寬一般爲UED給出的設計稿尺寸)。

而後咱們看一下Demo跑起來之後的的內存監控圖:

解釋一下:

1.第一個內存上升主要是由於頁面進入後,加載示例圖形成的,大約佔用內存8MB左右。src/xhdpi與本次測試的設備像素密度相同,若是xxhdpi像素密度的設備,內存佔用更大;若是遇到像素密度更小的設備,則示例圖可能會變得模糊。

2.圓圈表示拍照後內存的上升,每一次拍照都將建立一個新的Bitmap,大約佔用內存9MB左右。

3.觀察第三個圓圈,系統發生一次GC,系統回收一個Bitmap,但顯而易見並無回收乾淨。

4.觀察第五個圓圈,出現一次內存尖峯,再次發生GC,但一樣沒有回收乾淨,內存總體呈持續持續上升趨勢。

總結:內存並無泄露,五次拍照均產生的爲臨時變量,但大內存的佔用致使GC回收很是不乾淨。在實際使用中,未被即時回收的內存將可能致使OOM。 即便不會引發OOM,大塊內存分配引發的GC一樣極易引發界面卡頓,GC運行在主線程。

新的拍照操做

針對上面老的拍照操做,新的拍照操做主要作了以下優化:

1.不在直接經過photoImg.setImageResource(R.drawable.example);設置圖片,改成BitmapFactory.decodeStream(getResources().openRawResource(srcId), null, options);。 提升Bitmap加載速度的同時(decodeStream直接調用JNI方法),跳過Android系統針對設備像素密度對圖片作的優化,直接對圖片自己進行操做。

2.以Config.RGB_565解碼格式進行解碼,縮小Bitmap一半內存佔用。

3.以ImageView實際大小爲標準對示例圖與照片作壓縮。

4.對屢次拍照產生的Bitmap作複用,最終實際僅佔用一個Bitmap內存。

咱們看下優化後的內存監控圖:

內存曲線過於平緩……看的不太清晰……

1.示例圖由於通過壓縮,且跳過像素密度的適配,最終僅佔用約0.3MB內存。

2.由於示例圖與壓縮後的照片尺寸不同,不能進行Bitmap複用,因此第一次拍照後又建立了一個Bitmap,大約佔用內存1.9MB,以後屢次拍照複用第二個Bitmap,沒有進行內存分配,因此也沒有GC發生。

總結

優化結果很明顯啦~ 主要的優化點:

  • 跳過像素密度適配直接經過 decodeStream對圖片進行加載。
  • 按照控件大小加載圖片。
  • 對Bitmap進行復用。

但裏面會有一些坑點:

  • 在Activity沒有將界面徹底展現時,沒法獲取控件寬高。此類場景如何獲取請自行搜索。我在這個Demo中使用的方式是imageView.post(new Runnable() { void run()}
  • Bitmap複用有較大限制,4.4以前只能複用大小同樣的,4.4以後只能複用大小等於或更小的。
  • Bitmap複用有較大限制,只能複用相同解碼格式的,可能會有某些圖片沒有辦法用Config.RGB_565解碼,此時將不能複用。Demo中我用try catch捕獲複用失敗的異常,而後降級建立新的Bitmap.

Bitmap加載速度探索

上面咱們主要分析的是Bitmap佔用內存方面的一些場景,在實際使用過程當中,除了內存之外,Bitmap的快速加載也是很是值得咱們關注的問題。

這裏咱們僅討論最經常使用的三種Bitmap加載方法。

//從資源文件中加載
BitmapFactory.decodeResource();
//從流中加載
BitmapFactory.decodeStream();
//從byte[]中加載
BitmapFactory.decodeByteArray();

複製代碼

從資源文件中加載與流中加載對比

我將同一張1080x1920 655.45k的圖片放在資源文件中和Assets目錄下用分別用BitmapFactory.decodeResource();BitmapFactory.decodeStream();兩種方法加載,而後測算其加載速度。

同時由於每一次Bitmap的加載耗時都不同,因此我會列出屢次執行的數據。

【time1】
資源文件加載Bitmap 耗時:160ms
decodeStream加載本地圖片 耗時:57ms
【time2】
資源文件加載Bitmap 耗時:157ms
decodeStream加載本地圖片 耗時:47ms
【time3】
資源文件加載Bitmap 耗時:162ms
decodeStream加載本地圖片 耗時:56ms
【time4】
資源文件加載Bitmap 耗時:124ms
decodeStream加載本地圖片 耗時:43ms
【time5】
資源文件加載Bitmap 耗時:123ms
07decodeStream加載本地圖片 耗時:43ms

複製代碼

數據已經很明顯的說明問題了。由於BitmapFactory.decodeResource()方法會在圖片加載完成後作一些適配工做,而decodeStream直接讀取了字節碼,速度更快。

但由於缺乏了適配處理,因此加載的圖片是圖片本來的大小,在使用中須要對其進行處理。但在加載一些明顯圖片尺寸大於控件尺寸的場景,decodeStream顯然更爲合適。

I/O耗時和圖片解碼耗時

從接觸編程開始,咱們都一直在接受I/O是很耗時的觀點。那麼是否能夠假想,在從本地文件中加載圖片的場景,從本地讀取數據到內存的過程消耗了很重要的一部分時間,不管這段時間多與少,都是一個優化點。

OK,那麼接下來咱們只要經過測算其具體時間就能夠驗證假設了。

仍是那張圖片,咱們先從本地讀取其爲byte[],而後在從byte[]經過BitmapFactory.decodeByteArray();轉爲Bitmap。

【time1】
讀取本地圖片到byte[] 耗時:1ms
byte[] to Bitmap 耗時:43ms

【time2】
讀取本地圖片到byte[] 耗時:2ms
byte[] to Bitmap 耗時:40ms

【time3】
讀取本地圖片到byte[] 耗時:1ms
byte[] to Bitmap 耗時:43ms

【time4】
讀取本地圖片到byte[] 耗時:1ms
byte[] to Bitmap 耗時:39ms

【time5】
讀取本地圖片到byte[] 耗時:1
byte[] to Bitmap 耗時:39

複製代碼

結果仍是較爲失望的,從本地讀取到內存中的時間消耗僅爲1ms,主要耗時依然在解碼上。

BitmapFactory.decodeByteArray()與BitmapFactory.decodeStream()對比

BitmapFactory.decodeStream()直接經過流讀取圖片字節碼,而後進行圖片解碼操做,對比BitmapFactory.decodeByteArray(),直觀上要多出一步本地到內存的過程,雖然從本地讀取數據到內存耗時僅爲1ms,但我仍是想知道這二者的直接對比是怎麼樣的。

【time1】
decodeStream加載本地圖片 耗時:42ms
讀取本地圖片到byte[] 再到Bitmap 耗時:40ms
讀取本地圖片到byte[] 耗時:1ms

【time2】
decodeStream加載本地圖片 耗時:55ms
讀取本地圖片到byte[] 再到Bitmap 耗時:44ms
讀取本地圖片到byte[] 耗時:1ms

【time3】
decodeStream加載本地圖片 耗時:43ms
讀取本地圖片到byte[] 再到Bitmap 耗時:40ms
讀取本地圖片到byte[] 耗時:1ms

【time4】
decodeStream加載本地圖片 耗時:85ms
讀取本地圖片到byte[] 再到Bitmap 耗時:60ms
讀取本地圖片到byte[] 耗時:2ms

【time5】
decodeStream加載本地圖片 耗時:73ms
讀取本地圖片到byte[] 再到Bitmap 耗時:57ms
讀取本地圖片到byte[] 耗時:2ms

複製代碼

時間相差從3ms到20ms都有,雖然不大,但仍是有一丟丟改善。 若是對圖片加載速度很是苛刻的話,能夠考慮提早將圖片緩存到內存中,而後經過BitmapFactory.decodeByteArray()方式進行加載。但這須要消耗額外的內存空間,是典型的空間換時間。但考慮其20ms左右優化效果,考慮這種方式還需謹慎。

新的緩存代替品?

跟上一節。雖然從加載速度考慮,BitmapFactory.decodeByteArray()代替BitmapFactory.decodeStream()的收益不大,但換一種姿式,有沒有可能讓收益翻番?

一直以來圖片緩存大多都是指將圖片保存到本地或網絡,加載後獲得Bitmap保存的內存中,其優化一般是指將用過的Bitmap用緩存容器保存起來避免重複從硬盤或網絡加載

這樣的方式咱們關注的更可能是減少Bitmap從本地或網絡建立的時間,但這樣的緩存方式將會佔用大量的內存空間,通常狀況咱們都會選擇將六分之一的內存空間劃分給圖片緩存,以空間換時間,其代價仍是很大的。

但看過前面的一大波測試數據,咱們能夠很明顯的感覺到加載後的Bitmap佔用內存大小遠大於圖片本來大小。究其緣由,加載Bitmap會對文件自己作解碼以用於顯示,相似於解壓操做,而圖片自己是一種壓縮操做。

同時通過前面的測試,也許你發現了一個細節,從網絡或本地讀取後獲得的byte[]大小是圖片本來大小,那麼是否能夠犧牲一些byte[]到Bitmap的轉換時間,僅緩存byte[]在內存中?

以時間換空間策略,是否可行的關鍵在於從byte[] - Bitmap的解碼時間與解釋的內存開銷的權衡,咱們經過數據來驗證。 我準備了一張png圖片,分別導出了不一樣的分辨率,而且copy一份對png文件進行壓縮作對比測試,而後運行代碼輸出其各方面數據。 這次測試咱們主要考量兩個標準:byte[]代替Bitmap節省的空間和byte[]轉Bitmap耗費的時間。

圖片文件分辨率 是否壓縮 byte.length bitmap.size use time
50*80 false 10501b≈10kb 17600b≈17kb 1ms
50*80 true 3523b≈3kb 17600b≈17kb 1ms
200*355 false 140360b≈137kb 284000b≈277kb 11ms
200*355 true 27690b≈27kb 284000b≈277kb 3ms
500*888 false 870554b≈850kb 1776000b≈1734kb 36ms
500*888 true 171101b≈167kb 1776000b≈1734kb 10ms
1080*1920 false 984712b≈961kb 8294400b≈8100kb 65ms
1080*1920 true 631610b≈616kb 8294400b≈8100kb 34ms

咱們對上面數據作一個簡單的總結:

  • png文件壓縮不會減小生成的Bitmap大小,但能夠明顯減小byte大小
  • 分辨率越高,byte[]替換Bitmap節省內存的越明顯
  • 分辨率越高,png解碼爲Bitmap的耗時越久
  • 壓縮後能夠明顯減小解碼爲Bitmap的耗時(byte[]越小,解碼越快)

同時咱們也知道byte[]到Bitmap佔用的時間並非一成不變的,也就是說會在不一樣的設備上有不一樣的體現,以我目前測試的努比亞Z9來講,不一樣數據的差別在10~15ms之間徘徊,爲了保證測試數據的說服力,我將1080*1920分辨率圖片壓縮先後的use time的屢次數據進行展現:

壓縮前 壓縮後
65 34
82 40
81 40
98 41
98 44
87 41

另外說道byte[]越小,解碼越快的問題,咱們不難聯想到webp,webp比png,jpg更小,讀取後的byte[]也更小,是否解壓的更快呢?測試一下:

ico_1080_1920.png  bytes:984712b ≈ 961kb  bitmap:8294400b ≈ 8100kb  use:91ms
ico_1080_1920_compress.png  bytes:631610b ≈ 616kb  bitmap:8294400b ≈ 8100kb  use:44ms
un_compress.webp  bytes:367018b ≈ 358kb  bitmap:8294400b ≈ 8100kb  use:152ms
compress.webp  bytes:361200b ≈ 352kb  bitmap:8294400b ≈ 8100kb  use:141ms

複製代碼

結果顯而易見,下面兩張圖是上面兩張圖的webp版,雖然大幅度減小byte的大小,但解碼時間也大幅度增長了。究其緣由,webp的高強度壓縮增長了解碼複雜度,webp在其官網也早已對這種狀況進行了說明。

而byte[] - Bitmap所消耗的時間對系統流程度的影響又是如何呢?

我寫了一個demo,界面以下:

通過實際測試,緩存Bitmap到內存中的策略中,第一次加載圖片時,快速滑動列表,會有明顯卡頓;但在圖片所有緩存後,頁面無卡頓。

而緩存byte[]到內存中,在顯示時才解碼爲Bitmap,第一次加載圖片時,快速滑動列表,會有明細卡頓;byte[]所有緩存後,普通滑動速度幾乎無卡頓;快速滑動有卡頓感。

因此從用戶體驗的角度上來講,緩存byte[]可能並不適合在圖片列表這樣能夠快速滑動的場景代替Bitmap緩存。 而在ViewPager這樣的場景,由於頁面轉換不可能像列表同樣快速,byte[] - Bitmap所消耗的時間幾乎無感,彷佛適合。 但在頁面展現如此遲鈍的場景,彷佛直接從文件中加載Bitmap纔是最優的選擇。

關於緩存替代品byte[] - Bitmap,仁者見仁智者見智吧。

(要提一點,爲了不byte[] - Bitmap的過程當中產生大量的臨時Bitmap對象,緩存byte[]的策略中應用了inBitmap屬性,而這一屬性的使用幾乎不會影響到Bitmap的加載速度)

相關文章
相關標籤/搜索