MicroKibaco / CrazyDailyQuestion

每日一问: 水滴石穿,聚沙成塔,坚持数月, 必有收获~
35 stars 1 forks source link

2019-8-28: 说说如何处理大长图的加载? #24

Open zhjlong opened 5 years ago

rainbow7 commented 5 years ago

BitmapRegionDecoder

它的官方解释

BitmapRegionDecoder can be used to decode a rectangle region from an image. BitmapRegionDecoder is particularly useful when an original image is large and you only need parts of the image

很直白,加载一张图片的一部分。

使用也很简单。rect是一个显示区域,options是图片的设置项。这样就可以显示一张大图的的部分了。显示不同的区域,根据手势调整rect的区域即可。

public Bitmap decodeRegion(Rect rect, BitmapFactory.Options options) {
        BitmapFactory.Options.validate(options);
        synchronized (mNativeLock) {
            checkRecycled("decodeRegion called on recycled region decoder");
            if (rect.right <= 0 || rect.bottom <= 0 || rect.left >= getWidth()
                    || rect.top >= getHeight())
                throw new IllegalArgumentException("rectangle is outside the image");
            return nativeDecodeRegion(mNativeBitmapRegionDecoder, rect.left, rect.top,
                    rect.right - rect.left, rect.bottom - rect.top, options);
        }
    }
firehell commented 5 years ago

