先来点鸡汤:
Stay hungry,stay foolish
这句话的的解读:我们必须了解自己的渺小。如果我们不学习,科技发展的速度会让我们五年后被清空。所以,我们必须用初学者谦虚的自觉,饥饿者渴望的求知态度,来拥抱未来的知识。
- 这几天做的项目中需要从图库选择图片或者拍照生成图片,然后展现在IamgeView控件上。当然,从图库选择图片和拍照选择图片的功能实现起来很简单。直接写上代码:
CharSequence[] items = { "拍照", "图库" };
new AlertDialog.Builder(context).setTitle("请选择:")
.setIcon(R.drawable.ic_choose_picture)
.setItems(items, new OnClickListener() {
public void onClick(DialogInterface dialog,int which) {
switch (which) {
case 0:
// 调用拍照功能
// 创建File对象,用于存储拍照后的对象
takePhotoImage = new File(Environment.getExternalStorageDirectory(),"take_photo_image.jpg");
try {
if (takePhotoImage.exists()) {
takePhotoImage.delete();
}
takePhotoImage.createNewFile();
} catch (Exception e) {
e.printStackTrace();
}
// 将File对象转换成Uri对象
imageUri =Uri.fromFile(takePhotoImage);
Intent intent = new Intent(MediaStore.ACTION_IMAGE_CAPTURE);
// 指定图片的输出地址
intent.putExtra(MediaStore.EXTRA_OUTPUT,
imageUri);
startActivityForResult(intent, TAKE_PHOTO);
break;
case 1:
// 调用系统图库
// 创建File对象,用于存储选择图库后的图片
File choosePhoto = new File(Environment.getExternalStorageDirectory(),"choose_photo.jpg");
try {
if (choosePhoto.exists()) {
choosePhoto.delete();
}
choosePhoto.createNewFile();
} catch (Exception e) {
e.printStackTrace();
}
// 将File对象转换成Uri对象
imageUri = Uri.fromFile(choosePhoto);
Intent intent2 = new Intent("android.intent.action.GET_CONTENT");
intent2.setType("image/*");
intent2.putExtra("crop", "true");
intent2.putExtra("scale", true);
intent2.putExtra(MediaStore.EXTRA_OUTPUT,imageUri);
startActivityForResult(intent2, CROP_PHOTO);
break;
}
}
}).show();
以上代码很简单,就是创建一个对话框,有两个item选项。一个是拍照、一个是图库。如下所示:
两个item选项的功能都是为了生成一张图片并设置到ImageView控件上。
拍照和从图库选择图片都先创建了一个File对象,用于存储摄像头拍下的图片或者存储从图库中选择的图片。
然后将它放在手机的根目录下,我的手机是在内存设备的根目录下,这个无所谓。然后调用Uri的fromFile()方法将File对象转换成Uri对象。这个Uri对象标识着图片的唯一地址。
如果点击的是拍照,接着会构建出一个Intent对象,并将这个Intent的action指定为:MediaStore.ACTION_IMAGE_CAPTURE,再调用Intent的putExtra()方法指定图片的输出地址,这里就填入刚刚得到的Uri对象,最后调用startActivityForResult()来启动活动。
- 因为上面的代码是使用startActivityForResult()来启动活动的,所以拍照成功后会回调onActivityResult()方法,也即拍完照后会有结果返回到onActivityResult()方法中。所以重写该方法就能对拍照后的图片做进一步的处理了。方法如下所示:
/**
* 在调用startActivityForResult的时候会回调该方法
*/
@Override
public void onActivityResult(int requestCode, int resultCode, Intent data) {
switch (requestCode) {
// 拍照
case TAKE_PHOTO:
if (resultCode == Activity.RESULT_OK) {
Intent intent = new Intent("com.android.camera.action.CROP");
intent.setDataAndType(imageUri, "image/*");
intent.putExtra("scale", true);
intent.putExtra(MediaStore.EXTRA_OUTPUT, imageUri);
startActivityForResult(intent, CROP_PHOTO);
}
break;
// 裁剪图片
case CROP_PHOTO:
if (resultCode == Activity.RESULT_OK) {
try {
// 压缩Bitmap对象
BitmapFactory.Options options = new BitmapFactory.Options();
options.inSampleSize = 8;
options.inPreferredConfig = Bitmap.Config.RGB_565;
options.inPurgeable = true;
options.inInputShareable = true;
bitmap=BitmapFactory.decodeStream(getActivity().getContentResolver().openInputStream(imageUri),null, options);
setImageData(bitmap);
if (bitmapEntities.size() == 1) {
adapter = new BitmapAdapter(context, bitmapEntities);
gv_Picture.setAdapter(adapter);
} else {
adapter.notifyDataSetChanged();
}
linear_Picture.addView(view_Picture, 1);
} catch (Exception e) {
e.printStackTrace();
}
}
break;
}
}
- 上面说到如果用摄像头拍照成功后会回调onActivityResult()方法,这时候会继续构建Intent对象,把它的action指定为:
“com.android.camera.action.CROP”。
这个Intent是用于拍出的照片进行裁剪的。因为摄像头比较大,而我们可能只希望截取其中的一小部分。然后给这个Intent设置上一些必要的属性,并再调用startActivityForResult()来启动裁剪程序。裁剪后的图片同样会输出到手机根目录下的图片文件中。
===========================================
- 如果点击的item选项是图库。那么会调用系统的图库选择图片。在这里,跟拍照时基本的操作没有什么太多的差别。
都是先创建一个File对象,保存从图库选择的文件。然后构建出一个Intent对象,并将它的action指定为:“android.intent.action.GET_CONTENT”。接着给这个Intent对象设置一些必要的参数,包括是否允许缩放和裁剪、图片的输出位置等。最后调用startActivityForResult()方法,就可以打开相册程序选择照片了。 这里用到了一个小技巧,就是我们在调用startActivityForResult()方法的时候,给第二个参数传入的值仍然是:CROP_PHOTO常量,这样就可以复用之前调用摄像头显示图片的逻辑了,不用再编写第二遍显示图片的逻辑。
好了,以上的两个item选项的操作都讲完了,接下来就是关键的时刻了。
裁剪操作完成后,程序又会回调到onActivityResult方法中,这个时候就可以利用BItmapFactory的decodeStream()方法将存储在手机根目录下的图片文件解析成Bitmap对象,然后把它设置到ImageView控件上显示出来。-
我在这里用的是gridView控件显示一些图片。不过都是一样的,在gridView设置adapter的时候,需要在自定义的Adapter中把Bitmap对象设置到ImageView控件上。一开始设置一两张图片的时候如下所示:
但是放的图的一旦多一点,程序就直接崩溃了,看了一下logcat的错误日志。
一个醒目的Java.lang.OutOfMemoryError(内存溢出错误),最不想遇见的错误。说心里话,蛋疼。困惑了有段时间,试了很多的方法,终于很好的做了一些有效的图片压缩方法优化内存溢出的问题,但是并不能彻底的解决内存溢出问题。所以有时候我们自己要对图片的张数做一些限制。像钉钉的出差管理的图片选择最多允许选择9张、微信发朋友圈发图片最多允许9张。有时候需要一些限制才能解决一些无法避免的问题。
上面的发生内存溢出错误的代码定位了一下,直接就定位到下面这一行,如下图所示:
该行代码如下:
bitmap=BitmapFactory.decodeStream(getActivity().getContentResolver().openInputStream(imageUri));
为什么这行代码会导致OOM呢?给大家讲讲:该行代码的作用是用于从指定输入流中解析、创建Bitmap对象。但是由于手机系统的内存比较小,如果不停的去解析、创建Bitmap对象,可能由于前面所创建的Bitmap所占用的内存还没有回收,而导致程序运行时引发OutOfMemory错误。所以我们需要将Bitmap对象先进行压缩,使用的时候可以降低对内存的占用,这样就可以有效的解决这个问题。我的解决方法是将以上的代码替换为下图所示的代码:
上图所示的各行代码解释如下:
======
BitmapFactory.Options options = new BitmapFactory.Options();
该行代码用来创建一个BitmapFactory.Options对象,且Options 只保存图片尺寸大小,不保存图片到内存。
======
options.inSampleSize = 8;
该行代码是最关键的代码。给大家点进去看一下源码,源码如下所示:
源码的注释给大家讲讲。
注释说:如果该值设置为>1,就会请求解析器对原图做二次抽样,即二次解析,返回一个较小的图片用来节省内存。二次抽样样本大小的像素尺寸,对应于一个解码位图的像素。举个例子,如果inSampleSize == 4,会返回一个是原图1/4高度和1/4宽度的图像,和1/16像素的数量。对任何值< = 1的值都用=1来赋值。
以上源码的注释很好的说明了该行代码的关键性。大家要有效避免OOM错误的话一定要记得加上哦!只有这样,才能有效的节省Bitmap对象占用的内存。别忘记了!
======
options.inPreferredConfig = Bitmap.Config.RGB_565;
该行代码是附加上图片的Config参数,解析器或根据当前的参数配置进行对应的解析,这也可以有效减少加载的内存。
======
options.inPurgeable = true;
该行代码作用:由此产生的位图将分配它的像素,这样他们可以被净化系统需要回收的内存。
======
options.inInputShareable = true;
该行代码的作用:inInputShareable属性和inPurgeable 有关,当inPurgeable 属性设置为false的时候,inInputShareable属性就可以忽略。但是如果这两个属性都设置为true的时候,源码的注释这样说:确定位图可以共享一个参考输入数据如果它必须深拷贝的话。好了这都不是什么关键的,不设置也是可以的。
======
最后调用方法:
bitmap=BitmapFactory.decodeStream(getActivity().getContentResolver().openInputStream(imageUri),null, options);
该行代码就是根据options的一些选项设置解析输入流返回一个压缩后的Bitmap对象。这时候使用bitmap的时候就可以有效的避免OOM了。下面附上我的项目的测试图片给大家看,眼见为实嘛!如下图:
好了,没有报OOM的错误,9张图片都成功的呈现在界面上。(爆料一下,中间那张图片是我的哦!不要羡慕哥,有时间记得锻炼!你也可以有我的身材! :) )
===========================================
就先说到这里,以后会遇到更多实际开发的小问题,到时候继续学习进步并把一些解决方案分享给大家。
好了说了这么久了。干货上场了!
给大家几点程序员摆脱疲劳的方法(博客上写的,但是我自己认为不错的):
链接地址是:程序员摆脱疲劳的方法