BitmapFactory解析與Bitmap的記憶體優化
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解碼的程式碼封裝成了一個工具類,如下:
package com.myframe.utils;
import android.content.Context;
import android.graphics.Bitmap;
import android.graphics.BitmapFactory;
import android.os.Build;
import android.util.TypedValue;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.InputStream;
import java.lang.reflect.Field;
/**
* Bitmap解碼轉換工具
*/
publicclassBitmapDecodeUtil{
privatestaticfinalint DEFAULT_DENSITY =240;
privatestaticfinalfloat SCALE_FACTOR =0.75f;
privatestaticfinalBitmap.Config DEFAULT_BITMAP_CONFIG =Bitmap.Config.RGB_565;
/**
* 獲取優化的BitmapFactory.Options
* @param context
* @return
*/
privatestaticBitmapFactory.Options getBitmapOptions(Context context){
BitmapFactory.Options options =newBitmapFactory.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;
}
/**
* 通過資源ID解碼Bitmap
* @param context
* @param resId
* @return
*/
publicstaticBitmap decodeBitmap(Context context,int resId){
checkParam(context);
returnBitmapFactory.decodeResource(context.getResources(), resId, getBitmapOptions(context));
}
/**
* 通過資源路徑解碼Bitmap
* @param context
* @param pathName
* @return
*/
publicstaticBitmap decodeBitmap(Context context,String pathName){
checkParam(context);
returnBitmapFactory.decodeFile(pathName, getBitmapOptions(context));
}
/**
* 通過資源輸入流解碼Bitmap
* @param context
* @param is
* @return
*/
publicstaticBitmap decodeBitmap(Context context,InputStream is){
checkParam(context);
checkParam(is);
returnBitmapFactory.decodeStream(is,null, getBitmapOptions(context));
}
/**
* 通過資源ID獲取壓縮Bitmap
* @param context
* @param resId
* @param maxWidth
* @param maxHeight
* @return
*/
publicstaticBitmap compressBitmap(Context context,int resId,int maxWidth,int maxHeight){
checkParam(context);
finalTypedValue value =newTypedValue();
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();
}
}
}
returnnull;
}
/**
* 通過資源路徑獲取壓縮Bitmap
* @param context
* @param pathName
* @param maxWidth
* @param maxHeight
* @return
*/
publicstaticBitmap compressBitmap(Context context,String pathName,int