Android 中图片压缩分析(上)

在上述代码中,我们选择的压缩格式是CompressFormat.JPEG,除此之外还有两个选择:

其一,CompressFormat.PNG, PNG 格式是无损的,它无法再进行质量压缩,quality 这个参数就没有作用了,会被忽略,所以最后图片保存成的文件大小不会有变化;
其二,CompressFormat.WEBP ,这个格式是 google 推出的图片格式,它会比 JPEG 更加省空间,经过实测大概可以优化 30% 左右。

由于项目原因和兼容性选择了JPEG,因此接下来的分析也将是围绕 JPEG 展开。

将 PNG 图片转成 JPEG 格式之后不会降低这个图片的尺寸,但是会降低视觉质量,从而降低存储体积。同时,由于尺寸不变,所以将这个图片解码成相同色彩模式的 bitmap 之后,占用的内存大小和压缩前是一样的。

回到最初的代码示例,函数 compress 经过一连串的 java 层调用之后,最后来到了一个 native 函数,如下:

//Bitmap.cpp
static jboolean Bitmap_compress(JNIEnv* env, jobject clazz, jlong bitmapHandle,
jint format, jint quality,
jobject jstream, jbyteArray jstorage) {

LocalScopedBitmap bitmap(bitmapHandle);
SkImageEncoder::Type fm;

switch (format) {
case kJPEG_JavaEncodeFormat:
fm = SkImageEncoder::kJPEG_Type;
break;
case kPNG_JavaEncodeFormat:
fm = SkImageEncoder::kPNG_Type;
break;
case kWEBP_JavaEncodeFormat:
fm = SkImageEncoder::kWEBP_Type;
break;
default:
return JNI_FALSE;
}

if (!bitmap.valid()) {
return JNI_FALSE;
}

bool success = false;

std::unique_ptr strm(CreateJavaOutputStreamAdaptor(env, jstream, jstorage));
if (!strm.get()) {
return JNI_FALSE;
}

std::unique_ptr encoder(SkImageEncoder::Create(fm));
if (encoder.get()) {
SkBitmap skbitmap;
bitmap->getSkBitmap(&skbitmap);
success = encoder->encodeStream(strm.get(), skbitmap, quality);
}
return success ? JNI_TRUE : JNI_FALSE;
}

可以看到最后调用了函数 encoder->encodeStream(…) 编码保存本地。该函数是调用 skia 引擎来对图片进行编码压缩,对 skia 的介绍将在后文展开。

一段完整的示例代码如下:

// R.drawable.thumb 为 png 图片
Bitmap bitmap = BitmapFactory.decodeResource(getResources(), R.drawable.thumb);
try {
//保存压缩图片到本地
File file = new File(Environment.getExternalStorageDirectory(), “aaa.jpg”);
if (!file.exists()) {
file.createNewFile();
}
FileOutputStream fs = new FileOutputStream(file);
bitmap.compress(Bitmap.CompressFormat.JPEG, 50, fs);
Log.i(TAG, "onCreate: file.length " + file.length());
fs.flush();
fs.close();
} catch (FileNotFoundException e) {
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
}
//查看压缩之后的 Bitmap 大小
ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
bitmap.compress(Bitmap.CompressFormat.JPEG, 50, outputStream);
byte[] bytes = outputStream.toByteArray();
Bitmap compress = BitmapFactory.decodeByteArray(bytes, 0, bytes.length);
Log.i(TAG, "onCreate: bitmap.size = " + bitmap.getByteCount() + " compress.size = " + compress.getByteCount());

首先,我们来看看 quality 参数被设置为 50,质量压缩前后的图片对比,可以看到其尺寸大小并没有变化,但是视觉感受也可以明显地看到图片变的模糊了一些。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

通过日志也可以看到,在质量压缩前后图片转成 Bitmap 之后在内存中的大小也并没有变化,这是在保持像素的前提下,改变图片的位深及透明度等:

//压缩之后图片占用的存储体积
compress.length = 7814
//在内存中压缩前后图片占用的大小
bitmap.size = 350000 compress.size = 350000

对比二者,保存前的图片存储体积是 106k,质量设为 50 并且保存为 JPEG 格式之后,图片存储大小就只有 8k 了,并且质量设的越低,保存成文件之后,文件的体积也就越小。

三、Android Skia 图像引擎

在上文中,提到的Skia是Android 的重要组成部分。

Skia 是一个 Google 自己维护的 c++ 实现的图像引擎,实现了各种图像处理功能,并且广泛地应用于谷歌自己和其它公司的产品中(如:Chrome、Firefox、 Android等),基于它可以很方便为操作系统、浏览器等开发图像处理功能。

Skia 在 Android 中提供了基本的画图和简单的编解码功能,可以挂接其他的第三方编码解码库或者硬件编解码库,例如 libpng 和 libjpeg,libgif 等等。因此,这个函数调用bitmap.compress(Bitmap.CompressFormat.JPEG…),实际会调用 libjpeg.so 动态库进行编码压缩。

最终 Android 编码保存图片的逻辑是 Java 层函数→Native 函数→Skia函数→对应第三库函数(例如 libjpeg)。所以 skia 就像一个胶水层,用来链接各种第三方编解码库,不过 Android 也会对这些库做一些修改,比如修改内存管理的方式等等。

