標籤:
本文首先總結一下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的代碼:
[java] view plaincopy
- 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);
- }
二、在Android4.4系統上使用BitmapFactory.options的注意事項
前段時間將手機的Android系統升級到4.4之後,發現之前開發的App運行起來非常的卡,嚴重影響了使用者體驗。後來發現跟Bitmap.decodeByteArray的底層實現有關。本文將對問題原因進行總結,希望大家寫代碼時能留意一下,因為這種問題一旦遇到,要花很多時間才能發現原因。
我們的代碼能根據螢幕的密度對圖片進行縮放,因此我們使用最大的圖片資源,這樣的話對於任何的手機螢幕,都會對映像進行壓縮,不會造成視覺上的問題。圖片解碼前需要對BitmapFactory.Options進行設定,部分代碼如下:
[java] view plaincopy
- 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.inTargetDensity表示的是目標Bitmap即將被畫到螢幕上的像素密度(每英寸有多少個像素)。這個屬性往往會和options.inDensity和options.inScaled一起來覺得目標bitmap是否需要進行縮放。若果這個值為0,則BitmapFactory.decodeResource(Resources, int)和BitmapFactory.decodeResource(Resources, int, android.graphics.BitmapFactory.Options)decodeResourceStream(Resources, TypedValue, InputStream, Rect, BitmapFactory.Options) 將inTargetDensity用DisplayMetrics.densityDpi來設定,其它函數則不會對bitmap進行任何縮放。
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()的源碼。
[java] view plaincopy
- /**
- * 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)
- */
- public static Bitmap decodeByteArray(byte[] data, int offset, int length, Options opts) {
- if ((offset | length) < 0 || data.length < offset + length) {
- throw new 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) {
- throw new 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,
因為scale = (float) targetDensity / density;所以縮放倍速由inTargetDensity和inDensity兩個值確定。
可知在native層,當scale不等於1.0時會對圖片進行縮放,長寬縮放方式如下:
好了,就寫到這兒,大家可以查查以前寫的代碼,如果有根據螢幕密度載入Bitmap的部分,請將App在4.4的系統上跑跑看,觀察記憶體佔用的情況。解決上面的問題辦法其實也很簡單,大家可以想想看。
Bitmap基本概念及在Android4.4系統上使用BitmapFactory的注意事項