盒子
盒子
文章目录
  1. 前言
  2. JPEG文件结构
  3. ExifOutputStream
  4. 参考文献

Android性能优化:图片保存,还能更快

前言

本文主要介绍带Exif信息的JPEG图片的保存优化。原来在这种情况下,整个过程分为两步:

  • 首先调用Bitmap的compress()保存图片数据。
  • 然后通过ExifInterface类设置需要保存的Exif信息。

是否能通过一步直接完成Exif信息和Bitmap数据的写入呢?答案是能。

本文首先介绍了JPEG文件结构,通过对该结构的剖析,可以看出一次写入Exif信息和Bitmap数据是完全可能的。其次介绍了ExifInterface的使用方法。最后介绍了JPEG保存优化的核心类ExifOutputStream。

JPEG文件结构

基本概念:

  • JPEG(Joint Photographic Experts Group,一种针对图像的有损压缩方法):描述一个图像如何转换为字节流。
  • JFIF(JPEG File Interchange Format,JPEG文件交换格式):描述一个JPEG字节流如何转换为电脑上存储的文件。
  • EXIF(Exchangeable image file format,可交换图像文件格式):可以记录照片的属性信息和拍摄信息。Exif信息是附加在JPEG文件头中的,以0xFFE1为开头,后面两个字节表示Exif信息的长度。

为了更加简单的说明JPEG文件的结构,我们拿带有EXIF信息的最简单的图片作为例子,一共8张图,如下图所示:

第一张图是正常的F,后面7张都是对F进行了翻转或旋转,这些图片只有Exif中的Orientation值不一样,其余数据完全一样,因此这里我们就拿第八张图分析,我们通过iHex应用查看了该图片的16进制串,如下:

FFD8FFE0 00104A46 49460001 010100DC 00DC0000 FFE10022 45786966 00004949 2A000800 00000100 12010300 01000000 08000000 00000000 FFDB0043 00060504 04050606 0506060A 08060707 0A09090A 0A101417 100C0F0E 141D1A16 16141718 18161C23 1B1A1F25 2623202C 20161929 2A29272D 302D1F25 30282928 28FFC000 0B080030 00500101 1100FFC4 001F0000 01050101 01010101 00000000 00000000 01020304 05060708 090A0BFF C400B510 00020103 03020403 05050404 0000017D 01020300 04110512 21314106 13516107 22711432 8191A108 2342B1C1 1552D1F0 24336272 82090A16 1718191A 25262728 292A3435 36373839 3A434445 46474849 4A535455 56575859 5A636465 66676869 6A737475 76777879 7A838485 86878889 8A929394 95969798 999AA2A3 A4A5A6A7 A8A9AAB2 B3B4B5B6 B7B8B9BA C2C3C4C5 C6C7C8C9 CAD2D3D4 D5D6D7D8 D9DAE1E2 E3E4E5E6 E7E8E9EA F1F2F3F4 F5F6F7F8 F9FAFFDA 00080101 00003F00 F9528A28 A28A28A2 8A28A28A 28A28A28 AFD54A28 A2BF2AE8 AFD54A2B F2AE8AFD 54A28A2B F2AE8AFD 54A2BF2A E8AFD54A 28A28A28 A2BF2AE8 AFD54A28 A28A28A2 BF2AE8AF FFD9

是不是一头雾水?首先我们给出几个概念:

  • Exif信息以0xFFE1开头,后面的2个字节是Exif信息的长度,该长度为这2个字节和后面Exif信息的长度和。
  • Exif每个Tag的结构为AAAA BBBB CCCCCCCC DDDDDDDD,其中AAAA为Tag Name,DDDDDDDD为Tag Value,BBBB为Tag Value的数据类型,CCCCCCCC为Tag Value的个数。

这里将该串进行了拆解,并一一进行分析:

十六进制串 解释
FFD8 SOI(Start Of Image),JPEG的文件头。
FFE0 数码相机信息的标记头,即以下信息是数码配置信息。
0010 长度信息+真实数据的总长度为16字节,即真实数据的长度为14字节。
4A46 49460001 010100DC 00DC0000 数码相机配置信息。
FFE1 Exif 标记,表示Exif信息的开始。
0022 值为34,即Exif的数据+长度信息的长度为34字节,表示下面的Exif数据长度为32字节。
45786966 0000 Exif的ASCII字符串。
4949 Exif的数据是按大端(motorola byte align)还是小端(Intel byte align),4949表示小端,4d4d表示大端。
2A00 值为002A,常量。因为是小端,所以是2A00。
0800 0000 值为00000008,常量,表示第一个exif tag距离4949的偏移量。
0100 值为0001,表示后面有一个tag。
1201 值为0112,表示tag name为orientation。
0300 值为0003,表示类型为unsigned short,即orientation的值占2个字节。
01000000 orientation对应的值的个数,这里是1个。
08000000 00000000 值为00000000 00000008,具体的含义在下文中解释。
图片数据。
FFD9 EOI(End Of Image),JPEG的文件尾。

