美文网首页
记录一次LuBan压缩OOM的解决方案

记录一次LuBan压缩OOM的解决方案

作者: BlueSocks | 来源:发表于2023-10-05 20:06 被阅读0次

前言

今天接到一个bug,说一个模块选择大图片会导致APP重启。一套操作下来,捡回来了另外一个bug,LuBan 压缩OOM了,讲道理,这两个bug是同一个问题导致的, emmmm? 我原意是甩锅来着,泪目。 我们还是先来看报错的类与代码:ByteArrayAdapter的new byte[length]报错了,emmm,一看这个玩意都要OOM。但是LuBan 通过native层的哈夫曼图片编码也太香了, 业务诉求上图片转base64 还真的不能放弃LuBan压缩,除非自己写哈夫曼编码,或者把它源码拉下来改。但是改bug嘛,什么最重要,改动小而且能改正确才最重要,而不是推翻重做。 OK,那么就开整。

正文

我们知道大文件会导致这句话OOM,那么我们就尽量避免大文件就行了。既然是图片,我们考虑的方向就存在两个了,一定是图片的宽高很大,要不就是质量特别好,所以我们需要做什么呢?我们需要一些基础信息来确定最终的解决方案。

获取图片的宽高

这个很简单,当我们获取到bitmap 的时候,就可以获取到图片的宽高,那么获取bitmap 会导致OOM吗?文件过大,还是会啊,这是优化方向,我们先解决业务诉求。

val options = BitmapFactory.Options()
// 设置为true,只解码图片的边界而不加载整个图片
options.inJustDecodeBounds = true 
// 解码图片的大小
BitmapFactory.decodeFile(it.availablePath, options)
// 获取图片的宽度和高度
// 获取图片的宽度和高度
val width = options.outWidth
val height = options.outHeight
val outMimeType = options.outMimeType
val outColorSpace = options.outColorSpace
val outConfig = options.outConfig

通过上面的代码,我们就可以获取到图片的宽高,所以我们还需要压缩图片,但通过bitmap 压缩图片的时候,是不是要获取到bitmap 对象啊。

百分百压缩图片

我们获取到图片后,压缩百分之50,然后存储到外置卡的APP数据目录下面。

val bitmap = BitmapFactory.decodeFile(it.availablePath)
// 准备一个输出流。
val filePath =PathUtils.getExternalAppFilesPath() + "/luoye/" + System.currentTimeMillis() + ".png"
 val file= File(filePath)
if (file.parentFile.exists().not()){
      file.parentFile.mkdirs()
}
if (!file.exists()){
      file.createNewFile()
}
val outputStream = FileOutputStream(filePath)
bitmap.compress(Bitmap.CompressFormat.PNG, 50, outputStream)
outputStream.close()

可以看到,这个玩意,图片还是很大。而且这种模式下,原图片会加载到内存中,这就导致内存暴涨,然后GC后又掉下去。

有损压缩

既然图片过大,那么我们能不能把它转化一下,比如说PNG支持透明度,我们转化成JPEG 这种不支持透明度的。

val bitmap = BitmapFactory.decodeFile(it.availablePath)
// 准备一个输出流。
val filePath = PathUtils.getExternalAppFilesPath() + "/luoye/" + System.currentTimeMillis() + ".jpg"
val file= File(filePath)
if (file.parentFile.exists().not()){
          file.parentFile.mkdirs()
}
if (!file.exists()){
         file.createNewFile()
}
val outputStream = FileOutputStream(filePath)
bitmap.compress(Bitmap.CompressFormat.JPEG, 50, outputStream)
outputStream.close()

可以看到,这个和上面的百分百压缩是一样的,就是 Bitmap.CompressFormat.JPEG 有些区别。 可以看到,上面几种写法,都需要将图片完整的读取到内存中来才行。

获取缩略图

在Android中,如果你需要获取一张大图片的缩略图,而不导致OOM(Out of Memory,内存溢出),你可以采用一种叫做"缩略图"的技术。缩略图是一种小尺寸的图像,它代表了一张大图像的主要内容。

以下是一种获取大图片缩略图的方法,而不会导致OOM:

  1. 使用BitmapFactory.Options设置inJustDecodeBoundstrue,这样在加载图片时就不会立即加载所有的像素数据,而只加载图片的边界信息。
  2. 根据图片的原始尺寸和所需的缩略图尺寸,设置inSampleSize。这个值是2的整数次幂,用于对原始图片进行缩放。例如,如果你需要的缩略图是原始尺寸的一半,那么inSampleSize应该设置为2。
  3. 使用BitmapFactory.decodeStream,传入InputStream和刚才设置好的Options对象,这样就能加载缩略图而不是原始图片。

