1. 程式人生 > >android使用inSampleSize、inScaled、inDensity、inTargetDensity對圖片進行縮放

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的程式碼:

  1. public Bitmap readBitmap(Context context, int resId) {    
  2.        BitmapFactory.Options opts = new BitmapFactory.Options();    
  3.        opts.inPreferredConfig = Config.RGB_565;    
  4.        opts.inPurgeable = true;    
  5.        opts.inInputShareable = true;    
  6.        InputStream is = context.getResources().openRawResource(resId);    
  7.        return BitmapFactory.decodeStream(is, null, opts);    
  8.    }   
二、在Android4.4系統上使用BitmapFactory.options的注意事項
前段時間將手機的Android系統升級到4.4之後,發現之前開發的App執行起來非常的卡,嚴重影響了使用者體驗。後來發現跟Bitmap.decodeByteArray的底層實現有關。本文將對問題原因進行總結,希望大家寫程式碼時能留意一下,因為這種問題一旦遇到,要花很多時間才能發現原因。

我們的程式碼能根據螢幕的密度對圖片進行縮放,因此我們使用最大的圖片資源,這樣的話對於任何的手機螢幕,都會對影象進行壓縮,不會造成視覺上的問題。圖片解碼前需要對BitmapFactory.Options進行設定,部分程式碼如下:

  1. BitmapFactory.Options options = new BitmapFactory.Options();  
  2. DisplayMetrics displayMetrics = context.getResources.getDisplayMetrics();  
  3. ......  
  4. options.inTargetDensity = displayMetrics.densityDpi;  
  5. options.inScaled = true;  
  6. //getBitmapDensity()用於設定圖片將要被顯示的密度。
  7. options.inDensity = getBitmapDensity();  
  8. ......  
  9. 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()的原始碼。

  1. /** 
  2.   * Decode an immutable bitmap from the specified byte array. 
  3.   * 
  4.   * @param data byte array of compressed image data 
  5.   * @param offset offset into imageData for where the decoder should begin 
  6.   *               parsing. 
  7.   * @param length the number of bytes, beginning at offset, to parse 
  8.   * @param opts null-ok; Options that control downsampling and whether the 
  9.   *             image should be completely decoded, or just is size returned. 
  10.   * @return The decoded bitmap, or null if the image data could not be 
  11.   *         decoded, or, if opts is non-null, if opts requested only the 
  12.   *         size be returned (in opts.outWidth and opts.outHeight) 
  13.   */
  14.  publicstatic Bitmap decodeByteArray(byte[] data, int offset, int length, Options opts) {  
  15.      if ((offset | length) < 0 || data.length < offset + length) {  
  16.          thrownew ArrayIndexOutOfBoundsException();  
  17.      }  
  18.      Bitmap bm;  
  19.      Trace.traceBegin(Trace.TRACE_TAG_GRAPHICS, "decodeBitmap");  
  20.      try {  
  21.          bm = nativeDecodeByteArray(data, offset, length, opts);  
  22.          if (bm == null && opts != null && opts.inBitmap != null) {  
  23.              thrownew IllegalArgumentException("Problem decoding into existing bitmap");  
  24.          }  
  25.          setDensityFromOptions(bm, opts);  
  26.      } finally {  
  27.          Trace.traceEnd(Trace.TRACE_TAG_GRAPHICS);  
  28.      }  
  29.      return bm;  
  30.  }  

我們發現,該函式會呼叫本地函式 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的系統上跑跑看,觀察記憶體佔用的情況。解決上面的問題辦法其實也很簡單,大家可以想想看。