Android 中图片压缩分析(上),android 绘制流程
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<SkWStream> strm(CreateJavaOutputStreamAdaptor(env, jstream, jstorage));if (!strm.get()) {return JNI_FALSE;}
std::unique_ptr<SkImageEncoder> 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
TRUE, you need not supply Huffman tables at all, and any you do
supply will be overwritten.
由上可知,如果设置 optimize_coding 为 TRUE,将会使得压缩图像过程中,会先基于图像数据计算哈弗曼表。由于这个计算会显著消耗空间和时间,默认值被设置为 FALSE。
那么 optimize_coding 参数的影响究竟会有多大呢?查阅一些博客资料介绍,使用相同的原始图片,分别设置 optimize_coding=TRUE 和 FALSE 进行压缩,发现 FALSE 时的图片大小大约是 TRUE 时的 5-10 倍。换言之就是相同文件体积的图片,不使用哈夫曼编码图片质量会比使用哈夫曼低 5-10 倍。
关于这个差异我们再去查阅其他资料,发现有两篇讨论非常热烈:Investigate using “optimize_coding” when encoding to JPEG,About libjpeg optimize_coding,甚至 Skia 的官方人员也参与了讨论,他据此测试了两组数据:
sample image 1 (RGB gradients):
default (80): 2.5x slower, 34% smaller
quality 0: 1.7x slower, 52% smaller
quality 20: 2.1x slower, 55% smaller
quality 40: 2.3x slower
, 37% smaller
quality 60: 2.5x slower, 36% smaller
quality 100: 3.9x slower, 22% smaller
sample image 2 (photo):
default (80): 2x slower, 8% smaller
quality 0: 1.5x slower, 49% smaller
quality 20: 1.7x slower, 22% smaller
评论