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就在其中,就可以跟踪对应的代码进行分析了。
继续跟踪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中实现的,并且开了单独的线程进行处理,计算量本来也不大,所以效率会高很多。