可能是最详细的Android图片压缩原理分析(二)—— 鲁班压缩算法解析

本篇文章已授权微信公众号guolin_blog(郭霖)独家发布
稀土掘金链接

前言

        通过上一篇,我们了解了一些关于图片压缩的基础知识,这篇文章我们主要讲解一下鲁班压缩的算法逻辑,很多博客都是从Github上将人家的介绍直接拷贝过来,根本没有什么讲解,就是告诉你一些方法如何调用。

系列教程

可能是最详细的Android图片压缩原理分析(一)—— Android图片压缩必备基础知识
可能是最详细的Android图片压缩原理分析(二)—— 鲁班压缩算法解析
可能是最详细的Android图片压缩原理分析(三)—— 底层哈夫曼压缩讲解
可能是最详细的Android图片压缩原理分析(四)—— 大图加载优化(防止OOM)

一、 鲁班压缩的背景

鲁班压缩 —— Android图片压缩工具,仿微信朋友圈压缩策略。

        目前做App开发总绕不开图片这个元素。但是随着手机拍照分辨率的提升,图片的压缩成为一个很重要问题,随便一张图片都是好几M,甚至几十M,这样的照片加载到app,可想而知,随便加载几张图片,手机内存就不够用了,自然而然就造成了OOM ,所以,Android的图片压缩异常重要

        单纯对图片进行裁切,压缩已经有很多文章介绍。但是裁切成多少压缩成多少却很难控制好,裁切过头图片太小,质量压缩过头则显示效果太差。于是自然想到App巨头——微信会是怎么处理,Luban(鲁班)就是通过在微信朋友圈发送近100张不同分辨率图片对比原图与微信压缩后的图片逆向推算出来的压缩算法。

二、效果与对比

        因为是逆向推算,效果还没法跟微信一模一样,但是已经很接近微信朋友圈压缩后的效果,具体看以下对比!

内容原图LubanWechat
截屏 720P720*1280,390k720*1280,87k720*1280,56k
截屏 1080P1080*1920,2.21M1080*1920,104k1080*1920,112k
拍照 13M(4:3)3096*4128,3.12M1548*2064,141k1548*2064,147k
拍照 9.6M(16:9)4128*2322,4.64M1032*581,97k1032*581,74k
滚动截屏1080*6433,1.56M1080*6433,351k1080*6433,482k

三、Luban算法解析

1、微信的算法解析

  • 第一步进行采样率压缩;
  • 第二步进行宽高的等比例压缩(微信对原图和缩略图限制了最大长宽或者最小长宽);
  • 第三步就是对图片的质量进行压缩(一般75或者70);
  • 第四部就是采用webP的格式。

经过这四部的处理,基本上和微信朋友圈的效果一致,包括文件大小和显示效果

2、Luban的算法解析

Luban压缩目前的步骤只占了微信算法中的第二与第三步,算法逻辑如下:

1. 判断图片比例值,是否处于以下区间内;

  • [1, 0.5625) 即图片处于 [1:1 ~ 9:16) 比例范围内
  • [0.5625, 0.5) 即图片处于 [9:16 ~ 1:2) 比例范围内
  • [0.5, 0) 即图片处于 [1:2 ~ 1:∞) 比例范围内

        简单解释一下:获取图片的比例系数,如果在区间 [1, 0.5625) 中即图片处于 [1:1 ~ 9:16)比例范围内,比例以此类推,如果这个系数小于0.5,那么就给它放到 [1:2 ~ 1:∞)比例范围内。

