Android性能優化之Bitmap的內存優化

一、BitmapFactory解析Bitmap的原理

BitmapFactory提供的解析Bitmap的靜態工廠方法有如下五種:java

Bitmap decodeFile(...) Bitmap decodeResource(...) Bitmap decodeByteArray(...) Bitmap decodeStream(...) Bitmap decodeFileDescriptor(...)

其中經常使用的三個:decodeFile、decodeResource、decodeStream。 
decodeFile和decodeResource其實最終都是調用decodeStream方法來解析Bitmap,decodeStream的內部則是調用兩個native方法解析Bitmap的:android

nativeDecodeAsset() nativeDecodeStream()

這兩個native方法只是對應decodeFile和decodeResource、decodeStream來解析的,像decodeByteArray、decodeFileDescriptor也有專門的native方法負責解析Bitmap。api

接下來就是看看這兩個方法在解析Bitmap時究竟有什麼區別decodeFile、decodeResource,查看後發現它們調用路徑以下:bash

decodeFile->decodeStream 
decodeResource->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方法中主要作了兩件事:ui

一、對opts.inDensity賦值,沒有則賦默認值160 
二、對opts.inTargetDensity賦值,沒有則賦當前設備的densityDpi值spa

以後重點來了,以後參數將傳入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的狀況下: 
一、decodeFile、decodeStream在解析時不會對Bitmap進行一系列的屏幕適配,解析出來的將是原始大小的圖 
二、decodeResource在解析時會對Bitmap根據當前設備屏幕像素密度densityDpi的值進行縮放適配操做,使得解析出來的Bitmap與當前設備的分辨率匹配,達到一個最佳的顯示效果,而且Bitmap的大小將比原始的大

1.一、關於Density、分辨率、-hdpi等res目錄之間的關係

DensityDpi 分辨率 res Density
160dpi 320x533 mdpi 1
240dpi 480x800 hdpi 1.5
320dpi 720x1280 xhdpi 2
480dpi 1080x1920 xxhdpi 3
560dpi 1440x2560 xxxhdpi 3.5

dp與px的換算公式爲:

px = dp * Density

1.二、DisplayMetrics::densityDpi與density的區別

getResources().getDisplayMetrics().densityDpi——表示屏幕的像素密度 
getResources().getDisplayMetrics().density——1dp等於多少個像素(px)

舉個栗子:在屏幕密度爲160的設備下,1dp=1px。在屏幕密度爲320的設備下,1dp=2px。 
因此這就爲何在安卓中佈局建議使用dp爲單位,由於能夠根據當前設備的屏幕密度動態的調整進行適配

二、Bitmap的優化策略

2.一、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.二、優化策略

一、BitmapConfig的配置 
二、使用decodeFile、decodeResource、decodeStream進行解析Bitmap時,配置inDensity和inTargetDensity,二者應該相等,值能夠等於屏幕像素密度*0.75f 
三、使用inJustDecodeBounds預判斷Bitmap的大小及使用inSampleSize進行壓縮 
四、對Density>240的設備進行Bitmap的適配(縮放Density) 
五、2.3版本inNativeAlloc的使用 
六、4.4如下版本inPurgeable、inInputShareable的使用 
七、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(); } }

主要有兩類方法: 
1、decodeBitmap:對Bitmap不壓縮,可是會根據屏幕的密度合適的進行縮放壓縮 
2、compressBimtap:對Bitmap進行超過最大寬高的壓縮,同時也會根據屏幕的密度合適的進行縮放壓縮。

三、Bitmap優化先後性能對比

針對上面方案,作一下性能對比,圖片大小爲3.26M,分辨率爲2048*2048 
有兩臺設備:

3.一、density爲320的設備

這裏寫圖片描述

3.二、density爲560的設備

這裏寫圖片描述

能夠看到,都是加載同一圖片,在高屏幕像素密度的設備下所須要的內存須要很大、載入內存中的Bitmap的寬高也因設備的屏幕像素密度也改變,正如上面分析的同樣,使用decodeResource會自動適配當前設備的分辨率達到一個最佳效果,而只有這個方法會自動適配其它方法將不會,依次思路,咱們在封裝的工具類中在每個方法都加入了依屏幕像素密度來自動適配,而在實際中並不須要那麼高清的圖片,因此咱們能夠根據設備的density來進行縮放,好比:在400>=density>240的狀況下x0.8,在density>400的狀況下x0.7,這樣Bitmap所佔用的內存將減小很是多,能夠對面上面兩個圖片中bitmap和decodeBitmap兩個值的大小,decodeBitmap只是對density進行了必定的縮放,而佔用內存卻減小很是多,並且顯示效果也和原先的並沒有區別。 
以後對比咱們進行了inSampleSize壓縮的圖片,進行壓縮後的效果也看不出太大區別,而佔用內存也減小了不少。

四、Bitmap的回收

4.一、Android 2.3.3(API 10)及如下的系統

在2.3如下的系統中,Bitmap的像素數據是存儲在native中,Bitmap對象是存儲在Java堆中的,因此在回收Bitmap時,須要回收兩個部分的空間:native和java堆。 
即先調用recycle()釋放native中Bitmap的像素數據,再對Bitmap對象置null,保證GC對Bitmap對象的回收

4.二、Android 3.0(API 11)及以上的系統

在3.0以上的系統中,Bitmap的像素數據和對象自己都是存儲在java堆中的,無需主動調用recycle(),只需將對象置null,由GC自動管理

相關文章
相關標籤/搜索