徒手撸一个框架-MasterImageCompress图片压缩框架

1,807 阅读6分钟

MasterImageCompress

github链接:
github.com/xiaojigugu/…

线程池+队列+观察者模式+建造者模式 实现多线程图片压缩

使用

  1. Add it in your root build.gradle at the end of repositories:
	allprojects {
		repositories {
			...
			maven { url 'https://jitpack.io' }
		}
	}
  1. Add the dependency:
	dependencies {
	        implementation 'com.github.xiaojigugu:MasterImageCompress:1.0.1'
	}
  1. start
    <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"/>
    <uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE"/>
            //配置压缩条件
     CompressConfig compressConfig = CompressConfig
           .builder()
           .keepSource(true) //是否保留源文件
           //压缩方式,分为TYPE_QUALITY、TYPE_PIXEL、TYPE_PIXEL_AND_QUALITY,慎用单独的TYPE_QUALITY(很容易OOM)!
           .comPressType(CompressConfig.TYPE_PIXEL)
           //目标长边像素,对TYPE_PIXEL有效(eg:原图分辨率:7952 X 5304,压缩后7952最终会小于1280)
           .maxPixel(1280)
           //目标大小200kb以内,对TYPE_QUALITY有效
           .targetSize(200 * 1024)
           .format(Bitmap.CompressFormat.WEBP, Bitmap.Config.ARGB_8888) //压缩配置
           .outputDir("storage/emulated/0/DCIM/image_compressed/") //输出目录
           .build();
             
     //或者一句话CompressConfig compressConfig=CompressConfig.getDefault();
                   
     //添加需要压缩的图片路径       
     List<String> images = new ArrayList<>();
     for (File file1 : files) {
         String path = file1.getAbsolutePath();
         SystemOut.println("ImageCompressor ===> image,path=" + path);
         images.add(path);
     }
                    
     ImageCompressManager.builder()
           .paths(images)
           .config(compressConfig)
           .listener(new ImageCompressListener() {
                @Override
                public void onStart() {
                     SystemOut.println("ImageCompressor ===>开始压缩");
                }

                @Override
                public void onSuccess(List<ImageInstance> images) {
                     SystemOut.println("ImageCompressor ===>压缩成功");
                }

                @Override
                public void onFail(boolean allOrSingle, List<ImageInstance> images, CompressException e) {
                      SystemOut.println("ImageCompressor ===>压缩失败,isAll=" + allOrSingle);
                }
           })
           .compress();

效率对比

左侧原图大小,右侧压缩后大小

用时(基于mumu模拟器环境):

线程池说明

我只开了3个核心线程,最大5个线程(PS:这玩意开多少完全看项目需求)


固定执行一个核心线程,用来取压缩任务

观察者模式

通过java内置的ObservaableObserver实现简单的观察者模式
ImageCompressManager中数据更新时,Compressor压缩工具类会收到通知,通知开启多线程执行压缩任务

ImageCompressManager类中:

Compressor类中:

图片压缩原理解析

其实网上关于图片压缩的代码一搜一大堆,基本上都差不多,包括很热门的 luban框架。 写这个框架的初衷无非就是luban可配置项太少,导致不能达到我自己的需求,所以干脆重新写一个。
Android图片压缩说起来大体有三种手段,1. 采样压缩 2.质量压缩 3. 使用libjpeg
先说说libjpeg,使用libjpeg进行压缩操作需要引入相应的so包,这就会导致包体的增加,对于绝大部分项目来说,我们的图片压缩要求并没有那么严格。所以很显然,用这种方法就显得得不偿失了。最终 MasterImageCompress 还是采用了前两种方法,并且将选择权交给开发者手上,可以单独使用其中一个,也可以两种混用。

  1. 采样压缩(我喜欢叫像素压缩)
    采样压缩压缩的是像素大小(分辨率) 采样压缩的核心代码:
        BitmapFactory.Options options = new BitmapFactory.Options();
        //计算采样率只需要宽高
        options.inJustDecodeBounds = true;
        //此处在option中已取得宽高
        BitmapFactory.decodeFile(imageInstance.getInputPath(), options);
        //计算并设置采样率
        options.inSampleSize = calculateSampleSize(options, compressConfig);
        //重新设置为decode整张图片,准备压缩
        options.inJustDecodeBounds = false;
        //应用新配置
        Bitmap bitmap = BitmapFactory.decodeFile(imageInstance.getInputPath(), options);

说白了,采样压缩就是根据图片宽高合理的计算一个缩放比(采样率),然后通过这个缩放比去忽略一部分像素点来达到压缩图片的目的。但是由于Bitmap糟糕的内存占用,所以通常获取图片的宽高时,我们会通过 inJustDecodeBounds来控制取边还是取整张图,那么为什么这个属性可以做到只取边呢,我们看一下源码中的注释:

