我记得在2017年写项目的时候图片压缩工具就是用的Luban,时隔多年后在近期的俩个项目中依旧发现了Luban的身影,所以觉得有必要好好记录一下老朋友

想学习一款新技术最好的方式就是去看 官方文档(Github - Luban) ,因为除了会介绍基本的使用之外,如果你有兴趣的话也可以学一下源码;而我记录Blog更多的是我的一个学习、反思的过程,希望也可以帮到你


  • 基本了解
  • 压缩效果
  • 压缩算法
  • 方法列表
  • 基础使用
  • 异步调用
  • 同步调用
  • 项目实战


基本了解

Android市场 相比 Ios市场 更具有开放性、广泛性;在国内也有着众多厂商,同时意味着也有更多的手机品牌,这就面临着每款手机的 拍照分辨率,屏幕显示分辨率 可能有所不同,故而就会出现一些小问题;

随着手机拍照分辨率的提升,图片压缩就是一个很重要的问题;如果只是单纯对图片进行裁切,压缩可能很难满足诉求(关于原始压缩方式和原理,等后续有时间补一篇),这是因为没有一个标准,如果裁切过头图片太小,质量压缩过头则显示效果太差

个人见解:所谓的标准,可以说是策略,指的是不同场景下采用不同的方式(如果不理解,可以看看 策略模式,开放下思维)

在一些项目实战场景,可以看出图片压缩的重要性,例如~

  • 加载大图时浪费太多流量,同时还可能发生OOM(现在)
  • 上传大图时浪费太多流量,同时还可能出现上传失败的结果
压缩效果

于是这位前辈就想到App巨头“微信”会是怎么处理,Luban(鲁班)就是通过在微信朋友圈发送近100张不同分辨率图片,对比原图与微信压缩后的图片逆向推算出来的压缩算法

同一张图片经过 Luban压缩 WeChat压缩 de 效果与对比(压缩效果很好,所以沿用到了现在)

android 图片压缩 鲁班 和libjpeg哪个好 luban图片压缩_Luban

压缩算法

作者考虑到其他语言也想要实现Luban,所以描述了一遍算法步骤

第三挡压缩参考最新版微信压缩效果

作者在2018、2019年后就没再维护了,有俩种可能一种就是Luban已经很稳定了,另一种就是开启人生新篇章了吧

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

  • [1, 0.5625) 即图片处于 [1:1 ~ 9:16) 比例范围内
  • [0.5625, 0.5) 即图片处于 [9:16 ~ 1:2) 比例范围内
  • [0.5, 0) 即图片处于 [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)

3.计算压缩图片实际边长值,以第2步计算结果为准,超过某个边界值则:width / pow(2, n-1),height/pow(2, n-1)
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为比例值

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

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

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

方法列表

方法

描述

load

传入原图

filter

设置开启压缩条件

ignoreBy

不压缩的阈值,单位为K

setFocusAlpha

设置是否保留透明通道

setTargetDir

缓存压缩图片路径

setCompressListener

压缩回调接口

setRenameListener

压缩前重命名接口


基础使用

以下使用方式在 Luban 都可以看到,是作者提供的一些调用示例,方便大家快速使用

build.gradle

implementation 'top.zibin:Luban:1.1.8'
异步调用

Luban内部采用IO线程进行图片压缩,外部调用只需设置好结果监听即可:

