参考
Android 多种方式正确的加载图像,有效避免oom
Android图片缓存之Bitmap详解
BitmapFactory
一、使用BitmapFactory获取图像
- decodeFile 从文件读取
String SDCarePath=Environment.getExternalStorageDirectory().toString();
String filePath=SDCarePath+"/"+"haha.jpg";
Bitmap bm=BitmapFactory.decodeFile(path);
- decodeResource 从资源文件加载
//res/drawable下有 test.jpg文件
Bitmap bitmap =BitmapFactory.decodeResource(this.getContext().getResources(),R.drawable.test);
- decodeStream 从网络加载
- decodeByteArray
1.我们的内存去哪里了(为什么被消耗了这么多):
其实我们的内存就是去bitmap里了,BitmapFactory的每个decode函数都会生成一个bitmap对象,用于存放解码后的图像,然后返回该引用。如果图像数据较大就会造成bitmap对象申请的内存较多,如果图像过多就会造成内存不够用自然就会出现out of memory的现象。
2.怎样才是正确的加载图像:
我们知道我们的手机屏幕有着一定的分辨率(如:840×480),图像也有自己的像素(如高清图片:1080×720)。如果将一张840×480的图片加载铺满840×480的屏幕上这就是最合适的了,此时显示效果最好。如果将一张1080×720的图像放到840×480的屏幕并不会得到更好的显示效果(和840×480的图像显示效果是一致的),反而会浪费更多的内存。
我们一般的做法是将一张网络获取的照片或拍摄的照片放到一个一定大小的控件上面进行展现。这里就以nexus 5x手机拍摄的照片为例说明,其摄像头的像素为1300万(拍摄图像的分辨率为4032×3024),而屏幕的分辨率为1920x1080。其摄像头的分辨率要比屏幕的分辨率大得多,如果不对图像进行处理就直接显示在屏幕上,就会浪费掉非常多的内存(如果内存不够用直接就oom了),而且并没有达到更好的显示效果。
为了减少内存的开销,我们在加载图像时就应该参照控件(如:263pixel×263pixel)的宽高像素来获取合适大小的bitmap。
通过BitmapFactory.Options来缩放图片,主要用到inSampleSize参数,即采样率。采样率为1时即原始大小,为2时,宽高均为原来的1/2,像素数和占用内存数均为原来的1/4.采样率一般是2的指数,即1、2、4、8、16……
具体加载流程:
- 将BitmapFactory.Options的inJustDecodeBounds设为true并加载图片,此时只是解析图片原始宽高,并不会真正加载,所以这个操作是轻量级的。
- 从BitmapFactory.Options中取出图片原始宽高,结合目标view大小计算采样率
- 将BitmapFactory.Options的inJustDecodeBounds设为false并真正加载图片
public static Bitmap getFitSampleBitmap(String file_path, int width, int height) {
BitmapFactory.Options options = new BitmapFactory.Options();
options.inJustDecodeBounds = true;
BitmapFactory.decodeFile(file_path, options);
options.inSampleSize = getFitInSampleSize(width, height, options);
options.inJustDecodeBounds = false;
return BitmapFactory.decodeFile(file_path, options);
}
public static int getFitInSampleSize(int reqWidth, int reqHeight, BitmapFactory.Options options) {
int inSampleSize = 1;
if (options.outWidth > reqWidth || options.outHeight > reqHeight) {
int widthRatio = Math.round((float) options.outWidth / (float) reqWidth);
int heightRatio = Math.round((float) options.outHeight / (float) reqHeight);
inSampleSize = Math.min(widthRatio, heightRatio);
}
return inSampleSize;
}
这里需要注意的是如果我们decodeFile解析的文件是外部存储里的文件,我们需要在Manifists加上文件的读写权限,不然获取的bitmap会为null.
同理我们编写decodeResource的重载函数
public static Bitmap getFitSampleBitmap(Resources resources, int resId, int width, int height) {
BitmapFactory.Options options = new BitmapFactory.Options();
options.inJustDecodeBounds = true;
BitmapFactory.decodeResource(resources, resId, options);
options.inSampleSize = getFitInSampleSize(width, height, options);
options.inJustDecodeBounds = false;
return BitmapFactory.decodeResource(resources, resId, options);
}
对于decodeStream重载,和从file中加载和从resource中加载稍有不同,因对stream是一种有顺序的字符流,对其decode一次后,其顺序就会发生变化,再次进行第二次decode的时候就不能解码成功了,这也是为什么当我们对inputStream decode两次的时候会得到一个null值的bitmap的原因。所以我们对stream类型的源需要进行转换,转换有两种思路:
- 将inputStream的字节流读取后放到一个byte[]数组里,然后使用BitmapFactory.decodeByteArray两次decode进行压缩——但是发现这种方法其实治标不治本,还是无法节省内存。原因在readStream函数中会返回一个byte[]数组,在这个数组的大小即为原始图像的大小,因此并没有起到节省内存的效果。
public static Bitmap getFitSampleBitmap(InputStream inputStream, int width, int height) throws Exception {
BitmapFactory.Options options = new BitmapFactory.Options();
options.inJustDecodeBounds = true;
byte[] bytes = readStream(inputStream);
//BitmapFactory.decodeStream(inputStream, null, options);
BitmapFactory.decodeByteArray(bytes, 0, bytes.length, options);
options.inSampleSize = getFitInSampleSize(width, height, options);
options.inJustDecodeBounds = false;
// return BitmapFactory.decodeStream(inputStream, null, options);
return BitmapFactory.decodeByteArray(bytes, 0, bytes.length, options);
}/* * 从inputStream中获取字节流 数组大小 * */ public static byte[] readStream(InputStream inStream) throws Exception { ByteArrayOutputStream outStream = new ByteArrayOutputStream(); byte[] buffer = new byte[1024]; int len = 0; while ((len = inStream.read(buffer)) != -1) { outStream.write(buffer, 0, len); } outStream.close(); inStream.close(); return outStream.toByteArray(); }
-
将inputStream的字节流读取到一个文件里,然后通过处理file的方式来进行处理即可。
public static Bitmap getFitSampleBitmap(InputStream inputStream, String catchFilePath,int width, int height) throws Exception {
return getFitSampleBitmap(catchStreamToFile(catchFilePath, inputStream), width, height);
}
/*
* 将inputStream中字节流保存至文件
* */
public static String catchStreamToFile(String catchFile,InputStream inStream) throws Exception {File tempFile=new File(catchFile); try { if (tempFile.exists()) { tempFile.delete(); } tempFile.createNewFile(); } catch (IOException e) { e.printStackTrace(); } FileOutputStream fileOutputStream=new FileOutputStream(tempFile); byte[] buffer = new byte[1024]; int len = 0; while ((len = inStream.read(buffer)) != -1) { fileOutputStream.write(buffer, 0, len); } inStream.close(); fileOutputStream.close(); return catchFile;
}
这里我们可以看到,我们通过调用catchStreamToFile先将文件保存到指定文件名里,然后再利用两次decodeFile的形式来处理stream流的。 这样做的好处是什么呢:
1.避免了超大的中间内存变量的生成,所以自然就避免了oom现象。
2.对于从file和resource中加载图片其本质都是从文件中加载图片的。
3.一般inputStream都是应用于网络中获取图片的方式,我们采用了用文件进行缓存的方式进行图片加载还有效的避免了来回切换activity页面时多次从网络中下载同一种图片,从而造成的卡顿现象,使用这种方法,我们加载一次后,再进行第二次加载时,我们可以判断下是否是和第一次加载时的url是一致的,如果是那么直接从使用getFitSampleBitmap file的重载从第一次缓存的catchfile中加载即可,这样大大提高了加载速度(在主程序里我们可以用一个map变量保存下url和catchFileName的对应关系)。
二、BitmapFactory.decodeFileDescriptor
参考《android开发艺术探索》
上面讲到,使用inJustDecodeBounds参数进行采样加载时,decodeStream会有问题。原因在于FileInputStream是一种有序的文件流,decodeStream影响了文件流的位置属性,导致第二次decodeStream失败。当时推荐的解决方法是保存到本地文件作为缓存,然后再使用两次decodeFile来间接达到目的。其实通过FileInputStream获取文件描述符,然后再通过decodeFileDescriptor来操作更简单:
FileDescriptor fd = fileInputStream.getFD();
options.inJustDecodeBounds = true;
BitmapFactory.decodeFileDescriptor(fd,null,options);
options.inSampleSize...
options.inJustDecodeBounds = false;
return BitmapFactory.decodeFileDescriptor(fd,null,options);
注意:decodeFileDescriptor()来生成bimap比decodeFile()省内存
原因:查看BitmapFactory的源码,对比一下两者的实现,可以发现decodeFile()最终是以流的方式生成bitmap
public static Bitmap decodeFile(String pathName, Options opts) {
Bitmap bm = null;
InputStream stream = null;
try {
stream = new FileInputStream(pathName);
bm = decodeStream(stream, null, opts);
} catch (Exception e) {
/* do nothing.
If the exception happened on open, bm will be null.
*/
} finally {
if (stream != null) {
try {
stream.close();
} catch (IOException e) {
// do nothing here
}
}
}
return bm;
}
decodeFileDescriptor的源码,可以找到native本地方法decodeFileDescriptor,通过底层生成bitmap
public static Bitmap decodeFileDescriptor(FileDescriptor fd, Rect outPadding, Options opts) {
if (nativeIsSeekable(fd)) {
Bitmap bm = nativeDecodeFileDescriptor(fd, outPadding, opts);
if (bm == null && opts != null && opts.inBitmap != null) {
throw new IllegalArgumentException("Problem decoding into existing bitmap");
}
return finishDecode(bm, outPadding, opts);
} else {
FileInputStream fis = new FileInputStream(fd);
try {
return decodeStream(fis, outPadding, opts);
} finally {
try {
fis.close();
} catch (Throwable t) {/* ignore */}
}
}
}private static native Bitmap nativeDecodeFileDescriptor(FileDescriptor fd,Rect padding, Options opts);