flutter_luban和flutter_image_compress
最近在做flutter的项目的时候用到了图片上传和图片压缩,开始使用的压缩库是flutter_luban,压缩的效果不错,但是在一些比较老的手机上面压缩的效率很慢,一个5、6M的图片压缩需要大概30秒的时间,后来获取图片的时候先把图片的质量缩小了,然后再压缩,压缩的时长并没有改变。再后来只能先换了一个三方库flutter_image_compress先解决问题,然后有时间研究了下之前的库压缩慢的问题进行对比。

flutter_luban压缩介绍

flutter_luban方法调用起来也比较简单,先将需要压缩的图片封装到CompressObject类中,然后调用Luban.compressImage既可。

CompressObject compressObject = CompressObject(
    imageFile: image,
    path: path
  );
  Luban.compressImage(compressObject).then((_path) {
    // _path为压缩后图片路径
    File newImage = new File(_path);
  });

然后来看下compressImage方法的具体实现

static Future<String> compressImage(CompressObject object) async {
    return compute(_lubanCompress, object);
  }

然后就是调用到了_lubanCompress这个方法。从这个方法可以看出,flutter_luban只支持jpg和png格式的图片压缩,然后基本的步骤为

  • 图片的宽高进行的一系列的计算和校验
  • 然后根据不同的请款进行不同的压缩方式,其中例如宽高比例在0.5626到1之间,高度小于1664,图片大小小于150K,则按照传入的压缩比例进行图片质量压缩。
  • 如果不需要直接进行质量压缩的,会根据传入的压缩模式进行压缩
  • 没有传入压缩模式的,则根据图片大小,大于500K则进行_small2LargeCompressImage,小于500K则进行_large2SmallCompressImage
