Bitmap基本概念及在Android4.4系統上使用BitmapFactory的注意事項

本文首先總結一下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);  
    } 
二、在Android4.4系統上使用BitmapFactory.options的注意事項
前段時間將手機的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.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()的源碼。

   /**
     * 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的系統上跑跑看,觀察內存佔用的情況。解決上面的問題辦法其實也很簡單,大家可以想想看。



發佈了49 篇原創文章 · 獲贊 21 · 訪問量 81萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章