Luban.with(this)
        .load(photos)
        .ignoreBy(100)
        .setTargetDir(getPath())
        .filter(new CompressionPredicate() {
          @Override
          public boolean apply(String path) {
            return !(TextUtils.isEmpty(path) || path.toLowerCase().endsWith(".gif"));
          }
        })
        .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调用为例(应该也支持用协程写,只要将压缩操作放到子线程即可

Flowable.just(photos)
    .observeOn(Schedulers.io())
    .map(new Function<List<String>, List<File>>() {
      @Override public List<File> apply(@NonNull List<String> list) throws Exception {
        // 同步方法直接返回压缩后的文件
        return Luban.with(MainActivity.this).load(list).get();
      }
    })
    .observeOn(AndroidSchedulers.mainThread())
    .subscribe();

项目实战

以下均为我在项目中使用Luban的一些伪代码,可供借鉴

ImageCompressor

package com.jsmedia.jsmanager.utils.imgUtils

import android.content.Context
import top.zibin.luban.Luban
import top.zibin.luban.OnCompressListener
import java.io.File


object ImageCompressor {
    private const val IGNORE_SIZE = 1

    fun compress(context: Context, imageFile: File, compressResult: CompressResult) {
        Luban.with(context)
            .load(imageFile)
            .ignoreBy(IGNORE_SIZE)
            .setCompressListener(object : OnCompressListener {
                override fun onStart() {
                }

                override fun onSuccess(file: File) {
                    compressResult.onSuccess(file)
                }

                override fun onError(e: Throwable) {
                    compressResult.onError(e)
                }
            }).launch()
    }


    interface CompressResult {
        fun onSuccess(file: File)
        fun onError(e: Throwable)
    }
}

调用方式

ImageCompressor.compress(
	   this@StorePhotoActivity,
	   File(filePathByUri),
	   object : ImageCompressor.CompressResult {
	        override fun onSuccess(file: File) {
	        }
	
	        override fun onError(e: Throwable) {
	        }
	   })

项目 - 伪代码

/**
     * 拍照
     * */
    fun onCamera(view: View?) {
        ImagePicker.getInstance().startCamera(this, false, object : PickCallback() {
            override fun onPermissionDenied(permissions: Array<String?>?, message: String?) {
                ToastUtils.showShort(message)
            }

            override fun cropConfig(builder: ActivityBuilder) {
                builder.setMultiTouchEnabled(true)
                    .setGuidelines(CropImageView.Guidelines.ON_TOUCH)
                    .setCropShape(CropImageView.CropShape.OVAL)
                    .setRequestedSize(400, 400)
                    .setFixAspectRatio(true)
                    .setAspectRatio(1, 1)
            }

            override fun onPickImage(imageUri: Uri?) {
                super.onPickImage(imageUri)
                Log.e("tag", imageUri.toString())
                var bitmap = BitmapFactory.decodeStream(imageUri?.let {
                    contentResolver.openInputStream(
                        it
                    )
                });

                var filePathByUri = FileUtil.getFilePathByUri(this@StorePhotoActivity, imageUri)
                ImageCompressor.compress(
                    this@StorePhotoActivity,
                    File(filePathByUri),
                    object : ImageCompressor.CompressResult {
                        override fun onSuccess(file: File) {
                            mViewModel.upload(
                                2,
                                "album",
                                file.absolutePath,
                                MMKV.defaultMMKV().decodeString("storeId").toString()
                            )
                        }

                        override fun onError(e: Throwable) {
                        }
                    })
            }

            override fun onCropImage(imageUri: Uri?) {
            }

        })
    }

    /**
     * 相册
     * */
    fun onGallery(view: View?) {
        ImagePicker.getInstance().startGallery(this, false, object : PickCallback() {
            override fun onPermissionDenied(permissions: Array<String?>?, message: String?) {
                ToastUtils.showShort(message)
            }

            override fun onPickImage(imageUri: Uri?) {
                Log.e("tag", imageUri.toString())
                var bitmap = BitmapFactory.decodeStream(imageUri?.let {
                    contentResolver.openInputStream(
                        it
                    )
                });

                var filePathByUri = FileUtil.getFilePathByUri(this@StorePhotoActivity, imageUri)
                ImageCompressor.compress(
                    this@StorePhotoActivity,
                    File(filePathByUri),
                    object : ImageCompressor.CompressResult {
                        override fun onSuccess(file: File) {
                            mViewModel.upload(
                                2,
                                "album",
                                file.absolutePath,
                                MMKV.defaultMMKV().decodeString("storeId").toString()
                            )
                        }

                        override fun onError(e: Throwable) {
                        }
                    })

            }
        })
    }

    override fun onRequestPermissionsResult(
        requestCode: Int,
        permissions: Array<String?>,
        grantResults: IntArray
    ) {
        super.onRequestPermissionsResult(requestCode, permissions, grantResults)
        ImagePicker.getInstance()
            .onRequestPermissionsResult(this, requestCode, permissions, grantResults)
    }

    override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
        super.onActivityResult(requestCode, resultCode, data)
        ImagePicker.getInstance().onActivityResult(this, requestCode, resultCode, data)
    }