static String _lubanCompress(CompressObject object) {
    // 首先是将文件类型转换为图片类型
    // 这个转换过程大概耗费了10S左右,因为需要大量的计算
    Image image = decodeImage(object.imageFile.readAsBytesSync());
    var length = object.imageFile.lengthSync();
    print(object.imageFile.path);
    bool isLandscape = false;
    // 这里可以看出该算法只支持jpg和png的图片类型
    const List<String> jpgSuffix = ["jpg", "jpeg", "JPG", "JPEG"];
    const List<String> pngSuffix = ["png", "PNG"];
    // 判断图片类型
    bool isJpg = _parseType(object.imageFile.path, jpgSuffix);
    bool isPng = false;

    if (!isJpg) isPng = _parseType(object.imageFile.path, pngSuffix);

    // 获取图片大小,保持高>宽的方向,计算宽高比
    double size;
    int fixelW = image.width;
    int fixelH = image.height;
    double thumbW = (fixelW % 2 == 1 ? fixelW + 1 : fixelW).toDouble();
    double thumbH = (fixelH % 2 == 1 ? fixelH + 1 : fixelH).toDouble();
    double scale = 0;
    if (fixelW > fixelH) {
      scale = fixelH / fixelW;
      var tempFixelH = fixelW;
      var tempFixelW = fixelH;
      fixelH = tempFixelH;
      fixelW = tempFixelW;
      isLandscape = true;
    } else {
      scale = fixelW / fixelH;
    }
    // 存储文件
    var decodedImageFile;
    if (isJpg)
      decodedImageFile = new File(
          object.path + '/img_${DateTime.now().millisecondsSinceEpoch}.jpg');
    else if (isPng)
      decodedImageFile = new File(
          object.path + '/img_${DateTime.now().millisecondsSinceEpoch}.png');
    else
      throw Exception("flutter_luban don't support this image type");
    if (decodedImageFile.existsSync()) {
      decodedImageFile.deleteSync();
    }
    // 对于图片的长款进行一系列的计算
    var imageSize = length / 1024;
    if (scale <= 1 && scale > 0.5625) {
      if (fixelH < 1664) {
        if (imageSize < 150) {
          // 如果宽高比例在0.5626到1之间,高度小于1664,图片大小小于150K,则按照传入的压缩比例进行图片质量压缩
          decodedImageFile
              .writeAsBytesSync(encodeJpg(image, quality: object.quality));
          return decodedImageFile.path;
        }
        size = (fixelW * fixelH) / pow(1664, 2) * 150;
        size = size < 60 ? 60 : size;
      } else if (fixelH >= 1664 && fixelH < 4990) {
        thumbW = fixelW / 2;
        thumbH = fixelH / 2;
        size = (thumbH * thumbW) / pow(2495, 2) * 300;
        size = size < 60 ? 60 : size;
      } else if (fixelH >= 4990 && fixelH < 10240) {
        thumbW = fixelW / 4;
        thumbH = fixelH / 4;
        size = (thumbW * thumbH) / pow(2560, 2) * 300;
        size = size < 100 ? 100 : size;
      } else {
        int multiple = fixelH / 1280 == 0 ? 1 : fixelH ~/ 1280;
        thumbW = fixelW / multiple;
        thumbH = fixelH / multiple;
        size = (thumbW * thumbH) / pow(2560, 2) * 300;
        size = size < 100 ? 100 : size;
      }
    } else if (scale <= 0.5625 && scale >= 0.5) {
      if (fixelH < 1280 && imageSize < 200) {
        decodedImageFile
            .writeAsBytesSync(encodeJpg(image, quality: object.quality));
        return decodedImageFile.path;
      }
      int multiple = fixelH / 1280 == 0 ? 1 : fixelH ~/ 1280;
      thumbW = fixelW / multiple;
      thumbH = fixelH / multiple;
      size = (thumbW * thumbH) / (1440.0 * 2560.0) * 200;
      size = size < 100 ? 100 : size;
    } else {
      int multiple = (fixelH / (1280.0 / scale)).ceil();
      thumbW = fixelW / multiple;
      thumbH = fixelH / multiple;
      size = ((thumbW * thumbH) / (1280.0 * (1280 / scale))) * 500;
      size = size < 100 ? 100 : size;
    }
    if (imageSize < size) {
      decodedImageFile
          .writeAsBytesSync(encodeJpg(image, quality: object.quality));
      return decodedImageFile.path;
    }
    Image smallerImage;
    if (isLandscape) {
      smallerImage = copyResize(image,
          width: thumbH.toInt(),
          height: object.autoRatio ? null : thumbW.toInt());
    } else {
      smallerImage = copyResize(image,
          width: thumbW.toInt(),
          height: object.autoRatio ? null : thumbH.toInt());
    }
    if (decodedImageFile.existsSync()) {
      decodedImageFile.deleteSync();
    }
    // 根据传入的压缩模式进行压缩
    if (object.mode == CompressMode.LARGE2SMALL) {
      _large2SmallCompressImage(
        image: smallerImage,
        file: decodedImageFile,
        quality: object.quality,
        targetSize: size,
        step: object.step,
        isJpg: isJpg,
      );
    } else if (object.mode == CompressMode.SMALL2LARGE) {
      _small2LargeCompressImage(
        image: smallerImage,
        file: decodedImageFile,
        quality: object.step,
        targetSize: size,
        step: object.step,
        isJpg: isJpg,
      );
    } else {
      // 没有传压缩模式,则判断图片大小是否大于500K
      if (imageSize < 500) {
        _large2SmallCompressImage(
          image: smallerImage,
          file: decodedImageFile,
          quality: object.quality,
          targetSize: size,
          step: object.step,
          isJpg: isJpg,
        );
      } else {
        // 本次进行测试的图片为4M左右所以调用了以下方法
        _small2LargeCompressImage(
          image: smallerImage,
          file: decodedImageFile,
          quality: object.step,
          targetSize: size,
          step: object.step,
          isJpg: isJpg,
        );
      }
    }
    return decodedImageFile.path;
  }

因为如今手机拍摄的照片都偏大,平均在3~6M左右,所以本次选取的是一个4M左右的图片,则简单看一下_small2LargeCompressImage方法。