其中,需要进一步解释的是ORIENTATION,值可以从1-8,每个值对应的是图片的不同偏转,如下图所示:

在Android相机拍照中,一般只需要处理其中4种情况,分别是:

  • 1:正常角度,对应ExifInterface.ORIENTATION_NORMAL。
  • 3:偏转180度,需要偏转180度去纠正,对应ExifInterface.ORIENTATION_ROTATE_180。
  • 6:偏转270度,需要偏转90度去纠正,对应ExifInterface.ORIENTATION_ROTATE_90。
  • 8:偏转90度,需要偏转270度去纠正,对应ExifInterface.ORIENTATION_ROTATE_270。

如下图所示:

在Android Studio中能够看到没纠正过角度的图片,Mac的预览图片看到的是自动纠正过角度的图片。

一般来说,我们可以通过switch语句对每个orientation的值进行不同的纠正,但是 JPEG Orientation 给出了一个通用的纠正算法,思想为:

value = value - 1;
if (value & 100b != 0) 图片对角线翻转
if (value & 010b != 0) 图片旋转180度
if (value & 001b != 0) 图片水平翻转

具体实现如下:

public static Bitmap fixOrientation(String file) {
Matrix matrix = new Matrix();
Bitmap bitmap = BitmapFactory.decodeFile(file);
try {
ExifInterface exif = new ExifInterface(file);
int value = exif.getAttributeInt(ExifInterface.TAG_ORIENTATION, ExifInterface.ORIENTATION_UNDEFINED);
if (value > 0) {
value --;
if ((value & 0b100) != 0) { //对角线翻转
matrix.postScale(-1.0f, 1.0f);
matrix.postRotate(-90);
}
if ((value & 0b010) != 0) { //旋转180度
matrix.postRotate(180);
}
if ((value & 0b001) != 0) { //水平翻转
matrix.postScale(-1.0f, 1.0f);
}
return Bitmap.createBitmap(bitmap, 0, 0, bitmap.getWidth(), bitmap.getHeight(), matrix, true);
}
} catch (IOException e) {
e.printStackTrace();
}
return bitmap;
}

这里用到了ExifInterface类,该类是系统提供的操作Exif信息的核心类,使用方法是:

ExifInterface exif = new ExifInterface(PATH);
int orientation = exif.getAttributeInt(ExifInterface.TAG_ORIENTATION, ExifInterface.ORIENTATION_NORMAL);
exif.setAttribute(ExifInterface.TAG_ORIENTATION, "1”); //设置ORIENTATION
exif.saveAttributes(); //保存Exif信息

这里需要注意一点:saveAttributes()这个方法很耗时,需要将原文件的数据拷贝到另一个文件,并删除原文件,然后重命名新文件,因此建议在调用该方法之前把该设置的Exif信息全通过setAttribute()设置完,使得saveAttributes()只调用一次。

Android 7.1中新增了ExifInterface Support Library,但是很奇怪,我使用该类获取orientation,始终获取不到,而用android.media.ExifInterface类却没问题。

ExifOutputStream

ExifOutputStream是系统内部不public的类,位于”com.android.gallery3d.exif”包中,它提供了一次写入Exif信息和JPEG数据的方式。我们可以通过writeExif()实现一次IO写入JPEG和Exif信息。

public static void writeExif(byte[] jpeg, ExifData exif, String path) {
OutputStream os = null;
try {
os = new FileOutputStream(path);
ExifOutputStream eos = new ExifOutputStream(os);
// Set the exif header
eos.setExifData(exif);
// Write the original jpeg out, the header will be add into the file.
eos.write(jpeg);
} catch (FileNotFoundException e) {
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
} finally {
if (os != null) {
try {
os.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
}

根据统计,相比原来的实现,速度提升了10%。

参考文献

支持一下
扫一扫,支持xiazdong