以下是一段示例代码:

public static Bitmap createThumbnail(InputStream is, int width, int height) throws IOException {
    final BitmapFactory.Options options = new BitmapFactory.Options();
    options.inJustDecodeBounds = true;
    BitmapFactory.decodeStream(is, null, options);

    // Calculate inSampleSize
    options.inSampleSize = calculateInSampleSize(options, width, height);

    // Decode bitmap with inSampleSize set
    options.inJustDecodeBounds = false;
    return BitmapFactory.decodeStream(is, null, options);
}

public static int calculateInSampleSize(BitmapFactory.Options options, int reqWidth, int reqHeight) {
    // Raw height and width of image
    final int height = options.outHeight;
    final int width = options.outWidth;
    int inSampleSize = 1;

    if (height > reqHeight || width > reqWidth) {
        final int halfHeight = height / 2;
        final int halfWidth = width / 2;

        // Calculate the largest inSampleSize value that is a power of 2 and keeps both
        // height and width larger than the requested height and width.
        while ((halfHeight / inSampleSize) >= reqHeight && (halfWidth / inSampleSize) >= reqWidth) {
            inSampleSize *= 2;
        }
    }

    return inSampleSize;
}

请注意,这个方法会先读取整个输入流,然后再将其丢弃。如果你的输入流是从网络或其他资源获取的,那么这种方法可能会消耗大量的资源。为了避免这种情况,你可以先将输入流的数据读入一个字节数组,然后再从这个字节数组创建缩略图。

第三方maven

Compressor Android图片压缩。这个使用也非常简单,开启一个IO协程。 然后直接干就完事了。

 compressedImage = Compressor.compress(this@MainActivity, imageFile) {
          resolution(1280, 720)
          quality(80)
          format(Bitmap.CompressFormat.WEBP)
          size(2_097_152) // 2 MB
 }

因为这个项目全kotlin开发,大佬kotlin 写到过于优秀,看的有点打脑壳。所以不敢发表任何意见。但是最终项目中放弃了这种方式,而采用了缩略图方案+luban的方式将超大图压缩到300kb 一下。1是耗时比较久,2是压缩大图后界面重新渲染,没多少时间解决这个问题,就放弃了。

为什么LuBan是不可放弃的方案?

libjpeg-turbo是一个C语音编写的高效JPEG图像处理库,Android系统在7.0版本之前内部使用的是libjpeg非turbo版,并且为了性能关闭了Huffman编码。在7.0之后的系统内部使用了libjpeg-turbo库并且启用Huffman编码。

使用本分支可使7.0之前的系统也用上压缩速度更快,压缩效果更好的libjpeg-turbo库,但是需引入额外的so文件,会增加最终打包后app文件的大小。

所以核心的huffman编码。这个huffman编码是无损压缩算法,这就很牛逼了,这个可以很好的保证你丢进去图片的画质。当然这个肯定会影响解码时间。

而上面的一系列都是有损压缩,我们需要保证图片清晰度的同时,又要让图片小,所以需要结合luban 压缩算法,当然核心还是huffman 编码。所以我们将大图有损压缩到3到10MB 之后,采用luBan 压缩,这么就可以在一定程度上保证图片清晰度的同时,又保证了图片质量。

为什么不选择压缩为webp 格式?

是Google推出的网络图像格式,它结合了LZ77、哈夫曼编码和预测编码等技术实现高效的无损和有损压缩,压缩率号称比JPEG高40%。

核心问题就是,我们只是Android端,一张图片会整到各个端口,比如说ios,比如说老是js 框架的小程序网页啥的,那么转jpg是最兼容的方案。

那么应该怎么做?

  • 通过options 获取到图片的宽高。通过宽高的判断,去获取到一个获取缩略图所需要的inSampleSize
  • 通过设置新的options 获取到缩略图的bitmap
  • 将bitmap 转化为jpeg,并且存储到app 外置卡的缓存目录。
  • 通过luban压缩将缓存目录中的文件压缩一次,同时删除缓存文件。

剩下的就是将这种逻辑的代码实现出来就行了。当然大图压缩这只是一种思路,这是在没有更改luBan 代码的情况下实现的,我们丢弃了图片中的很多信息,首先我们将图片宽高缩放了,然后还丢弃了透明度,这个透明度可以不丢弃,看业务诉求。如果需要改luban的,就需要了解的知识就不再是这么片面了。

相关文章

网友评论

      本文标题:记录一次LuBan压缩OOM的解决方案

      本文链接:https://www.haomeiwen.com/subject/tozubdtx.html