static _small2LargeCompressImage({
    Image image,
    File file,
    quality,
    targetSize,
    step,
    bool isJpg: true,
  }) {
    // 根据图片类型进行处理
    if (isJpg) {
      var im = encodeJpg(image, quality: quality);
      var tempImageSize = Uint8List.fromList(im).lengthInBytes;
      if (tempImageSize / 1024 < targetSize && quality <= 100) {
        quality += step;
        // 这里可以看出使用了递归,如果压缩效果不够的话会进行多次的压缩,
        // 所以之前手机压缩图片使用了30S,这里连续压缩了6次才达到想要的效果,耗费了大概12S
        _small2LargeCompressImage(
          image: image,
          file: file,
          quality: quality,
          targetSize: targetSize,
          step: step,
          isJpg: isJpg,
        );
        return;
      }
      file.writeAsBytesSync(im);
    } else {
      _compressPng(
        image: image,
        file: file,
        targetSize: targetSize,
        large2Small: false,
      );
    }
  }

所以flutter_luban就是根据图片的大小以及宽高的情况,对图片进行重复的质量压缩,直到达到预期的效果为止,如果是内存较小或者cpu运算能力差的手机,则会耗费很长时间。再有就是,该方法没有单独开线程进行压缩,然后我使用的时候也没有开线程,所以在主线程中导致更加慢。

flutter_image_compress压缩介绍

flutter_image_compress算法使用起来也比较简单,但是这个方法压缩之后返回的是一个二进制数组也就是byte串,如果需要文件格式还需要再转一下文件或者图片格式。

Future<List<int>> testCompressFile(File file) async {
  final result = await FlutterImageCompress.compressWithFile(
    file.absolute.path,
    minWidth: 2300,//压缩后的最小宽度
    minHeight: 1500,//压缩后的最小高度
    quality: 20,//压缩质量
    rotate: 0,//旋转角度
  );
  return result;
}

await testCompressFile(image).then((value){
  String newPath = image.path.replaceAll(".jpg", "1.jpg");
  File file = new File(newPath);
  //保存压缩后图片
  file.writeAsBytes(value).then((newImage){
      //newImage为压缩后图片
  });
});

然后看下compressWithFile方法,这个方法的话首先是校验了下必传的参数,然后判断会否传了文件的路径,有路径则通过channel通道调用到Android的代码中进行压缩的操作。

static Future<List<int>> compressWithFile(
    String path, {
    int minWidth = 1920,
    int minHeight = 1080,
    int inSampleSize = 1,
    int quality = 95,
    int rotate = 0,
    bool autoCorrectionAngle = true,
    CompressFormat format = CompressFormat.jpeg,
    bool keepExif = false,
  }) async {
    assert(
      path != null,
      "A non-null String must be provided to FlutterImageCompress.",
    );
    if (path == null || !File(path).existsSync()) {
      return [];
    }

    final support = await _validator.checkSupportPlatform(format);
    if (!support) {
      return null;
    }

    // 通过channel调用到Android代码
    final result = await _channel.invokeMethod("compressWithFile", [
      path,
      minWidth,
      minHeight,
      quality,
      rotate,
      autoCorrectionAngle,
      _convertTypeToInt(format),
      keepExif,
      inSampleSize,
    ]);
    return convertDynamic(result);
  }

然后就要去Android代码中找相应的调用方法了。首先找到接受这个调用的文件,因为包名是flutter_image_compress,所以在Android代码下会有一个flutter_image_compress的文件夹,存放的就是这个三方库所需要的代码,其中FlutterImageCompressPlugin中的onMethodCall方法就是作为接受flutter端的调用的方法。我们可以看到compressWithFile就在其中,就可以跟踪对应的代码进行分析了。

Android 鲁班压缩网络图片 luban图片压缩_List

 继续跟踪CompressFileHandler方法,首先就是对于参数的获取和校验以及图片格式的校验,然后开始压缩图片,进行handleFile操作。

class CompressFileHandler(private val call: MethodCall, result: MethodChannel.Result) : ResultHandler(result) {

  companion object {
    @JvmStatic
    private val executor = Executors.newFixedThreadPool(5)
  }

