Android bitmap(三) BitmapFactory inSampleSize

参考
Android 多种方式正确的加载图像,有效避免oom
Android图片缓存之Bitmap详解
BitmapFactory

一、使用BitmapFactory获取图像
  • decodeFile 从文件读取

<pre>
String SDCarePath=Environment.getExternalStorageDirectory().toString();
String filePath=SDCarePath+"/"+"haha.jpg";
Bitmap bm=BitmapFactory.decodeFile(path);
</pre>

  • decodeResource 从资源文件加载

<pre>
//res/drawable下有 test.jpg文件
Bitmap bitmap =BitmapFactory.decodeResource(this.getContext().getResources(),R.drawable.test);
</pre>

  • 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并真正加载图片

<pre>
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;
}
</pre>

这里需要注意的是如果我们decodeFile解析的文件是外部存储里的文件,我们需要在Manifists加上文件的读写权限,不然获取的bitmap会为null.
<pre>
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE"/>
</pre>

同理我们编写decodeResource的重载函数
<pre>
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);
}
</pre>

对于decodeStream重载,和从file中加载和从resource中加载稍有不同,因对stream是一种有顺序的字符流,对其decode一次后,其顺序就会发生变化,再次进行第二次decode的时候就不能解码成功了,这也是为什么当我们对inputStream decode两次的时候会得到一个null值的bitmap的原因。所以我们对stream类型的源需要进行转换,转换有两种思路:

  1. 将inputStream的字节流读取后放到一个byte[]数组里,然后使用BitmapFactory.decodeByteArray两次decode进行压缩——但是发现这种方法其实治标不治本,还是无法节省内存。原因在readStream函数中会返回一个byte[]数组,在这个数组的大小即为原始图像的大小,因此并没有起到节省内存的效果。

<pre>
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();
}

</pre>

  1. 将inputStream的字节流读取到一个文件里,然后通过处理file的方式来进行处理即可。
    <pre>
    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;
    

    }
    </pre>

这里我们可以看到,我们通过调用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来操作更简单:
<pre>
FileDescriptor fd = fileInputStream.getFD();
options.inJustDecodeBounds = true;
BitmapFactory.decodeFileDescriptor(fd,null,options);
options.inSampleSize...
options.inJustDecodeBounds = false;
return BitmapFactory.decodeFileDescriptor(fd,null,options);
</pre>

注意:decodeFileDescriptor()来生成bimap比decodeFile()省内存
原因:查看BitmapFactory的源码,对比一下两者的实现,可以发现decodeFile()最终是以流的方式生成bitmap
<pre>
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;
}
</pre>

decodeFileDescriptor的源码,可以找到native本地方法decodeFileDescriptor,通过底层生成bitmap

<pre>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);
</pre>

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 199,902评论 5 468
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 84,037评论 2 377
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 146,978评论 0 332
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 53,867评论 1 272
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 62,763评论 5 360
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,104评论 1 277
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,565评论 3 390
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,236评论 0 254
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,379评论 1 294
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,313评论 2 317
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,363评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,034评论 3 315
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,637评论 3 303
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,719评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 30,952评论 1 255
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 42,371评论 2 346
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 41,948评论 2 341

推荐阅读更多精彩内容