前言
之前有兩篇博客講解了如何從系統內已有的Camera和Gallery應用中獲取圖片的例子,看到評論里有朋友說有時候會報錯,導致程序崩潰的問題。本篇博客主要就這個問題分析講解一下,最后將以一個簡單的Demo演示。關於從系統內已有的Camera和Gallery應用中獲取圖片還不了解的朋友,可以先看看另外兩篇博客:Android--調用系統照相機拍照與攝像、Android--從系統Gallery獲取圖片。
分析出錯原因
之前講到的從系統現有的Camera和Gallery應用中獲取圖片的Demo中,均直接使用系統應用返回的Uri,通過ImageView.setImageURI(Uri)方法顯示在界面上。而對於Android設備來說,向內存中加載一張圖片,消耗的內存並不受圖片的大小而影響,影響它的是圖片的分辨率,圖片的分辨率越大加載到內存所占用的內存將越多。使用ImageView.setImageURI(Uri)方法將導致了一個嚴重的錯誤,雖然ImageView直接引用圖片的Uri,它會對圖片進行一部分優化,使得它可以正常顯示,但是這種辦法不利於Bitmap資源的回收。所以在重復操作之后,經歷過多次的GC,也沒有辦法回收出足夠加載圖片的內存,導致應用崩潰。
解決方案
既然已經知道導致程序崩潰的原因是內存溢出導致的,那么只需要維護好Uri所代表的圖片內存即可。具體優化流程如下:
1、系統中現有的Camera和Gallery應用獲取圖片返回的都是一個Uri類型的數據,它是一個內容提供者的路徑,可以使用ContentResolver獲取它,這個以前有講過,不了解的朋友可以看看另外一篇博客:Android--ContentProvider。而在Context中,可以使用getContentResolver()方法獲取到當前的內容解析者,並通過它的openInputStream()方法獲取到圖片的輸入流,通過輸入流可以獲取到一個Bitmap對象。
2、上面提到,Android中加載圖片到內存中所占內存的大小取決於圖片的分辨率,所有得到Bitmap還不能直接使用它,必須對其進行優化,以最大適應當前設備的屏幕分辯率又不會導致加載過多像素而導致內存不足的情況。關於加載大分辨率到內存還不了解的朋友可以參見另外一篇博客:Android--加載大分辨率圖片到內存。
3、得到了優化過后的圖片還需要在使用過后進行回收,Bitmap提供了兩個方法用於判斷是否已經回收它以及強制Bitmap回收自己。以下是它們的完整簽名:
- boolean isRecycled():返回Bitmap對象是否已經被回收。
- void recycle():強制一個Bitmap對象回收自己。
優化后的Demo
上面講到的兩個demo,從Gallery中獲取圖片比較簡單,代碼量小,那么就在這個基礎之上進行代碼的優化。從Gallery中獲取圖片的Uri並不直接使用,而是把它轉化為一個Bitmap,並且優化它以達到適應屏幕分辨率的效果。
1 package cn.bgxt.sysgallerydemo; 2 3 import java.io.InputStream; 4 5 import android.net.Uri; 6 import android.os.Bundle; 7 import android.util.Log; 8 import android.view.View; 9 import android.view.WindowManager; 10 import android.view.View.OnClickListener; 11 import android.widget.Button; 12 import android.widget.ImageView; 13 import android.widget.Toast; 14 import android.app.Activity; 15 import android.content.Intent; 16 import android.graphics.Bitmap; 17 import android.graphics.BitmapFactory; 18 import android.graphics.Canvas; 19 import android.graphics.Color; 20 import android.graphics.BitmapFactory.Options; 21 import android.graphics.Matrix; 22 import android.graphics.Paint; 23 24 public class MainActivity extends Activity { 25 private Button btn_getImage; 26 private ImageView iv_image; 27 private final static String TAG = "main"; 28 private WindowManager wm; 29 private Bitmap bitmap; 30 private Bitmap blankBitmap; 31 32 @Override 33 protected void onCreate(Bundle savedInstanceState) { 34 super.onCreate(savedInstanceState); 35 setContentView(R.layout.activity_main); 36 37 // 得到應用窗口管理器 38 wm = getWindowManager(); 39 btn_getImage = (Button) findViewById(R.id.btn_getImage); 40 iv_image = (ImageView) findViewById(R.id.iv_image); 41 42 btn_getImage.setOnClickListener(getImage); 43 44 } 45 46 private View.OnClickListener getImage = new OnClickListener() { 47 48 @Override 49 public void onClick(View v) { 50 // 設定action和miniType 51 Intent intent = new Intent(); 52 intent.setAction(Intent.ACTION_PICK); 53 intent.setType("image/*"); 54 // 以需要返回值的模式開啟一個Activity 55 startActivityForResult(intent, 0); 56 } 57 }; 58 59 @Override 60 protected void onActivityResult(int requestCode, int resultCode, Intent data) { 61 // 如果獲取成功,resultCode為-1 62 Log.i(TAG, "resultCode:" + resultCode); 63 if (requestCode == 0 && resultCode == -1) { 64 // 獲取原圖的Uri,它是一個內容提供者的地址 65 Uri uri = data.getData(); 66 Log.i(TAG, "uri:" + data.getData().toString()); 67 try { 68 // 從ContentResolver中獲取到Uri的輸入流 69 InputStream is = getContentResolver().openInputStream(uri); 70 71 // 得到屏幕的寬和高 72 int windowWidth = wm.getDefaultDisplay().getWidth(); 73 int windowHeight = wm.getDefaultDisplay().getHeight(); 74 75 // 實例化一個Options對象 76 BitmapFactory.Options opts = new BitmapFactory.Options(); 77 // 指定它只讀取圖片的信息而不加載整個圖片 78 opts.inJustDecodeBounds = true; 79 // 通過這個Options對象,從輸入流中讀取圖片的信息 80 BitmapFactory.decodeStream(is, null, opts); 81 82 // 得到Uri地址的圖片的寬和高 83 int bitmapWidth = opts.outWidth; 84 int bitmapHeight = opts.outHeight; 85 // 分析圖片的寬高比,用於進行優化 86 if (bitmapHeight > windowHeight || bitmapWidth > windowWidth) { 87 int scaleX = bitmapWidth / windowWidth; 88 int scaleY = bitmapHeight / windowHeight; 89 if (scaleX > scaleY) { 90 opts.inSampleSize = scaleX; 91 } else { 92 opts.inSampleSize = scaleY; 93 } 94 } else { 95 opts.inSampleSize = 1; 96 } 97 98 // 設定讀取完整的圖片信息 99 opts.inJustDecodeBounds = false; 100 is = getContentResolver().openInputStream(uri); 101 102 // 如果沒有被系統回收,就強制回收它 103 if (blankBitmap != null && !bitmap.isRecycled()) { 104 bitmap.recycle(); 105 } 106 bitmap = BitmapFactory.decodeStream(is, null, opts); 107 108 // 如果沒有被系統回收,就強制回收它 109 if (blankBitmap != null && !blankBitmap.isRecycled()) { 110 blankBitmap.recycle(); 111 } 112 // 在內存中創建一個可以操作的Bitmap對象 113 blankBitmap = Bitmap.createBitmap(bitmap.getWidth(), 114 bitmap.getHeight(), Bitmap.Config.ARGB_8888); 115 // 為圖片添加一個畫板 116 Canvas canvas = new Canvas(blankBitmap); 117 // 把讀取的圖片畫到新創建的Bitmap對象中 118 canvas.drawBitmap(bitmap, new Matrix(), new Paint()); 119 Paint paint = new Paint(); 120 paint.setColor(Color.RED); 121 paint.setTextSize(30); 122 // 通過創建的畫筆,在Bitmap上寫入水印 123 canvas.drawText("我是水印", 10, 50, paint); 124 125 iv_image.setImageBitmap(blankBitmap); 126 } catch (Exception e) { 127 Toast.makeText(MainActivity.this, "獲取圖片失敗", 0).show(); 128 } 129 } 130 super.onActivityResult(requestCode, resultCode, data); 131 } 132 }
效果展示:
總結
其實對於這兩個簡單的Demo而言,只需要針對分辨率進行優化即可,一般而言因為功能簡單,系統配置只要還過的去,都是可以被正常GC的,但是對於一些經常操作圖片的應用來說,還是顯式的通過代碼的方式來管理Bitmap的內存。最后加入Canvas進行渲染水印,不是必須的,只是加了個功能而已,直接使用bitmap對象也可以。