1、BitmapFactory解析Bitmap的原理
BitmapFactory提供的解析Bitmap的靜態工廠方法有以下五種:
Bitmap decodeFile(...) Bitmap decodeResource(...) Bitmap decodeByteArray(...) Bitmap decodeStream(...) Bitmap decodeFileDescriptor(...)
其中常用的三個:decodeFile、decodeResource、decodeStream。decodeFile和decodeResource其實最終都是調用decodeStream方法來解析Bitmap,decodeStream的內部則是調用兩個native方法解析Bitmap的:
nativeDecodeAsset() nativeDecodeStream()
這兩個native方法只是對應decodeFile和decodeResource、decodeStream來解析的,像decodeByteArray、decodeFileDescriptor也有專門的native方法負責解析Bitmap。
接下來就是看看這兩個方法在解析Bitmap時究竟有什麼區別decodeFile、decodeResource,查看後發現它們調用路徑如下:
decodeFile->decodeStreamdecodeResource->decodeResourceStream->decodeStream
decodeResource在解析時多調用了一個 decodeResourceStream 方法,而這個decodeResourceStream方法代碼如下:
public static Bitmap decodeResourceStream(Resources res, TypedValue value, InputStream is, Rect pad, Options opts) { if (opts == null) { opts = new Options(); } if (opts.inDensity == 0 && value != null) { final int density = value.density; if (density == TypedValue.DENSITY_DEFAULT) { opts.inDensity = DisplayMetrics.DENSITY_DEFAULT; } else if (density != TypedValue.DENSITY_NONE) { opts.inDensity = density; } } if (opts.inTargetDensity == 0 && res != null) { opts.inTargetDensity = res.getDisplayMetrics().densityDpi; } return decodeStream(is, pad, opts); }
它主要是對Options進行處理了,在得到 opts.inDensity 屬性的前提下,如果我們沒有對該屬性設定值,那麼將opts.inDensity = DisplayMetrics.DENSITY_DEFAULT;賦定這個默認的Density值,這個默認值爲160,爲標準的dpi比例,即在Density=160的設備上1dp=1px,這個方法中還有這麼一行
opts.inTargetDensity = res.getDisplayMetrics().densityDpi;
對 opts.inTargetDensity 進行了賦值,該值爲當前設備的densityDpi值,所以說在decodeResourceStream方法中主要做了兩件事:
1、對opts.inDensity賦值,沒有則賦默認值1602、對opts.inTargetDensity賦值,沒有則賦當前設備的densityDpi值
之後重點來了,之後參數將傳入decodeStream方法,該方法中在調用native方法進行解析Bitmap後會調用這個方法 setDensityFromOptions(bm, opts); :
private static void setDensityFromOptions(Bitmap outputBitmap, Options opts) { if (outputBitmap == null || opts == null) return; final int density = opts.inDensity; if (density != 0) { outputBitmap.setDensity(density); final int targetDensity = opts.inTargetDensity; if (targetDensity == 0 || density == targetDensity || density == opts.inScreenDensity) { return; } byte[] np = outputBitmap.getNinePatchChunk(); final boolean isNinePatch = np != null && NinePatch.isNinePatchChunk(np); if (opts.inScaled || isNinePatch) { outputBitmap.setDensity(targetDensity); } } else if (opts.inBitmap != null) { // bitmap was reused, ensure density is reset outputBitmap.setDensity(Bitmap.getDefaultDensity()); } }
該方法主要就是把剛剛賦值過的兩個屬性inDensity和inTargetDensity給Bitmap進行賦值,不過並不是直接賦給Bitmap就完了,中間有個判斷,當inDensity的值與inTargetDensity或與設備的屏幕Density不相等時,則將應用inTargetDensity的值,如果相等則應用inDensity的值。
所以總結來說, setDensityFromOptions 方法就是把 inTargetDensity 的值賦給Bitmap,不過前提是opts.inScaled = true;
進過上面的分析,可以得出這樣一個結論:
在不配置Options的情況下:
1、decodeFile、decodeStream在解析時不會對Bitmap進行一系列的屏幕適配,解析出來的將是原始大小的圖
2、decodeResource在解析時會對Bitmap根據當前設備屏幕像素密度densityDpi的值進行縮放適配操作,使得解析出來的Bitmap與當前設備的分辨率匹配,達到一個最佳的顯示效果,並且Bitmap的大小將比原始的大
1.1、關於Density、分辨率、-hdpi等res目錄之間的關係
DensityDpi | 分辨率 | res | Density |
---|---|---|---|
160dpi | 320×533 | mdpi | 1 |
240dpi | 480×800 | hdpi | 1.5 |
320dpi | 720×1280 | xhdpi | 2 |
480dpi | 1080×1920 | xxhdpi | 3 |
560dpi | 1440×2560 | xxxhdpi | 3.5 |
dp與px的換算公式爲:
px = dp * Density
1.2、DisplayMetrics::densityDpi與density的區別
getResources().getDisplayMetrics().densityDpi——表示屏幕的像素密度getResources().getDisplayMetrics().density——1dp等於多少個像素(px)
舉個栗子:在屏幕密度爲160的設備下,1dp=1px。在屏幕密度爲320的設備下,1dp=2px。所以這就爲什麼在安卓中佈局建議使用dp爲單位,因爲可以根據當前設備的屏幕密度動態的調整進行適配
2、Bitmap的優化策略
2.1、BitmapFactory.Options的屬性解析
BitmapFactory.Options中有以下屬性:
inBitmap——在解析Bitmap時重用該Bitmap,不過必須等大的Bitmap而且inMutable須爲true inMutable——配置Bitmap是否可以更改,比如:在Bitmap上隔幾個像素加一條線段 inJustDecodeBounds——爲true僅返回Bitmap的寬高等屬性 inSampleSize——須>=1,表示Bitmap的壓縮比例,如:inSampleSize=4,將返回一個是原始圖的1/16大小的Bitmap inPreferredConfig——Bitmap.Config.ARGB_8888等 inDither——是否抖動,默認爲false inPremultiplied——默認爲true,一般不改變它的值 inDensity——Bitmap的像素密度 inTargetDensity——Bitmap最終的像素密度 inScreenDensity——當前屏幕的像素密度 inScaled——是否支持縮放,默認爲true,當設置了這個,Bitmap將會以inTargetDensity的值進行縮放 inPurgeable——當存儲Pixel的內存空間在系統內存不足時是否可以被回收 inInputShareable——inPurgeable爲true情況下才生效,是否可以共享一個InputStream inPreferQualityOverSpeed——爲true則優先保證Bitmap質量其次是解碼速度 outWidth——返回的Bitmap的寬 outHeight——返回的Bitmap的高 inTempStorage——解碼時的臨時空間,建議16*1024
2.2、優化策略
1、BitmapConfig的配置
2、使用decodeFile、decodeResource、decodeStream進行解析Bitmap時,配置inDensity和inTargetDensity,兩者應該相等,值可以等於屏幕像素密度*0.75f
3、使用inJustDecodeBounds預判斷Bitmap的大小及使用inSampleSize進行壓縮
4、對Density>240的設備進行Bitmap的適配(縮放Density)
5、2.3版本inNativeAlloc的使用
6、4.4以下版本inPurgeable、inInputShareable的使用
7、Bitmap的回收
針對上面方案,把Bitmap解碼的代碼封裝成了一個工具類,如下:
public class BitmapDecodeUtil { private static final int DEFAULT_DENSITY = 240; private static final float SCALE_FACTOR = 0.75f; private static final Bitmap.Config DEFAULT_BITMAP_CONFIG = Bitmap.Config.RGB_565; private static BitmapFactory.Options getBitmapOptions(Context context) { BitmapFactory.Options options = new BitmapFactory.Options(); options.inScaled = true; options.inPreferredConfig = DEFAULT_BITMAP_CONFIG; options.inPurgeable = true; options.inInputShareable = true; options.inJustDecodeBounds = false; if (Build.VERSION.SDK_INT <= Build.VERSION_CODES.GINGERBREAD_MR1) { Field field = null; try { field = BitmapFactory.Options.class.getDeclaredField("inNativeAlloc"); field.setAccessible(true); field.setBoolean(options, true); } catch (NoSuchFieldException e) { e.printStackTrace(); } catch (IllegalAccessException e) { e.printStackTrace(); } } int displayDensityDpi = context.getResources().getDisplayMetrics().densityDpi; float displayDensity = context.getResources().getDisplayMetrics().density; if (displayDensityDpi > DEFAULT_DENSITY && displayDensity > 1.5f) { int density = (int) (displayDensityDpi * SCALE_FACTOR); options.inDensity = density; options.inTargetDensity = density; } return options; } public static Bitmap decodeBitmap(Context context, int resId) { checkParam(context); return BitmapFactory.decodeResource(context.getResources(), resId, getBitmapOptions(context)); } public static Bitmap decodeBitmap(Context context, String pathName) { checkParam(context); return BitmapFactory.decodeFile(pathName, getBitmapOptions(context)); } public static Bitmap decodeBitmap(Context context, InputStream is) { checkParam(context); checkParam(is); return BitmapFactory.decodeStream(is, null, getBitmapOptions(context)); } public static Bitmap compressBitmap(Context context,int resId, int maxWidth, int maxHeight) { checkParam(context); final TypedValue value = new TypedValue(); InputStream is = null; try { is = context.getResources().openRawResource(resId, value); return compressBitmap(context, is, maxWidth, maxHeight); } catch (Exception e) { e.printStackTrace(); } finally { if (is != null) { try { is.close(); } catch (IOException e) { e.printStackTrace(); } } } return null; } public static Bitmap compressBitmap(Context context, String pathName, int maxWidth, int maxHeight) { checkParam(context); InputStream is = null; try { is = new FileInputStream(pathName); return compressBitmap(context, is, maxWidth, maxHeight); } catch (FileNotFoundException e) { e.printStackTrace(); } finally { if (is != null) { try { is.close(); } catch (IOException e) { e.printStackTrace(); } } } return null; } public static Bitmap compressBitmap(Context context, InputStream is, int maxWidth, int maxHeight) { checkParam(context); checkParam(is); BitmapFactory.Options opt = new BitmapFactory.Options(); opt.inJustDecodeBounds = true; BitmapFactory.decodeStream(is, null, opt); int height = opt.outHeight; int width = opt.outWidth; int sampleSize = computeSampleSize(width, height, maxWidth, maxHeight); BitmapFactory.Options options = getBitmapOptions(context); options.inSampleSize = sampleSize; return BitmapFactory.decodeStream(is, null, options); } private static int computeSampleSize(int width, int height, int maxWidth, int maxHeight) { int inSampleSize = 1; if (height > maxHeight || width > maxWidth) { final int heightRate = Math.round((float) height / (float) maxHeight); final int widthRate = Math.round((float) width / (float) maxWidth); inSampleSize = heightRate < widthRate ? heightRate : widthRate; } if (inSampleSize % 2 != 0) { inSampleSize -= 1; } return inSampleSize <= 1 ? 1 : inSampleSize; } private static <T> void checkParam(T param){ if(param == null) throw new NullPointerException(); }
主要有兩類方法:
一、decodeBitmap:對Bitmap不壓縮,但是會根據屏幕的密度合適的進行縮放壓縮
二、compressBimtap:對Bitmap進行超過最大寬高的壓縮,同時也會根據屏幕的密度合適的進行縮放壓縮。
3、Bitmap優化前後性能對比
針對上面方案,做一下性能對比,圖片大小爲3.26M,分辨率爲2048*2048有兩臺設備:
3.1、density爲320的設備
3.2、density爲560的設備
可以看到,都是加載同一圖片,在高屏幕像素密度的設備下所需要的內存需要很大、載入內存中的Bitmap的寬高也因設備的屏幕像素密度也改變,正如上面分析的一樣,使用decodeResource會自動適配當前設備的分辨率達到一個最佳效果,而只有這個方法會自動適配其它方法將不會,依次思路,我們在封裝的工具類中在每一個方法都加入了依屏幕像素密度來自動適配,而在實際中並不需要那麼高清的圖片,所以我們可以根據設備的density來進行縮放,比如:在400>=density>240的情況下x0.8,在density>400的情況下x0.7,這樣Bitmap所佔用的內存將減少非常多,可以對面上面兩個圖片中bitmap和decodeBitmap兩個值的大小,decodeBitmap只是對density進行了一定的縮放,而佔用內存卻減少非常多,而且顯示效果也和原先的並無區別。之後對比我們進行了inSampleSize壓縮的圖片,進行壓縮後的效果也看不出太大區別,而佔用內存也減少了很多。
4、Bitmap的回收
4.1、Android 2.3.3(API 10)及以下的系統
在2.3以下的系統中,Bitmap的像素數據是存儲在native中,Bitmap對象是存儲在java堆中的,所以在回收Bitmap時,需要回收兩個部分的空間:native和java堆。即先調用recycle()釋放native中Bitmap的像素數據,再對Bitmap對象置null,保證GC對Bitmap對象的回收
4.2、Android 3.0(API 11)及以上的系統
在3.0以上的系統中,Bitmap的像素數據和對象本身都是存儲在java堆中的,無需主動調用recycle(),只需將對象置null,由GC自動管理