2. 判断图片最长边是否过边界值;

  • [1, 0.5625) 边界值为:1664 * n(n=1), 4990 * n(n=2), 1280 * pow(2, n-1)(n≥3)
  • [0.5625, 0.5) 边界值为:1280 * pow(2, n-1)(n≥1)
  • [0.5, 0) 边界值为:1280 * pow(2, n-1)(n≥1)

        步骤二:上去一看一脸懵,1664是什么,n是什么,pow又是什么。。。这写的估计只有作者自己能看懂了。其实就是判断图片最长边是否过边界值,此边界值是模仿微信的一个经验值,就是说1664、4990都是经验值,模仿微信的策略

        至于n,是返回的是options.inSampleSize的值,就是采样压缩的系数,是int型,Google建议是2的倍数,所以为了配合这个建议,代码中出现了小于10240返回的是4这种操作。最后说一下pow,其实是 (长边/1280), 这个1280也是个经验值,逆向推出来的,解释到这里逻辑也清晰了。真是坑啊啊,哈哈哈

3. 计算压缩图片实际边长值,以第2步计算结果为准,超过某个边界值则:

  • width / pow(2, n-1),
  • height/ pow(2, n-1)

        步骤三:这个感觉没什么用,还是计算压缩图片实际边长值,人家也说了,以第2步计算结果为准,其实就是晃你的,乍一看 ,这么多步骤,哈哈哈哈,唬你呢!

4. 计算压缩图片的实际文件大小,以第2、3步结果为准,图片比例越大则文件越大。

size = (newW * newH) / (width * height) * m;

  • [1, 0.5625) 则 width & height 对应 1664,4990,1280 * n(n≥3),m 对应 150,300,300;
  • [0.5625, 0.5) 则 width = 1440,height = 2560, m = 200;
  • [0.5, 0) 则 width = 1280,height = 1280 / scale,m = 500;注:scale为比例值

        步骤四:这个感觉也没什么用,这个m应该是压缩比。但整个过程就是验证一下压缩完之后,size的大小,是否超过了你的预期,如果超过了你的预期,将进行重复压缩。

5. 判断第4步的size是否过小

  • [1, 0.5625) 则最小 size 对应 60,60,100
  • [0.5625, 0.5) 则最小 size 都为 100
  • [0.5, 0) 则最小 size 都为 100

        步骤五:这一步也没啥用,也是为了后面循环压缩使用。 这个size就是上面计算出来的,最小 size 对应的值公式为:size = (newW * newH) / (width * height) * m,对应的三个值,就是上面根据图片的比例分成的三组,然后计算出来的。

6. 将前面求到的值压缩图片 width, height, size 传入压缩流程,压缩图片直到满足以上数值

        最后一步也没啥用,看字就知道是为了循环压缩,或许是微信也这样做?既然你已经有了预期,为什么不根据预期直接一步到位呢?但是裁剪的系数压缩的系数怎么调整会达到最优一个效果,我的项目中已经对此功能进行了增加,目前还在内测,没有开源,后期稳定后会开源给大家使用。

3、将算法带入到开源代码中

咱们直接看算法所在类 Engine.java

// 计算采样压缩的值,也就是模仿微信的经验值,核心内容
private int computeSize() {
// 补齐宽度和长度
    srcWidth = srcWidth % 2 == 1 ? srcWidth + 1 : srcWidth;
    srcHeight = srcHeight % 2 == 1 ? srcHeight + 1 : srcHeight;

// 获取长边和短边
    int longSide = Math.max(srcWidth, srcHeight);
    int shortSide = Math.min(srcWidth, srcHeight);

// 获取图片的比例系数,如果在区间[1, 0.5625) 中即图片处于 [1:1 ~ 9:16) 比例
    float scale = ((float) shortSide / longSide);
// 开始判断图片处于那种比例中,就是上面所说的第一个步骤
    if (scale <= 1 && scale > 0.5625) {
// 判断图片最长边是否过边界值,此边界值是模仿微信的一个经验值,就是上面所说的第二个步骤
      if (longSide < 1664) {
// 返回的是 options.inSampleSize的值,就是采样压缩的系数,是int型,Google建议是2的倍数
        return 1;
      } else if (longSide < 4990) {
        return 2;
// 这个10240上面的逻辑没有提到,也是经验值,不用去管它,你可以随意调整
      } else if (longSide > 4990 && longSide < 10240) {
        return 4;
      } else {
        return longSide / 1280 == 0 ? 1 : longSide / 1280;
      }
// 这些判断都是逆向推导的经验值,也可以说是一种策略
    } else if (scale <= 0.5625 && scale > 0.5) {
      return longSide / 1280 == 0 ? 1 : longSide / 1280;
    } else {
    // 此时图片的比例是一个长图,采用策略向上取整
      return (int) Math.ceil(longSide / (1280.0 / scale));
    }
  }
  