  fun handle(registrar: PluginRegistry.Registrar) {
    executor.execute {
      @Suppress("UNCHECKED_CAST") val args: List<Any> = call.arguments as List<Any>
      // 获取flutter端发送的参数
      val filePath = args[0] as String
      var minWidth = args[1] as Int
      var minHeight = args[2] as Int
      val quality = args[3] as Int
      val rotate = args[4] as Int
      val autoCorrectionAngle = args[5] as Boolean
      val format = args[6] as Int
      val keepExif = args[7] as Boolean
      val inSampleSize = args[8] as Int

      // 校验图片格式,现支持jpeg, png, heic, webp
      val formatHandler = FormatRegister.findFormat(format)

      if (formatHandler == null) {
        log("No support format.")
        reply(null)
        return@execute
      }

      // 是否自动校验角度
      val exifRotate =
              if (autoCorrectionAngle) {
                val bytes = File(filePath).readBytes()
                Exif.getRotationDegrees(bytes)
              } else {
                0
              }

      if (exifRotate == 270 || exifRotate == 90) {
        val tmp = minWidth
        minWidth = minHeight
        minHeight = tmp
      }
      val targetRotate = rotate + exifRotate

      try {
        val outputStream = ByteArrayOutputStream()
        // 压缩图片
        formatHandler.handleFile(registrar.context(), filePath, outputStream, minWidth, minHeight, quality, targetRotate, keepExif, inSampleSize)
        reply(outputStream.toByteArray())
      } catch (e: Exception) {
        if (FlutterImageCompressPlugin.showLog) e.printStackTrace()
        reply(null)
      }
    }
  }
}

找到CommonHandler文件中的handleFile方法,其中bitmap.compress就是对图片进行压缩的方法

override fun handleFile(context: Context, path: String, outputStream: OutputStream, minWidth: Int, minHeight: Int, quality: Int, rotate: Int, keepExif: Boolean, inSampleSize: Int) {
    val options = BitmapFactory.Options()
    options.inJustDecodeBounds = false
    options.inPreferredConfig = Bitmap.Config.RGB_565
    options.inSampleSize = inSampleSize
    if (android.os.Build.VERSION.SDK_INT < android.os.Build.VERSION_CODES.M) {
      @Suppress("DEPRECATION")
      options.inDither = true
    }
    val bitmap = BitmapFactory.decodeFile(path, options)

    // 进行压缩
    val array = bitmap.compress(minWidth, minHeight, quality, rotate, type)

    if (keepExif && bitmapFormat == Bitmap.CompressFormat.JPEG) {
      val byteArrayOutputStream = ByteArrayOutputStream()
      byteArrayOutputStream.write(array)
      val tmpOutputStream = ExifKeeper(path).writeToOutputStream(
              context,
              byteArrayOutputStream
      )
      outputStream.write(tmpOutputStream.toByteArray())
    } else {
      outputStream.write(array)
    }
  }

在BitmapCompressExt.kt文件中定义了compress方法,根据传的最小高度和宽度确定出图片的最终大小,保证高度和宽度都不小于最小高度和宽度,然后根据计算后的高度和宽度,对位图进行压缩,一次既可。

fun Bitmap.compress(minWidth: Int, minHeight: Int, quality: Int, rotate: Int = 0, format: Int): ByteArray {
  val outputStream = ByteArrayOutputStream()
  compress(minWidth, minHeight, quality, rotate, outputStream, format)
  return outputStream.toByteArray()
}

fun Bitmap.compress(minWidth: Int, minHeight: Int, quality: Int, rotate: Int = 0, outputStream: OutputStream, format: Int = 0) {
  val w = this.width.toFloat()
  val h = this.height.toFloat()
  
  log("src width = $w")
  log("src height = $h")
  
  val scale = calcScale(minWidth, minHeight)
  
  log("scale = $scale")
  
  val destW = w / scale
  val destH = h / scale
  
  log("dst width = $destW")
  log("dst height = $destH")
  
  Bitmap.createScaledBitmap(this, destW.toInt(), destH.toInt(), true)
    .rotate(rotate)
    .compress(convertFormatIndexToFormat(format), quality, outputStream)
}

总结

整体来说,flutter_luban方法主要耗时在计算上面,而且全部是通过flutter来实现的,所以对于内存较低,CPU也低的机型来说,很耗时。但是对于flutter_image_compress来说,主要的计算方面都是在Android中实现的,并且开了单独的线程进行处理,计算量本来也不大,所以效率会高很多。