翻译过来大体意思就是:
如果设置为true,decoder将不会返回bitmap,但是它的外部区域将会被设置(其实是放入了BitmapFactory.Option中),允许调用者在不占用内存的情况下查询bitmap。 那么设置了为true以后就可以拿到长宽属性了吗?我们再往下看看BitmapFactory.Option类中关于长宽的注释:
看一下我们关心的地方,注释说道:如果inJustDecodeBounds设置为true,outWidth/outHeight将会是输入图片的width/height并且不会进行任何的缩放
那么到这里我们就拿到了宽高属性,接下来就是计算缩放比(采样率)
就是计算长边与目标像素大小的比值:
原图分辨率:
缩放比=长边像素/目标像素效果: (int)4032/1280=3
缩放比=长边像素/目标像素+1:(int)4032/1280+1=4(MasterImageCompress采用这种)

那么为什么要+1呢?我们来看一下源码注释:
英语10级水准的我现场翻译:
!@#¥%……&()——+——)(&……%¥#@!@#¥%……&*()................................(不想看),直接看For Example后面的注释=>如果inSampleSize == 4则返回的图片的宽高将是原图的1/4,像素数量将是原来的1/16;inSampleSize <= 1时将按照1处理;注意:decoders使用的常量是2的幂方数,任何其他不是2的幂方数的数值将会向下找最接近他的2的幂方数。
所以,我们计算出来的缩放比在最终使用的时候很可能小于我们的目标缩放比,这样计算出来的像素肯定是要大于我们需求的最大像素的,所以这里我们选择缩放比=长边像素/目标像素+1。 这里我还是计算一下,免得小伙伴没看懂:
原图大小:3024 X 4032 得:长边像素:4032
缩放比:4032 / 1280 = 3(demo中设置取1280作为我们能忍受的最大图片像素大小)
  实际缩放比:2(小于3且最接近3的2的幂方数)
  压缩后像素:3024 / 2 = 1512   4032 / 2= 2016 =>最终像素:1512 X 2016
缩放比+1=> 4032 / 1280 +1 = 4
  实际缩放比:4
  压缩后像素:3024 / 4 = 756  4032 / 4= 1008 =>最终像素:1512 X 4032
最终我们能够保证得到一个像素小于目标最大像素的图片,但是不能确保刚好等于目标最大像素。

  1. 质量压缩
    质量压缩改变的是透明度、位深等,不能改变加载出来的Bitmap占用的内存大小,但能切实改变磁盘占用大小
    核心代码就一句:
    Bitmap.compress(compressConfig.getComPressFormat(), quality, byteArrayOutputStream); 我们就是通过修改quality参数达到压缩的目的,老规矩,看一下源码注释:
    真·人工智能翻译:
    向给定的输出流中写入一个压缩版本的bitmap.如果返回true,则可以通过将相应的inputstream传递给BitmapFactory.decodeStream()来重构bitmap。注意:并非所有格式都直接支持所有bitmap配置,因此很可能从BitmapFactory返回的bitmap会有不同的位深,并且/或者可能丢失每个像素的alpha值(例如:JPEG只支持不透明像素)。
    @param quality:提示压缩器,取值0-100.0代表最小尺寸,100代表最大尺寸,一些诸如PNG这样无损的图片格式将会忽略质量设置。 好,看了注释没啥说的了,就是直接调用compress()压缩呗,quality递减。
    /**
     * 质量压缩
     */
    private void compressQuality(ImageInstance imageInstance, CompressConfig compressConfig) {
        SystemOut.println("ImageCompressor ===>compressQuality()");
        Bitmap inputBitmap = BitmapFactory.decodeFile(imageInstance.getInputPath());
        ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream();
        //上来就先压90%
        int quality = 90;
        inputBitmap.compress(compressConfig.getComPressFormat(), quality, byteArrayOutputStream);
        //如果压缩后图片还是>targetSize,则继续压缩(整个过程是在线程池中开了线程处理的)
        while (byteArrayOutputStream.toByteArray().length > compressConfig.getTargetSize()) {
            byteArrayOutputStream.reset();
            quality -= 10;
            if (quality <= 10) {//为了缩短压缩次数,节约大家时间,每次质量比上次减少10%
                quality = 5;//限制最低压缩到5
            }
            inputBitmap.compress(compressConfig.getComPressFormat(), quality, byteArrayOutputStream);
            if (quality == 5) {
                //压缩结束
                inputBitmap.recycle();
                break;
            }
        }
    }

ok~到此结束