Android中的图片加载一直是很重要的一块,也是很令人头疼的一块,动不动就出现OOM。所以我们有fresco等优秀的第三方框架,什么三级缓存,一行代码就帮我们轻松实现。但当面对超级长超级大分辨率尺寸的图时,就显得无能为力了,如果直接加载到内存中就又会出现OOM。
1.BitmapRegionDecoder
实现长图大图的加载,最关键的类就是BitmapRegionDecoder
他可以实现对图片的局部加载。
mDecoder=BitmapRegionDecoder.newInstance(image,false);
//这里不能用option来获取图片的宽和高,因为经过BitmapRegionDecoder类处理过后的inputstream不能在获取的其信息,自动返回-1
imageHeight=mDecoder.getHeight();
imageWidth=mDecoder.getWidth();
bmp = mDecoder.decodeRegion(mRect, option);
复制代码
这里注意经过BitmapRegionDecoder类处理过后的inputstream不能再用option获取其信息,会自动返回-1。当创建decoder对象后其实并没有将图片加载到内存中,只有调用了bmp = mDecoder.decodeRegion(mRect, option);
之后才将这个mrect矩形的图片局部加载到内存中。
自己实现
那么既然Android有这么方便的类,那我们岂不是很简单就可以自己实现啦!所以参考 鸿洋_的Android 高清加载巨图方案 拒绝压缩图片 这篇博客,我们可以自己实现一个简易的加载长图框架:
- 自定义一个view,重写他的
ondraw()
和onTouchEvent()
- 创建GestureDetector.OnGestureListener的实现类和scroller去接管触摸事件,在move时记录滑动距离,重写
computeScroll()
去辅助滑动 - 初始化我们的局部加载类BitmapRegionDecoder,当屏幕滑动到哪,记录其坐标到rect里然后直接mDecoder.decodeRegion(mRect, option);调用 invalidate()去ondraw()
- 同时注意设置
option.inBitmap
开启图片的复用,进一步减少内存占用 - 另一个减小内存开销的就是设置合适的采样率,根据控件的大小对图片进行合适的采样压缩
int insamplesize=1;
while (imageWidth>1.6*width) {
imageWidth /= 2;
insamplesize*=2;
}
option.inMutable=true;
option.inSampleSize=insamplesize;
复制代码
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
if (mDecoder!=null) {
option.inBitmap=bmp;
matrix.setScale(scale,scale);
bmp = mDecoder.decodeRegion(mRect, option);
canvas.drawBitmap(bmp,matrix,bitmapPaint);
Log.i("TAG", "onDraw: "+bmp.getByteCount());
}
}
复制代码
注意:这里记录一个小小的坑:当用matrix进行图片的放大时,一定一定要设置画笔Paint,设置抗锯齿等优化,设与不设的差距真的挺大的
private Paint bitmapPaint;
bitmapPaint = new Paint();
bitmapPaint.setAntiAlias(true);
bitmapPaint.setFilterBitmap(true);
bitmapPaint.setDither(true);
复制代码
结论
这样一个简易的图片加载框架就实现了,完美的避免了OOM,因为不用把图片完整的加载到内存中!但是,经过实测,这种方法在加载分辨率比较小的大图时滑动还是挺丝滑的,但一遇到分辨率再大一点的,就会感受到明显的滑动的卡顿,于是我把目光转向了subsampling scale image view这个目前应该是最流行的开源框架
2.subsampling scale imag实现原理
这里以原理实现为主,不想贴很多代码,具体可自己下载阅读github地址
1.ImageSource
subsampling scale image view通过这个类ImageSource去获取图片,所以我们的图片资源都需要通过ImageSource去加载,支持从assets,文件,流中加载,从源码上看他其实就是一个工具类,用于方便加载各个路径的文件
2.fullImageSampleSize
.fullImageSampleSamplSize由private int calculateInSampleSize(float scale)
计算出,这个值应该是我们首先应该理解的。
他决定了图片是否需要用BitmapRegionDecoder进行区域加载。如果他的计算结果等于1
,则表示这张图的分辨率还不够大,不需要进行切割进行区域加载,所以这种情况下是最简单的,直接将图加载进入,放大缩小,移动,都是通过Matrix来实现的,所以接下来就来说一下Matrix
3.Matrix
matrix,矩阵,很多关于图片的功能都能通过他来做一些十分的变换来实现(可惜当年线代没学好。。。)比如图片我的位移,放缩,旋转等等。subsampling scale image view也用了matrix来实现图片的放缩和位移,主要方法是matrix.setPolyToPoly(srcArray, 0, dstArray, 0, 4
); 有两个数组srA,rray和dstArray dstarray数组决定了图片在屏幕的位置,而大图的移动滑动就是通过他来实现的
4.Tile
private static class Tile
这个内部类就是切片类,subsampling scale image view中最重要的一个数据结构。
private static class Tile {
private Rect sRect;
private int sampleSize;
private Bitmap bitmap;
private boolean loading;
private boolean visible;
// Volatile fields instantiated once then updated before use to reduce GC.
private Rect vRect;
private Rect fileSRect;
}
复制代码
他的属性也很简单就是用来存储图片的一段切片信息,各种rect和bitmap和一个samplesiz其中需要区分一下各个rect
- srect和filesrect其实是保存这个切片的原始大小区域,也就调用是
mDecoder.decodeRegion(mRect, option)
区域加载时传入的rect - vRect描述绘制在view画布中的实际位置,也就是说图片放大后的上下滑动就是通过改变这个rect结合
matrix.setPolyToP()
来实现的
protected void onDraw(Canvas canvas) {
......
if (matrix == null) { matrix = new Matrix(); }
matrix.reset();
setMatrixArray(srcArray, 0, 0, tile.bitmap.getWidth(), 0, tile.bitmap.getWidth(), tile.bitmap.getHeight(), 0, tile.bitmap.getHeight());
if (getRequiredRotation() == ORIENTATION_0) {
setMatrixArray(dstArray, tile.vRect.left, tile.vRect.top, tile.vRect.right, tile.vRect.top, tile.vRect.right, tile.vRect.bottom, tile.vRect.left,
}...
...
matrix.setPolyToPoly(srcArray, 0, dstArray, 0, 4);
canvas.drawBitmap(tile.bitmap, matrix, bitmapPaint);
......
}
复制代码
5.三个task
TilesInitTask,TileLoadTask ,BitmapLoadTask
subsampling scale image view内部又创建了三个继承自AsyncTask
的task用来在后台加载decode图从而不阻碍ui主线程,更加流畅。 所以当fullImageSampleSize==1
时,就直接用BitmapLoadTask解码整个图片不需切割,当期大于1时,就需要用TileLoadTask区域解码分割后的图片
Map<Integer, List<Tile>> tileMap
最后介绍这个框架的核心,就是这个map
我们知道,图片放得越大,所需要的像素分辨率就要越高才能匹配,要不然就会很模糊。相反,如果图片缩得很小,就不需要很高的分辨率,多了就浪费了。而Android中就可以根据 option.inSampleSize来对图片进行采样压缩,减小分辨率。
所以,根据这个原理,subsampling scale image view将其根据需要计算出不同的采样率,当做key,然后根据不同的采样率进行切割,生成List<Tile>
放大的时候,subsampling scale image view会选取合适的采样率后获取到List<Tile>
然后进行解码,并且,他只会解码显示的部分,也就是til.visiable
为true时才会解码。否者将其回收。
综上就是subsampling scale image view的大致实现原理
对比
对比自己实现的和subsampling scale image view,后者在大图的切片方面做得更好只将大图切成若干片,在判断是否可见,如果可见就加载到内存中,否者回收;滑动时只改变矩阵的值进行简单的位移变换,进一步提升了流畅度,而且根据不同放缩比例选择合适的采样率,进一步减少内存占用。自己实现的每滑动一次就要重新解码绘制好几次,所以后者性能更高,值得学习。