當前位置:
首頁 > 最新 > Bitmap 的高效載入

Bitmap 的高效載入

這篇文章來自我讀 《Android 開發藝術探索》 第 12 章《Bitmap 的載入和 Cache》時的筆記,我對內容進行了一些整理和擴展。


什麼是 Bitmap

Bitmap(點陣圖)是一種通過像素點陣來保存圖片信息的文件,它為每一個像素分配特定的位置和顏色值信息。根據位深度(每個像素使用多少位二進位位來保存信息),可將點陣圖分為1、4、8、16、24 及 32 點陣圖像等。

每個像素使用的信息位數越多,可用的顏色就越多,顏色表現就越逼真,相應的數據量越大。這樣在同等圖片尺寸下,位深度越大,Bitmap 文件佔用空間越大。

Bitmap 文件(.bmp)一般不直接用於網路傳輸而是使用 .jpg 等壓縮格式。

另外,Bitmap 在放大後會有失真現象。與之對應的另外一種圖片格式叫矢量圖,它用點、直線或者多邊形等基於數學方程的幾何圖元來表示圖像,這樣圖像放大後就不會出現失真。

在 Android 中,主要是通過BitmapFactory工廠類來載入,它提供了多個靜態方法,用於從不同來源載入Bitmap.

從位元組數組中載入

BitmapFactory.decodeByteArray(byte[] data, int offset, int length,BitmapFactory.Options opts)

BitmapFactory.decodeByteArray(byte[] data, int offset, int length)

從文件中載入

BitmapFactory.decodeFile(String pathName)

BitmapFactory.decodeFile(String pathName, BitmapFactory.Options opts)

從應用資源中載入

BitmapFactory.decodeResource(Resources res, int id)

BitmapFactory.decodeResource(Resources res, int id, BitmapFactory.Options opts)

從輸入流中載入

BitmapFactory.decodeStream(InputStream is)

BitmapFactory.decodeStream(InputStream is, Rect outPadding, BitmapFactory.Options opts)

從 FileDescriptor 中載入

BitmapFactory.decodeFileDescriptor(FileDescriptor fd)

BitmapFactory.decodeFileDescriptor(FileDescriptor fd, Rect outPadding, BitmapFactory.Options opts)

FileDescriptor用來表示打開的流、位元組或者Socket,一般通過FileInputStream和FileOutputStream來創建。 比如:FileDescriptor fd = fileInputStream.getFD()

示例代碼:

按照上面這樣的方式載入會遇到一個問題:當圖片太大時,會佔用較多的內存,而 Android 系統對每個應用所用內存的大小有限制,如果不經任何處理的載入多張,應用就會崩掉,然後會有一條異常信息:

java.lang.OutofMemoryError:bitmap size exceeds VM budget.

就是說 Bitmap 所佔內存超過了虛擬機分配的內存。 因此需要一些措施來避免這個問題。


仔細看上面列出的BitmapFactory的五種靜態方法,每一種都有一個帶有BitmapFactory.Options參數的重載方法。這個BitmapFactory.Options決定了BitmapFactory解析Bitmap的結果,它有兩個重要的欄位inSampleSize和inJustDecodeBounds可以解決Bitmap佔用內存過多的問題。

inSampleSize

這個值如果設置成大於,那麼BitmapFactory解析後的圖片將是原圖的縮放版本。

例如:設置inSampleSize = 2,那麼解析圖片的寬和高是原圖的,大小就是原圖的。

官方文檔中還有這麼一句:

Note: the decoder uses a final value based on powers of 2, any other value will be rounded down to the nearest power of 2.

也就是BitmapFactory最終採用的inSampleSize的值是的冪次。如果設置成其他數字那將採用小於這個數的的正數次冪。

例如把inSampleSize設置成,最終採用的將會是.

inJustDecodeBounds

如果這個參數設置為true,BitmapFactory只會解析圖片的寬高信息,並賦值給BitmapFactory.Options的outWidth和outHeight;設置為false時,BitmapFactory才會真正的解析圖片。

注意:BitmapFactory獲取的圖片的尺寸信息受運行設備的屏幕密度、圖片所在資源目錄影響。

使用套路

了解了上面兩個參數,再藉助BitmapFactory,就可以施展我們的載入套路了:

將BitmapFactory.Options的inJustDecodeBounds設為ture,通過BitmapFactory解析圖片尺寸信息

從BitmapFacory.Options中取出outWidth和outHeight

根據目標View的大小,計算出inSampleSize

為BitmapFacory.Options的inSampleSize賦值,並將inJustDecodeBounds設為false,開始解析圖片

示例代碼:

inSampleSize應該選擇寬高縮放比例中較小的一個。例如:ImageView尺寸為,而圖片尺寸為200 * 400,寬的縮放比例為,高的縮放比例為。如果inSampleSize設置為4,那麼載入後的圖片大小就是,這樣圖片比小,就會被拉伸,導致圖片失真;如果設為,載入的圖片大小為,這樣就不會被拉伸。

2. 方便起見,這裡通過decodeResource()來載入圖片,其他來源與此相同。


緩存的主要目的就是為用戶節省流量,同時提高載入速度,提升用戶體驗——同一個圖片如果已經通過網路載入過,那麼就將它保存在內存或存儲設備上,這樣當用戶再次請求該圖片時,先從內存或者存儲設備上獲取,如果沒有在通過網路載入。

