Android超长图加载与subsampling scale image view实现分析

党权
2023-12-01

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,后者在大图的切片方面做得更好只将大图切成若干片,在判断是否可见,如果可见就加载到内存中,否者回收;滑动时只改变矩阵的值进行简单的位移变换,进一步提升了流畅度,而且根据不同放缩比例选择合适的采样率,进一步减少内存占用。自己实现的每滑动一次就要重新解码绘制好几次,所以后者性能更高,值得学习。

转载于:https://juejin.im/post/5d4ae0346fb9a06b1417cf11

 类似资料: