在Android開發中,咱們常常與Bitmap打交道,而對Bitmap的不恰當的操做常常會致使OOM(Out of Memory)。這篇文章咱們會介紹如何高效地在Android開發中使用Bitmap,在保證圖片顯示質量的前提下儘量佔用更小的內存。html
Android中的Bitmap對象是對位圖的抽象,它能夠從文件系統、資源文件夾、網絡等各類不一樣的來源獲取。位圖能夠看作是像素點的集合,本質上就是經過一系列二進制位來描述一張圖片,具備不一樣色彩格式的位圖使用不一樣數量的二進制位來描述一個像素點,於是圖片質量和圖片大小也就不一樣。android
首先,咱們來介紹下兩個名詞:density和densityDpi,它們的含義分別以下:git
density:能夠理解爲相對屏幕密度,咱們知道,1個DIP在160dpi的屏幕上大約爲1像素大小。咱們以160dpi爲基準線,density的值即爲相對於160dpi屏幕的相對屏幕密度。好比,160dpi屏幕的density值爲1, 320dpi屏幕的density值爲2github
Bitmap佔用的內存不只與它的像素點數和色彩格式有關,還和具體設備的屏幕密度、所在的drawable文件夾有關。下面咱們來經過一個實例介紹這些因素是如何影響Bitmap所佔用的內存的大小的。這裏咱們使用的虛擬機的屏幕密度爲240dpi,圖片文件(670 * 376)存放在drawable-xhdpi文件夾下。咱們能夠經過如下代碼獲取Bitmap對象並計算它所佔用的內存大小:數組
Bitmap bitmap = BitmapFactory.decodeResource(getResources(), R.drawable.size); int size = bitmap.getByteCount();
咱們能夠獲得size值爲567384。以上代碼中咱們經過getByteCount方法來獲取Bitmap對象以字節爲單位的大小,咱們來看一下這個方法的源碼:網絡
public final int getByteCount() { // int result permits bitmaps up to 46,340 x 46,340 return getRowBytes() * getHeight(); }
其中getHeight方法會返回Bitmap對象的mHeight實例域,也就是圖片的高度(單位爲px),而getRowBytes方法返回的是圖片的像素寬度與色彩深度的乘積。這樣綜合起來,咱們知道了getByteCount方法的返回值是這樣計算的:像素寬 * 像素高 * 色彩深度。其中色彩深度與Bitmap的色彩格式有關,默認爲ARGB_8888,也就是一個像素大小爲32位(4字節)。根據這個公式咱們來算一下:670 * 376 * 4 = 1007680。跟咱們獲得的567384差了很多,這是爲何呢?由於咱們沒有考慮到的圖片所在資源文件夾以及設備的屏幕密度。性能
這兩個參數分別對應這BitmapFactory中的inDensity和inTargetDensity。好比咱們的圖片在drawable-xhdpi文件夾下,那麼inDensity值就爲320;設備的屏幕密度爲240dpi,於是inTargetDensity的值就爲240。把圖片顯示到一個設備上要根據各自的屏幕密度進行縮放,這個縮放係數即爲inTargetDensity除以inDensity。具體解釋如下:咱們知道dpi表明着每inch的像素點數,那麼設圖片像素寬高分別爲pixWidth、pixHeight,咱們把圖片放到了drawable-xhdpi文件夾下(inDensity爲240dpi),pixWidth、pixHeight分別除以inDensity能夠獲得圖片的物理寬高(單位inch),而後咱們把這個物理寬高分別乘以設備的屏幕密度再相乘,也就能夠獲得目標設備上圖片的像素數了。按照這個過程咱們能夠獲得目標設備上圖片的像素數的計算公式:(pixWidth / inDensity * inTargetDensity) * (pixHeight / inDensity * inTargetDensity) 。將這個像素數乘以4就能夠獲得在內存中的大小了,咱們來驗證下:(670 / 320 * 240) * (376 / 320 *240) * 4 = 566830。和經過getByteCount獲得的值近似相等。關於爲何不相等,你們能夠參考這篇文章:Android 開發繞不過的坑:你的 Bitmap 究竟佔多大內存? 而在實際開發中,這種影響咱們一般能夠忽略。網站
上面咱們介紹了內存中Bitmap的大小的計算方法,咱們固然但願Bitmap在圖像品質能夠接受的前提下佔用儘量小的內存。下面咱們來介紹一下如何更加高效的加載Bitmap對象。ui
BitmapFactory類提供瞭如下四個靜態方法用來以不一樣的「原料」生產一個Bitmap對象:spa
咱們下面的講解主要圍繞decodeResource方法來進行,經過對它的options進行合理的配置,咱們就可以將Bitmap對象調整到令咱們滿意的大小。
要實現高效加載Bitmap,首先咱們要了解Options類的幾個參數,由於正是經過合理的配置這幾個參數,咱們纔可以實現高效的加載Bitmap對象。Options類是BitmapFactory的一個靜態內部類,咱們來看一下它的源碼:
1 public static class Options { 2 public Options() { 3 inDither = false; 4 inScaled = true; 5 inPremultiplied = true; 6 } 7 ... 8 public Bitmap inBitmap; //用於實現Bitmap的複用,下面會具體介紹 9 public int inSampleSize; //採樣率 10 public boolean inPremultiplied; 11 public boolean inDither; //是否開啓抖動 12 public int inDensity; //即上文咱們提到的inDensity 13 public int inTargetDensity; //目標屏幕密度,同上文提到的含義相同 14 public boolean inScaled; //是否支持縮放 15 public int outWidth; //圖片的原始寬度 16 public int outHeight; //圖片的原始高度 17 ... 18 }
下面咱們來具體介紹如何經過配置Options來實現Bitmap的高效加載。
在上面的源碼中,咱們看到Options類中存在一個inScaled參數,這個參數表示是否支持縮放,咱們從Options的默然構造方法中能夠看到這個參數被初始化爲了true,也就是說默認是支持縮放的。那麼將如何進行縮放呢?答案是根據縮放係數進行縮放。關於縮放係數的計算方法,其實咱們在講解如何計算內存中Bitmap的大小時已經介紹過了。縮放係數就是inDensity除以inTargetDensity。inDensity表示咱們的圖片所處的資源文件夾對應的dpi,inTargetDensity表示目標設備的屏幕密度。
經過以上的實踐咱們瞭解到了,就算不給decodeResource方法傳入Options對象,它也會根據縮放係數對Bitmap進行縮放。咱們固然也能夠手動設置縮放係數,下面咱們仍是拿上面那個圖片舉例子,請看如下代碼:
BitmapFactory.Options options = new BitmapFactory.Options(); options.inDensity = 160; options.inTargetDensity = 320; Bitmap bitmap = BitmapFactory.decodeResource(getResources(), R.drawable.size, options); int size = bitmap.getByteCount();
咱們先來計算下size應該爲多大:(670 / 160 * 320) * (376 / 160 * 320) *4 = 4030720。咱們運行程序,可獲得size的實際大小爲:4030720。因而可知咱們的設置生效了。
下面咱們來介紹inSampleSize這個參數,當這個參數爲1時,採樣後的圖片大小和原來同樣;當這個參數爲2時,採樣後的圖片寬高均爲原來的1/2,大小也就成了原來的1/4。也就是說,採樣後的大小等於原始大小除以採樣率的平方。官方文檔規定,inSampleSize的值應爲2的非負整數次冪(1,2,4,... ),不然會被系統向下取整並找到一個最接近的值。經過設置inSampleSize咱們就可以將圖片縮放到一個合理的大小,那麼該如何設置inSampleSize的值呢?在講解這個以前,咱們先來考慮如下狀況:咱們的ImageView的大小爲100 * 100,要顯示的圖片大小爲300 * 400,此時咱們應該將inSampleSize設爲多少呢。誰先咱們經過計算能夠獲得圖片寬是ImageView的3倍,而圖片高是ImageView的4倍。那麼咱們應該將圖片寬高縮小爲原來的4倍嗎?假如咱們把圖片寬高都變爲原來的1/4,那麼如今圖片大小爲75 * 100,ImageView大小爲100 * 100,圖片要顯示在ImageView中須要進行拉伸,而拉伸的話可能會致使圖片失真。因此咱們應該把圖片寬高變爲原來的1/3,以保證它不小於ImageView的大小,這樣儘管多佔用一些內存,但不會形成圖片質量的降低,這仍是頗有必要的。經過以上分析,咱們知道了在設置inSampleSize時應該注意使得縮放後的圖片大小不小於相應的ImageView大小。
計算inSampleSize的步驟一般以下:
BitmapFactory.Options options = new BitmapFactory.Options(); options.inJustDecodeBounds = true; BitmapFactory.decodeResource(getResources(), resId, options); //如今原始寬高以存儲在了options對象的outWidth和outHeight實例域中
1 //dstWidth和dstHeight分別爲目標ImageView的寬高 2 public static int calSampleSize(BitmapFactory.Options options, int dstWidth, int dstHeight) { 3 int rawWidth = options.outWidth; 4 int rawHeight = options.outHeight; 5 int inSampleSize = 1; 6 if (rawWidth > dstWidth || rawHeight > dstHeight) { 7 float ratioHeight = (float) rawHeight / dstHeight; 8 float ratioWidth = (float) rawWidth / dstHeight; 9 inSampleSize = (int) Math.min(ratioWidth, ratioHeight); 10 } 11 return inSampleSize; 12 }
以上代碼的邏輯很直接,惟一須要注意的就是要記得使採樣後的圖片可以「覆蓋」ImageView,以防止圖片質量降低。計算inSampleSize並加載採樣後圖片的完整demo請見這裏:計算inSampleSize並顯示圖片的完整示例
下面咱們來介紹下inBitmap這個參數的做用。
這個參數用來實現Bitmap內存的複用,但複用存在一些限制,具體體如今:在Android 4.4以前只能重用相同大小的Bitmap的內存,而Android 4.4及之後版本則只要後來的Bitmap比以前的小便可。使用inBitmap參數前,每建立一個Bitmap對象都會分配一塊內存供其使用,而使用了inBitmap參數後,多個Bitmap能夠複用一塊內存,這樣能夠提升性能。
關於這個複用Bitmap內存的詳細方法以及注意事項Android Developer網站已給出了詳細的說明(Managing Bitmap Memory)。這裏簡單的貼出部分示例代碼瞭解下它的大體用法:
private static void addInBitmapOptions(BitmapFactory.Options options, ImageCache cache) { // inBitmap only works with mutable bitmaps, so force the decoder to // return mutable bitmaps. options.inMutable = true; if (cache != null) { // Try to find a bitmap to use for inBitmap. Bitmap inBitmap = cache.getBitmapFromReusableSet(options); if (inBitmap != null) { // If a suitable bitmap has been found, // set it as the value of inBitmap. options.inBitmap = inBitmap; } } } static boolean canUseForInBitmap( Bitmap candidate, BitmapFactory.Options targetOptions) { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) { // From Android 4.4 (KitKat) onward we can re-use // if the byte size of the new bitmap is smaller than // the reusable bitmap candidate // allocation byte count. int width = targetOptions.outWidth / targetOptions.inSampleSize; int height = targetOptions.outHeight / targetOptions.inSampleSize; int byteCount = width * height * getBytesPerPixel(candidate.getConfig()); return byteCount <= candidate.getAllocationByteCount(); } // On earlier versions, // the dimensions must match exactly and the inSampleSize must be 1 return candidate.getWidth() == targetOptions.outWidth && candidate.getHeight() == targetOptions.outHeight && targetOptions.inSampleSize == 1; }
Android Developer上的 Displaying Bitmap Efficiently 系列教程對Android開發中如何高效使用Bitmap作出了權威地描述,學好這個系列,玩兒轉Bitmap天然就不在話下了:)
1. Displaying Bitmap Efficiently
2. Android 開發繞不過的坑:你的 Bitmap 究竟佔多大內存?
3. 《Android開發藝術探索》