android使用inSampleSize、inScaled、inDensity、inTargetDensity對圖片進行縮放
本文首先總結一下Bitmap的相關概念,然後通過一個實際的問題來分析設定BitmapFactory.options的注意事項,以減少不必要的記憶體佔用率,避免發生OOM。
一、 Bitmap的使用trick
儘量不要使用setImageBitmap或setImageResource 或BitmapFactory.decodeResource來設定一張大圖, 因為這些函式在完成decode後,最終都是通過java層的createBitmap來完成的, 需要消耗更多記憶體。因此,改用先通過BitmapFactory.decodeStream方法,創建出一個bitmap,再將其設為ImageView的 source,decodeStream最大的祕密在於其直接呼叫 JNI >> nativeDecodeAsset() 來完成decode,無需再使用java層的createBitmap,從而節省了java層的空間。
如果在讀取時加上圖片的Config引數,可以更有效減少載入的記憶體,從而有效阻止丟擲out of Memory異常.另外,decodeStream直接拿的圖片來讀取位元組碼了,不會根據機器的各種解析度來自動適應,使用了decodeStream之後,需要在hdpi和mdpi,ldpi中配置相應的圖片資源, 否則在不同解析度機器上都是同樣大小(畫素點數量),顯示出來的大小就不對了。
BitmapFactory.Options.inPreferredConfig
* ALPHA_8:數字為8,圖形引數應該由一個位元組來表示,應該是一種8位的點陣圖
* ARGB_4444:4+4+4+4=16,圖形的引數應該由兩個位元組來表示,應該是一種16位的點陣圖.
* ARGB_8888:8+8+8+8=32,圖形的引數應該由四個位元組來表示,應該是一種32位的點陣圖.
* RGB_565:5+6+5=16,圖形的引數應該由兩個位元組來表示,應該是一種16位的點陣圖.
*
* ALPHA_8,ARGB_4444,ARGB_8888都是透明的點陣圖,也就是所字母A代表透明。
* ARGB_4444:意味著有四個引數,即A,R,G,B,每一個引數由4bit表示.
* ARGB_8888:意味著有四個引數,即A,R,G,B,每一個引數由8bit來表示.
* RGB_565:意味著有三個引數,R,G,B,三個引數分別佔5bit,6bit,5bit.
*
*
* BitmapFactory.Options.inPurgeable;
*
* 如果 inPurgeable 設為True的話表示使用BitmapFactory建立的Bitmap
* 用於儲存Pixel的記憶體空間在系統記憶體不足時可以被回收,
* 在應用需要再次訪問Bitmap的Pixel時(如繪製Bitmap或是呼叫getPixel),
* 系統會再次呼叫BitmapFactory decoder重新生成Bitmap的Pixel陣列。
* 為了能夠重新解碼影象,bitmap要能夠訪問儲存Bitmap的原始資料。
*
* 在inPurgeable為false時表示建立的Bitmap的Pixel記憶體空間不能被回收,
* 這樣BitmapFactory在不停decodeByteArray建立新的Bitmap物件,
* 不同裝置的記憶體不同,因此能夠同時建立的Bitmap個數可能有所不同,
* 200個bitmap足以使大部分的裝置重新OutOfMemory錯誤。
* 當isPurgable設為true時,系統中記憶體不足時,
* 可以回收部分Bitmap佔據的記憶體空間,這時一般不會出現OutOfMemory 錯誤。
下面給出一段讀取Bitmap的程式碼:
- public Bitmap readBitmap(Context context, int resId) {
- BitmapFactory.Options opts = new BitmapFactory.Options();
- opts.inPreferredConfig = Config.RGB_565;
- opts.inPurgeable = true;
- opts.inInputShareable = true;
-
InputStream is = context.getResources().openRawResource(resId);
- return BitmapFactory.decodeStream(is, null, opts);
- }
前段時間將手機的Android系統升級到4.4之後,發現之前開發的App執行起來非常的卡,嚴重影響了使用者體驗。後來發現跟Bitmap.decodeByteArray的底層實現有關。本文將對問題原因進行總結,希望大家寫程式碼時能留意一下,因為這種問題一旦遇到,要花很多時間才能發現原因。
我們的程式碼能根據螢幕的密度對圖片進行縮放,因此我們使用最大的圖片資源,這樣的話對於任何的手機螢幕,都會對影象進行壓縮,不會造成視覺上的問題。圖片解碼前需要對BitmapFactory.Options進行設定,部分程式碼如下:
- BitmapFactory.Options options = new BitmapFactory.Options();
- DisplayMetrics displayMetrics = context.getResources.getDisplayMetrics();
- ......
- options.inTargetDensity = displayMetrics.densityDpi;
- options.inScaled = true;
- //getBitmapDensity()用於設定圖片將要被顯示的密度。
- options.inDensity = getBitmapDensity();
- ......
- Bitmap bitmap = getBitmapFromPath(loadPath, options);
options.inDensity表示的是bitmap所使用的畫素密度。如果這個值和options.inTargetDensity不一致,則會對影象進行縮放。 如果被設定成0,則 decodeResource(Resources, int), decodeResource(Resources, int, android.graphics.BitmapFactory.Options), 和decodeResourceStream(Resources, TypedValue, InputStream, Rect, BitmapFactory.Options)將用螢幕密度值來設定這個引數,其它函式將不進行縮放。
圖片的縮放倍數是根據inTargetDensity/inDensity來計算得到的。
我們使用的圖片是640 * 1136,編碼格式ARGB_8888,則大小為 640*1136*4=291K。手機螢幕密度為480,則options.inTargtetDensity為480;inDensity被設定成160. 安照以上的設定,bitmap的大小將被放大9倍,圖片編碼後的大小應為640*1136*4=26M。我們的App中總共載入了3張這樣的圖片,故執行起來非常的卡。
但為何Android4.4之前的版本沒有這樣的問題,為此我們分析了Bitmap.decodeByteArray()的原始碼。
- /**
- * Decode an immutable bitmap from the specified byte array.
- *
- * @param data byte array of compressed image data
- * @param offset offset into imageData for where the decoder should begin
- * parsing.
- * @param length the number of bytes, beginning at offset, to parse
- * @param opts null-ok; Options that control downsampling and whether the
- * image should be completely decoded, or just is size returned.
- * @return The decoded bitmap, or null if the image data could not be
- * decoded, or, if opts is non-null, if opts requested only the
- * size be returned (in opts.outWidth and opts.outHeight)
- */
- publicstatic Bitmap decodeByteArray(byte[] data, int offset, int length, Options opts) {
- if ((offset | length) < 0 || data.length < offset + length) {
- thrownew ArrayIndexOutOfBoundsException();
- }
- Bitmap bm;
- Trace.traceBegin(Trace.TRACE_TAG_GRAPHICS, "decodeBitmap");
- try {
- bm = nativeDecodeByteArray(data, offset, length, opts);
- if (bm == null && opts != null && opts.inBitmap != null) {
- thrownew IllegalArgumentException("Problem decoding into existing bitmap");
- }
- setDensityFromOptions(bm, opts);
- } finally {
- Trace.traceEnd(Trace.TRACE_TAG_GRAPHICS);
- }
- return bm;
- }
我們發現,該函式會呼叫本地函式 nativeDecodeByteArray(byte[] data, int offset, int length, Options opts)來解析圖片。
android4.4以前的BitmapFactory.cpp中nativeDecodeByteArray呼叫doDecode函式時不會根據density進行縮放處理(沒有查到所有的4.4以前的所有程式碼,以4.2為例):
willscale這個引數決定了它是否能被縮放,由於沒有傳入scale值到doDecode中,scale一直使用預設值1.0f,所以willScale根據預設值計算將始終為false,即bitmap不會被縮放。
android4.4平臺nativeDecodeByteArray對doDecode的呼叫方式沒有改變,但改變了doDecode函式的實現,特別是對willScale的計算方式進行了修改
其中全域性變數gOptions_scaledFieldID為java檔案中BitmapFactory.Options的inScale變數在native層的id,
好了,就寫到這兒,大家可以查查以前寫的程式碼,如果有根據螢幕密度載入Bitmap的部分,請將App在4.4的系統上跑跑看,觀察記憶體佔用的情況。解決上面的問題辦法其實也很簡單,大家可以想想看。