當然,不只是圖片,其他文件(比如音樂和視頻文件)同樣也可以通過這種方式緩存。

通常緩存區會設定成一定的大小,不能佔用過多空間。這樣當存儲空間滿了的時候就會面對一個問題:如何處理新添加的數據與已經緩存了的數據。

一種常用的演算法是演算法,總體思想就是當存儲空間快要佔滿的時候,優先刪除那些近期最少使用的緩存對象。

採用演算法的緩存工具有和,它們內部都是通過一個來管理緩存數據。內部通過和雙向鏈表來管理對象,這樣既能根據進行迅速查找,又可以在訪問對象後及時調整元素間的順序(把最近一次訪問的對象放在表頭),以保證緩存空間滿的時候能優先刪除那些最近很少使用的數據。我們可以使用這兩個工具實現在運行內存和外部存儲中緩存文件。


LruCache是在Android 3.1(API level 12)時添加的 API,用於在中進行數據緩存。內部使用LinkedHashMap通過的方式保存value。每當一個被訪問,它就會被放到隊頭,當緩存區滿時,處於隊尾的就會被回收。

初始化

使用

還提供了來根據手動刪除緩存數據。


DiskLruCache並不是 Android SDK,但是獲得了谷歌官方推薦。它的作者是Jake Wharton。 它用來在上進行緩存,實現原理與類似,不過在中,一個值可以對應多個。 另外,的長度不能超過個字元並且只能包含a-z、-、_以及0-9。

DiskLruCache提供了edit方法用於寫入緩存,get方法用於讀取緩存。

這裡通過一個載入圖片的小 demo 來演示DiskLruCache的使用。

loadImg():通過一個載入圖片並設置給

loadImgFromCache(String url): 通過從緩存載入

DiskLruCache的get方法用於通過key來訪問value,會返回一個Snapshot對象,通過snapshot.getInputStream(int index)來拿到輸入流。

值是所對應的的索引,從開始。如果初始化時指定一個對應兩個,那麼時拿到的就是第二個的輸入流。如果緩存中還沒有添加過該,那麼就會返回。

initDiskLruCache(): 初始化DiskLruCache

DiskLruCache需要通過靜態方法open()來打開,這個方法需要四個參數:

File directory:緩存目錄,可以是應用自身目錄,也可以是公共目錄。注意:多個進程不要使用同一個緩存目錄。

int appVersion:App 版本,版本變化會清空所有緩存,一般設為

int valueCount:一個對應的數量,一般設為

long maxSize:最大緩存容量,單位為

loadImgFromNetwork:通過從網路中獲取圖片

LoadImgTask:非同步載入圖片並寫入緩存

DiskLruCache的get方法會返回一個editor對象,通過editor.newOutPutStream(int index)可以拿到輸出流,然後就可以向緩存中寫入數據。如果當前正在被編輯,也就是已經調用,那麼方法將返回。數據寫入完成後需要調用editor.commit()進行提交,如果寫入出錯還可以調用editor.abort()取消寫入。

如果需要通過BitmapFactory.Options來進行縮放,為了避免第一次解析輸入流的尺寸後,再一次通過解析輸入流獲取Bitmap為null,可以通過FileDescriptor來進行尺寸解析:

由於對 key 有限制,所以這裡用了一個keyForUrl(String url)方法來獲得 url 的 MD5 值,並以此作為 key :

其他 API

close():關閉已經打開的,與對應,可以在的中調用

size() :返回當前緩存所佔空間大小,單位是位元組

flush():同步日誌文件,不需要每次寫入都調用,可以在的方法中調用

delete():清空緩存 更多關於的內容可以通過源碼以及這篇博客了解。

為了更好的用戶體驗,可能還需要同時使用LruCache和DiskLruCache來進行二級緩存,這樣載入圖片時,程序先在內存中查找圖片 (對應LruCache),如果沒有,再到存儲設備中查找(對應DiskLruCache),如果還沒有,那就只能通過網路進行載入。


在使用RecyclerView、ListView或者GridView時,需要同時載入多張圖片,如果這時候用戶滾動列表過於頻繁,就會同時啟動許多圖片載入任務,造成頁面卡頓,滾動不流暢。

解決這個問題的辦法就是通過setOnScrollChangedListener()為布局添加滾動事件監聽,在OnScrollChangedListener當布局滾動時,停止載入;當布局停止滾動後,再開始載入對應圖片。

以ListView為例:

如果使用以上方法後還有明顯卡頓,可以在AndroidManifest.xml中為相應的Activity開啟硬體加速。


載入主要會遇到內存佔用過大和緩存的問題:通過縮放,可以減少應用內存佔用,避免的出現;通過內存和存儲設備緩存,為用戶節省流量,提高載入速度;另外還需要注意列表同時載入多張圖片時的卡頓問題。

玩《地球末日生存》突然連不上伺服器,難受...

生活不隻眼前的技術 還有詩和遠方


喜歡這篇文章嗎?立刻分享出去讓更多人知道吧!

本站內容充實豐富,博大精深,小編精選每日熱門資訊,隨時更新,點擊「搶先收到最新資訊」瀏覽吧!


請您繼續閱讀更多來自 wenhaiz 的精彩文章:

TAG:wenhaiz |