前言
之前有两篇博客讲解了如何从系统内已有的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对象也可以。