https://www.jianshu.com/p/116e05dfd66b 参考这个文章,一些想法 1.用浏览器加载的方法,从上到下一点一点加载,这样的好处是图片清晰度比较高 2.先加载一张缩略图占位,然后压缩整个图片,减少图片的像素加载出图片,会快一些但不够清晰 //尺寸压缩(通过缩放图片像素来减少图片占用内存大小) `public static void sizeCompress(Bitmap bmp, File file) { // 尺寸压缩倍数,值越大,图片尺寸越小 int ratio = 8; // 压缩Bitmap到对应尺寸 Bitmap result = Bitmap.createBitmap(bmp.getWidth() / ratio, bmp.getHeight() / ratio, Config.ARGB_8888); Canvas canvas = new Canvas(result); Rect rect = new Rect(0, 0, bmp.getWidth() / ratio, bmp.getHeight() / ratio); canvas.drawBitmap(bmp, null, rect, null);

    ByteArrayOutputStream baos = new ByteArrayOutputStream();
    // 把压缩后的数据存放到baos中
    result.compress(Bitmap.CompressFormat.JPEG, 100, baos);
    try {
        FileOutputStream fos = new FileOutputStream(file);
        fos.write(baos.toByteArray());
        fos.flush();
        fos.close();
    } catch (Exception e) {
        e.printStackTrace();
    }
}`
chinwetang commented 5 years ago

看到小伙伴回答的,大概是基于加载速度内存优化两个方向的,我没其他想法,拾人牙慧答一下;

加载优化的就是Http协议中的Range / Accept-Range ,他可以指定获取body的一部分,从而达到分块加载图片的效果; 内存优化的方向是,我屏幕只有这么大,那我就只加载跟屏幕一样的Bitmap就好了,超出屏幕外面的就没必要加到内存里面增加负担;

@Override 
protected void onDraw(Canvas canvas) { 
//这个mDecoder就是上面同学说到的BitmapRegionDecoder,options就是BitmapFactory.Options
      Bitmap bm = mDecoder.decodeRegion(mRect, options);
      canvas.drawBitmap(bm, 0, 0, null);
} 

根据手势滑动每次重新计算mRect去获取部分bitmap绘制到屏幕上。

skylarliuu commented 5 years ago

(谢谢上面同学的回答)

加载大长图可以使用 压缩或局部加载的方法。

压缩可以使用BitmapFactory类的相关方法,获取到图片的原始大小后,再计算图片压缩比,最后压缩图片后再显示。不过压缩就会降低图片的清晰度,如果要按原图加载图片,可以用BitmapRegionDecoder实现图片局部加载。

BitmapRegionDecoder可以用来显示图片指定的一个矩形区域。通过自定义View重写onTouchEvent()判断手势滑动位置,从而改变矩形区域的位置。

如果是加载网络大图片,还可以分块请求数据,让图片更快地显示出来。

(分享鸿洋的一篇博客:Android 高清加载巨图方案 拒绝压缩图片

zhengjunke commented 5 years ago

参考简书资料:https://www.jianshu.com/p/4640764bfbc6 图片太大,进行压缩处理: BitmapFactory这个类就提供了多个解析方法(decodeResource、decodeStream、decodeFile等)用于创建Bitmap。我们可以根据图片的来源来选择解析方法。比如如果图片来源于网络,就可以使用decodeStream方法;如果是sd卡里面的图片,就可以选择decodeFile方法;如果是资源文件里面的图片,就可以使用decodeResource方法等。 BitmapFactory为这些方法都提供了一个可选的参数BitmapFactory.Options,用来辅助我们解析图片。这个参数有一个属性inSampleSize,这个属性可以帮助我们来进行图片的压缩。 如果不进行压缩处理,图片超出屏幕显示范围,进行局部加载处理: BitmapRegionDecoder bitmapRegionDecoder = BitmapRegionDecoder.newInstance(inputStream, false); bitmapRegionDecoder.decodeRegion(rect, options); 参数一是一个rect,控制显示区域,参数二是BitmapFactory.Options,你可以控制图片的inSampleSize,inPreferredConfig等。

pioneerz commented 5 years ago

1.对图片进行压缩处理

BitmapFactory.Options options = new BitmapFactory.Options();
        options.inJustDecodeBounds = true;
        BitmapFactory.decodeResource(getResources(), R.mipmap.wei, options);
        options.inSampleSize = Math.max(options.outWidth / DensityUtils.screenWidth(this),
                options.outHeight / DensityUtils.screenHeight(this));
        options.inJustDecodeBounds = false;
        Bitmap bitmap = BitmapFactory.decodeResource(getResources(), R.mipmap.wei, options);
        mImageView.setImageBitmap(bitmap);
        mMemoryText.setText("bitmapMemory="+bitmap.getByteCount() + "   imageMemory="+bitmap.getAllocationByteCount());

这种方案虽然也能够显示,但是不能够保证图片的清晰度。

2.局部展示:

InputStream is = null;
        try {
            is = getAssets().open("wei.png");
            BitmapRegionDecoder decoder = BitmapRegionDecoder.newInstance(is, false);
            BitmapFactory.Options options1 = new BitmapFactory.Options();
            options1.inPreferredConfig = Bitmap.Config.ARGB_8888;
            Bitmap bitmap = decoder.decodeRegion(new Rect(0, 0, DensityUtils.screenWidth(this),
                    DensityUtils.screenHeight(this)- DensityUtils.dip2px(this, 200)), options1);
            mImageView.setImageBitmap(bitmap);
            mMemoryText.setText("bitmapMemory="+bitmap.getByteCount() + "   imageMemory="+bitmap.getAllocationByteCount());
        } catch (IOException e) {
            e.printStackTrace();
        } finally {
            if (is != null) {
                try {
                    is.close();
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
        }

这种方案是加载图片的局部,如果要观察其余部分,可以通过增加手势操作或者触摸事件。

chengying1216 commented 5 years ago

https://www.jianshu.com/p/4640764bfbc6

SeniorDoctorHui commented 5 years ago

对于加载大长图主要有以下一些路径:

1,对于加载网络的图片,如果只是作为缩略图展示的话,可以下载该图片的缩略图来展示,以提高加载速度和省流量的目的。点击详情图片时,才去加载原图。

2,就是使用上面各位同学说的BitmapRegionDecoder这个类来展示局部图片,根据可见区域来加载图片,从而避免oom的问题。但是该类只是Android为我们提供的基本API,实际使用中,你会发现如果在onDraw方法中绘制bitmap对象会占用主线程资源,导致丢帧而造成卡顿的现象。除此之外还需要考虑快速滑动而造成bitmap大量创建和大量回收而生成的抖动问题。而且如果要加载长图,还要有放大缩小的功能,这些也需要我们自己实现。

3,使用SubsamplingScaleImageView框架来加载长图,该框架底层就是使用BitmapRegionDecoder来加载局部图片的,该框架提供了缩放和触摸等手势的功能。该框架为了解决滑动卡顿的问题,对于move事件进行了优化,不会直接加载清晰度高的图片,而是对于原解码率的图片进行scale放大,直到up事件发生后,才会在后台加载相应解码率的图片,生成对应的bitmap,再进行相应的更新。因此滑动时,我们先会看到模糊的图片,然后才能看到清晰的图片。它的缺点是不能直接加载网络的图片,要把网络的图片下载到本地才能加载。

4,使用SketchImageView来加载长图,底层也是BitmapRegionDecoder实现的。这是一套很完整的图片加载框架,对于大图的分块加载,缩放手势,缓存复用缓存回收应有尽有,支持加载网络链接的大图。是一套很不错的加载大图的商业级方案,具体介绍可以看https://github.com/panpf/sketch