Android 在之前从某种程度来说使用的算是 libjpeg 的功能阉割版,压缩图片默认使用的是 standard huffman,而不是 optimized huffman,也就是说使用的是默认的哈夫曼表,并没有根据实际图片去计算相对应的哈夫曼表,Google 在初期考虑到手机的性能瓶颈,计算图片权重这个阶段非常占用 CPU 资源的同时也非常耗时,因为此时需要计算图片所有像素 argb 的权重,这也是 Android 的图片压缩率对比 iOS 来说差了一些的原因之一。

四、图像压缩与 Huffman 算法

这里简单介绍一下哈夫曼算法,哈夫曼算法是在多媒体处理里常用的算法之一。比如一个文件中可能会出现五个值 a,b,c,d,e,它们用二进制表达是:

a. 1010
b. 1011
c. 1100
d. 1101
e. 1110

我们可以看到,最前面的一位数字是 1,其实是浪费掉了,在定长算法下最优的表达式为:

a. 010
b. 011
c. 100
d. 101
e. 110

这样我们就能做到节省一位的损耗,那哈夫曼算法比起定长算法改进的地方在哪里呢?在哈夫曼算法中我们可以给信息赋予权重,即为信息加权,假设 a 占据了 60%,b 占据了 20%, c 占据了 20%,d,e 都是 0%:

a:010 (60%)
b:011 (20%)
c:100 (20%)
d:101 (0%)
e:110 (0%)

在这种情况下,我们可以使用哈夫曼树算法再次优化为:

a:1
b:01
c:00

所以思路当然就是出现频率高的字母使用短码,对出现频率低的使用长码,不出现的直接就去掉,最后 abcde 的哈夫曼编码就对应:1 01 00
通过权重对应生成的的哈夫曼表为:

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

定长编码下的abcde:010 011 100 101 110,使用哈夫曼树加权后的编码则为 1 01 00,这就是哈夫曼算法的整体思路(关于算法的详细介绍可以去查阅相关资料)。

所以这个算法一个很重要的思路是必须知道每一个元素出现的权重,如果我们能够知道每一个元素的权重,那么就能够根据权重动态生成一个最优的哈夫曼表。

但是怎么去获取每一个元素,对于图片就是每一个像素中 argb 的权重呢,只能去循环整个图片的像素信息,这无疑是非常消耗性能的,所以早期 android 就使用了默认的哈夫曼表进行图片压缩。

五、libjpeg 与 optimize_coding

libjpeg 在压缩图像时,有一个参数叫 optimize_coding,关于这个参数,libjpeg.doc 有如下解释:

TRUE causes the compressor to compute optimal Huffman coding tables
for the image. This requires an extra pass over the data and
therefore costs a good deal of space and time. The default is
FALSE, which tells the compressor to use the supplied or default
Huffman tables. In most cases optimal tables save only a few percent
of file size compared to the default tables. Note that when this is
自我介绍一下,小编13年上海交大毕业,曾经在小公司待过,也去过华为、OPPO等大厂,18年进入阿里一直到现在。

深知大多数初中级Android工程师,想要提升技能,往往是自己摸索成长或者是报班学习,但对于培训机构动则近万的学费,着实压力不小。自己不成体系的自学效果低效又漫长,而且极易碰到天花板技术停滞不前!

因此收集整理了一份《2024年Android移动开发全套学习资料》,初衷也很简单,就是希望能够帮助到想自学提升又不知道该从何学起的朋友,同时减轻大家的负担。

img

img

img

img

既有适合小白学习的零基础资料,也有适合3年以上经验的小伙伴深入学习提升的进阶课程,基本涵盖了95%以上Android开发知识点,真正体系化!

由于文件比较大,这里只是将部分目录截图出来,每个节点里面都包含大厂面经、学习笔记、源码讲义、实战项目、讲解视频,并且会持续更新!

如果你觉得这些内容对你有帮助,可以扫码获取!!(备注:Android)

面试复习笔记

这份资料我从春招开始,就会将各博客、论坛。网站上等优质的Android开发中高级面试题收集起来,然后全网寻找最优的解答方案。每一道面试题都是百分百的大厂面经真题+最优解答。包知识脉络 + 诸多细节。
节省大家在网上搜索资料的时间来学习,也可以分享给身边好友一起学习。

《960页Android开发笔记》

《1307页Android开发面试宝典》

包含了腾讯、百度、小米、阿里、乐视、美团、58、猎豹、360、新浪、搜狐等一线互联网公司面试被问到的题目。熟悉本文中列出的知识点会大大增加通过前两轮技术面试的几率。

《507页Android开发相关源码解析》

只要是程序员,不管是Java还是Android,如果不去阅读源码,只看API文档,那就只是停留于皮毛,这对我们知识体系的建立和完备以及实战技术的提升都是不利的。

真正最能锻炼能力的便是直接去阅读源码,不仅限于阅读各大系统源码,还包括各种优秀的开源库。

《Android学习笔记总结+移动架构视频+大厂面试真题+项目实战源码》,点击传送门即可获取!

我们知识体系的建立和完备以及实战技术的提升都是不利的。

真正最能锻炼能力的便是直接去阅读源码,不仅限于阅读各大系统源码,还包括各种优秀的开源库。

[外链图片转存中…(img-gl0R6z73-1711765338445)]

《Android学习笔记总结+移动架构视频+大厂面试真题+项目实战源码》,点击传送门即可获取!
  • 7
    点赞
  • 13
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值