// 图片旋转方法
private Bitmap rotatingImage(Bitmap bitmap, int angle) {
    Matrix matrix = new Matrix();
// 将传入的bitmap 进行角度旋转
    matrix.postRotate(angle);
// 返回一个新的bitmap
    return Bitmap.createBitmap(bitmap, 0, 0, bitmap.getWidth(), bitmap.getHeight(), matrix, true);
  }

// 压缩方法,返回一个File
File compress() throws IOException {
// 创建一个option对象
    BitmapFactory.Options options = new BitmapFactory.Options();
// 获取采样压缩的值
    options.inSampleSize = computeSize();
// 把图片进行采样压缩后放入一个bitmap, 参数1是bitmap图片的格式,前面获取的
    Bitmap tagBitmap = BitmapFactory.decodeStream(srcImg.open(), null, options);
// 创建一个输出流的对象
    ByteArrayOutputStream stream = new ByteArrayOutputStream();
// 判断是否是JPG图片
    if (Checker.SINGLE.isJPG(srcImg.open())) {
// Checker.SINGLE.getOrientation这个方法是检测图片是否被旋转过,对图片进行矫正
      tagBitmap = rotatingImage(tagBitmap, Checker.SINGLE.getOrientation(srcImg.open()));
    }
// 对图片进行质量压缩,参数1:通过是否有透明通道来判断是PNG格式还是JPG格式,
// 参数2:压缩质量固定为60,参数3:压缩完后将bitmap写入到字节流中
    tagBitmap.compress(focusAlpha ? Bitmap.CompressFormat.PNG : Bitmap.CompressFormat.JPEG, 60, stream);
// bitmap用完回收掉
    tagBitmap.recycle();
// 将图片流写入到File中,然后刷新缓冲区,关闭文件流和Byte流
    FileOutputStream fos = new FileOutputStream(tagImg);
    fos.write(stream.toByteArray());
    fos.flush();
    fos.close();
    stream.close();
    return tagImg;
  }

整个讲解已经在代码里已经做了注释,要结合前面逻辑看,就明白了。

四、Luban原框架问题分析

1、原框架问题分析

  • 解码前没有对内存做出预判
  • 质量压缩写死 60
  • 没有提供图片输出格式选择
  • 不支持多文件合理并行压缩,输出顺序和压缩顺序不能保证一致
  • 检测文件格式和图像的角度多次重复创建InputStream,增加不必要开销,增加OOM风险
  • 可能出现内存泄漏,需要自己合理处理生命周期
  • 图片要是有大小限制,只能进行重复压缩
  • 原框架用的还是RxJava1.0

2、技术改造方案

  • 解码前利用获取的图片宽高对内存占用做出计算,超出内存的使用RGB-565尝试解码
  • 针对质量压缩的时候,提供传入质量系数的接口
  • 对图片输出支持多种格式,不局限于File
  • 利用协程来实现异步压缩和并行压缩任务,可以在合适时机取消协程来终止任务
  • 参考Glide对字节数组的复用,以及InputStream的mark()、reset()来优化重复打开开销
  • 利用LiveData来实现监听,自动注销监听。
  • 压缩前计算好大小,逆向推导出尺寸压缩系数和质量压缩系数
  • 现在已经出了RxJava3和协程,但大多数项目中已经有了线程池,要利用项目中的线程池,而不是导入一个三方库就建一个线程池而造成资源浪费

