Bitmap的分析與使用
-
Bitmap的建立
- 建立Bitmap的時候,Java不提供
new Bitmap()
的形式去建立,而是通過BitmapFactory
中的靜態方法去建立,如:BitmapFactory.decodeStream(is);//通過InputStream去解析生成Bitmap
(這裡就不貼BitmapFactory
中建立Bitmap
的方法了,大家可以自己去看它的原始碼),我們跟進
BitmapFactory中建立
Bitmap`的原始碼,最終都可以追溯到這幾個native函式
private static native Bitmap nativeDecodeStream(InputStream is, byte[] storage, Rect padding, Options opts); private static native Bitmap nativeDecodeFileDescriptor(FileDescriptor fd, Rect padding, Options opts); private static native Bitmap nativeDecodeAsset(long nativeAsset, Rect padding, Options opts); private static native Bitmap nativeDecodeByteArray(byte[] data, int offset, int length, Options opts); 複製程式碼
而
Bitmap
又是Java物件,這個Java物件又是從native,也就是C/C++中產生的,所以,在Android中Bitmap的記憶體管理涉及到兩部分,一部分是native,另一部分是dalvik,也就是我們常說的java堆(如果對java堆與棧不瞭解的同學可以戳),到這裡基本就已經瞭解了建立Bitmap的一些記憶體中的特性(大家可以使用adb shell dumpsys meminfo
去檢視Bitmap例項化之後的記憶體使用情況)。 - 建立Bitmap的時候,Java不提供
-
Bitmap的使用
-
我們已經知道了
BitmapFactory
是如何通過各種資源建立Bitmap
了,那麼我們如何合理的使用它呢?以下是幾個我們使用Bitmap
需要關注的點-
Size
- 這裡我們來算一下,在Android中,如果採用
Config.ARGB_8888
的引數去建立一個Bitmap
,這是Google推薦的配置色彩引數,也是Android4.4及以上版本預設建立Bitmap的Config引數(Bitmap.Config.inPreferredConfig
的預設值),那麼每一個畫素將會佔用4byte,如果一張手機照片的尺寸為1280×720,那麼我們可以很容易的計算出這張圖片佔用的記憶體大小為 1280x720x4 = 3686400(byte) = 3.5M,一張未經處理的照片就已經3.5M了! 顯而易見,在開發當中,這是我們最需要關注的問題,否則分分鐘OOM! - 那麼,我們一般是如何處理Size這個重要的因素的呢?,當然是調整
Bitmap
的大小到適合的程度啦!辛虧在BitmapFactory
中,我們可以很方便的通過BitmapFactory.Options
中的options.inSampleSize
去設定Bitmap
的壓縮比,官方給出的說法是
If set to a value > 1, requests the decoder to subsample the original image, returning a smaller image to save memory....For example, inSampleSize == 4 returns an image that is 1/4 the width/height of the original, and 1/16 the number of pixels. Any value <= 1 is treated the same as 1.
很簡潔明瞭啊!也就是說,只要按計算方法設定了這個引數,就可以完成我們Bitmap的Size調整了。那麼,應該怎麼調整姿勢才比較舒服呢?下面先介紹其中一種通過
InputStream
的方式去建立Bitmap
的方法,上一段從Gallery中獲取照片並且將圖片Size調整到合適手機尺寸的程式碼: - 這裡我們來算一下,在Android中,如果採用
static final int PICK_PICS = 9; public void startGallery(){ Intent i = new Intent(); i.setAction(Intent.ACTION_PICK); i.setType("image/*"); startActivityForResult(i,PICK_PICS); } private int[] getScreenWithAndHeight(){ WindowManager wm = (WindowManager) getSystemService(Context.WINDOW_SERVICE); DisplayMetrics dm = new DisplayMetrics(); wm.getDefaultDisplay().getMetrics(dm); return new int[]{dm.widthPixels,dm.heightPixels}; } /** * * @param actualWidth 圖片實際的寬度,也就是options.outWidth * @param actualHeight 圖片實際的高度,也就是options.outHeight * @param desiredWidth 你希望圖片壓縮成為的目的寬度 * @param desiredHeight 你希望圖片壓縮成為的目的高度 * @return */ private int findBestSampleSize(int actualWidth, int actualHeight, int desiredWidth, int desiredHeight) { double wr = (double) actualWidth / desiredWidth; double hr = (double) actualHeight / desiredHeight; double ratio = Math.min(wr, hr); float n = 1.0f; //這裡我們為什麼要尋找 與ratio最接近的2的倍數呢? //原因就在於API中對於inSimpleSize的註釋:最終的inSimpleSize應該為2的倍數,我們應該向上取與壓縮比最接近的2的倍數。 while ((n * 2) <= ratio) { n *= 2; } return (int) n; } @Override protected void onActivityResult(int requestCode, int resultCode, Intent data) { if(resultCode == RESULT_OK){ switch (requestCode){ case PICK_PICS: Uri uri = data.getData(); InputStream is = null; try { is = getContentResolver().openInputStream(uri); } catch (FileNotFoundException e) { e.printStackTrace(); } BitmapFactory.Options options = new BitmapFactory.Options(); //當這個引數為true的時候,意味著你可以在解析時候不申請記憶體的情況下去獲取Bitmap的寬和高 //這是調整Bitmap Size一個很重要的引數設定 options.inJustDecodeBounds = true; BitmapFactory.decodeStream( is,null,options ); int realHeight = options.outHeight; int realWidth = options.outWidth; int screenWidth = getScreenWithAndHeight()[0]; int simpleSize = findBestSampleSize(realWidth,realHeight,screenWidth,300); options.inSampleSize = simpleSize; //當你希望得到Bitmap例項的時候,不要忘了將這個引數設定為false options.inJustDecodeBounds = false; try { is = getContentResolver().openInputStream(uri); } catch (FileNotFoundException e) { e.printStackTrace(); } Bitmap bitmap = BitmapFactory.decodeStream(is,null,options); iv.setImageBitmap(bitmap); try { is.close(); is = null; } catch (IOException e) { e.printStackTrace(); } break; } } super.onActivityResult(requestCode, resultCode, data); } 複製程式碼
我們來看看對於1080P的魅族Note3拍攝的高清無碼照片這段程式碼的功效:
壓縮後:
壓縮前:-
Reuse 上面介紹了
BitmapFactory
通過InputStream
去建立Bitmap
的這種方式,以及BitmapFactory.Options.inSimpleSize
和BitmapFactory.Options.inJustDecodeBounds
的使用方法,但將單個Bitmap載入到UI是簡單的,但是如果我們需要一次性載入大量的圖片,事情就會變得複雜起來。Bitmap
是吃記憶體大戶,我們不希望多次解析相同的Bitmap
,也不希望可能不會用到的Bitmap
一直存在於記憶體中,所以,這個場景下,Bitmap
的重用變得異常的重要。 在這裡只介紹一種BitmapFactory.Options.inBitmap
的重用方式,下一篇文章會介紹使用三級快取來實現Bitmap的重用。根據官方文件在Android 3.0 引進了BitmapFactory.Options.inBitmap,如果這個值被設定了,decode方法會在載入內容的時候去重用已經存在的bitmap. 這意味著bitmap的記憶體是被重新利用的,這樣可以提升效能, 並且減少了記憶體的分配與回收。然而,使用inBitmap有一些限制。特別是在Android 4.4 之前,只支援同等大小的點陣圖。 我們看來看看這個引數最基本的運用方法。
new BitmapFactory.Options options = new BitmapFactory.Options(); //inBitmap只有當inMutable為true的時候是可用的。 options.inMutable = true; Bitmap reusedBitmap = BitmapFactory.decodeResource(getResources(),R.drawable.reused_btimap,options); options.inBitmap = reusedBitmap; 複製程式碼
這樣,當你在下一次decodeBitmap的時候,將設定了
options.inMutable=true
以及options.inBitmap
的Options
傳入,Android就會複用你的Bitmap了,具體例項:@Override protected void onCreate(@Nullable Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(reuseBitmap()); } private LinearLayout reuseBitmap(){ LinearLayout linearLayout = new LinearLayout(this); linearLayout.setLayoutParams(new ViewGroup.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT)); linearLayout.setOrientation(LinearLayout.VERTICAL); ImageView iv = new ImageView(this); iv.setLayoutParams(new ViewGroup.LayoutParams(500,300)); options = new BitmapFactory.Options(); options.inJustDecodeBounds = true; //inBitmap只有當inMutable為true的時候是可用的。 options.inMutable = true; BitmapFactory.decodeResource(getResources(),R.drawable.big_pic,options); //壓縮Bitmap到我們希望的尺寸 //確保不會OOM options.inSampleSize = findBestSampleSize(options.outWidth,options.outHeight,500,300); options.inJustDecodeBounds = false; Bitmap bitmap = BitmapFactory.decodeResource(getResources(),R.drawable.big_pic,options); options.inBitmap = bitmap; iv.setImageBitmap(bitmap); linearLayout.addView(iv); ImageView iv1 = new ImageView(this); iv1.setLayoutParams(new ViewGroup.LayoutParams(500,300)); iv1.setImageBitmap( BitmapFactory.decodeResource(getResources(),R.drawable.big_pic,options)); linearLayout.addView(iv1); ImageView iv2 = new ImageView(this); iv2.setLayoutParams(new ViewGroup.LayoutParams(500,300)); iv2.setImageBitmap( BitmapFactory.decodeResource(getResources(),R.drawable.big_pic,options)); linearLayout.addView(iv2); return linearLayout; } 複製程式碼
以上程式碼中,我們在解析了一次一張1080P解析度的圖片,並且設定在
options.inBitmap
中,然後分別decode了同一張圖片,並且傳入了相同的options
。最終只佔用一份第一次解析Bitmap
的記憶體。 -
Recycle 一定要記得及時回收Bitmap,否則如上分析,你的native以及dalvik的記憶體都會被一直佔用著,最終導致OOM
// 先判斷是否已經回收 if(bitmap != null && !bitmap.isRecycled()){ // 回收並且置為null bitmap.recycle(); bitmap = null; } System.gc(); 複製程式碼
-
-
Enjoy Android :) 如果有誤,輕噴,歡迎指正。
-