五、篇幅总结

1、结语

        Luban压缩当初出来的时候号称 "可能是最接近微信朋友圈的图片压缩算法" ,但这个库已经三四年没有维护了,随着产品的迭代微信已经也不是当初的那个微信了,Luban压缩的库也要进行更新了。所以为了适应现在的项目,我之后会根据上面的技术改造方案对图片压缩出一个船新版本的库,更为强大。

2、再续前缘

        Luban还有一个turbo分支,这个分支主要是为了兼容Android 7.0以前的系统版本,导入libjpeg-turbojni版本。
        libjpeg-turbo是一个C语音编写的高效JPEG图像处理库,Android系统在7.0版本之前内部使用的是libjpeg非turbo版,并且为了性能关闭了Huffman编码。在7.0之后的系统内部使用了libjpeg-turbo库并且启用Huffman编码
        那么什么是Huffman编码呢?前面提到的skio引擎又是什么东西呢?让我们一起进入新的篇章:底层哈夫曼压缩讲解

Luban(鲁班)——Android图片压缩工具,仿微信朋友圈压缩策略。项目描述目前做app开发总绕不开图片这个元素。但是随着手机拍照分辨率的提升,图片的压缩成为一个很重要的问题。单纯对图片进行裁切,压缩已经有很多文章介绍。但是裁切成多少,压缩成多少却很难控制好,裁切过头图片太小,质量压缩过头则显示效果太差。于是自然想到app巨头“微信”会是怎么处理,Luban(鲁班)就是通过在微信朋友圈发送近100张不同分辨率图片,对比原图与微信压缩后的图片逆向推算出来的压缩算法。因为有其他语言也想要实现 Luban,所以描述了一遍算法步骤 因为是逆向推算,效果还没法跟微信一模一样,但是已经很接近微信朋友圈压缩后的效果,具体看以下对比!效果与对比内容原图LubanWechat截屏 720P720*1280,390k720*1280,87k720*1280,56k截屏 1080P1080*1920,2.21M1080*1920,104k1080*1920,112k拍照 13M(4:3)3096*4128,3.12M1548*2064,141k1548*2064,147k拍照 9.6M(16:9)4128*2322,4.64M1032*581,97k1032*581,74k滚动截屏1080*6433,1.56M1080*6433,351k1080*6433,482k导入compile 'io.reactivex:rxandroid:1.2.1' compile 'io.reactivex:rxjava:1.1.6' compile 'top.zibin:Luban:1.0.5'使用Listener方式Luban内部采用io线程进行图片压缩,外部调用只需设置好结果监听即可Luban.get(this)     .load(File)                     //传人要压缩的图片     .putGear(Luban.THIRD_GEAR)      //设定压缩档次,默认三挡     .setCompressListener(new OnCompressListener() { //设置回调         @Override         public void onStart() {             //TODO 压缩开始前调用,可以在方法内启动 loading UI         }         @Override         public void onSuccess(File file) {             //TODO 压缩成功后调用,返回压缩后的图片文件         }         @Override         public void onError(Throwable e) {             //TODO 当压缩过去出现问题时调用         }     }).launch();    //启动压缩RxJava方式RxJava 调用方式请自行随意控制线程Luban.get(this)         .load(file)         .putGear(Luban.THIRD_GEAR)         .asObservable()         .subscribeOn(Schedulers.io())         .observeOn(AndroidSchedulers.mainThread())         .doOnError(new Action1() {             @Override             public void call(Throwable throwable) {                 throwable.printStackTrace();             }         })         .onErrorResumeNext(new Func1>() {             @Override             public Observable<? extends File> call(Throwable throwable) {                 return Observable.empty();             }         })         .subscribe(new Action1() {             @Override             public void call(File file) {                 //TODO 压缩成功后调用,返回压缩后的图片文件             }         }); 标签:Luban(鲁班)
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值