Android包大小优化之无Alpha通道PNG转JPG的探索

2019-03-16  本文已影响0人  CyanStone

apk的大小与推广成本、转化率有着密不可分的关系,所以对包大小的优化,应做到谓锱铢必较,特别像抖音这样上亿DAU的应用,追求到极极极极极致都不为过。除了常见的APK瘦身方式,还有哪些方式呢?本文是对其中一个想法的探索过程。

1.问题引入

2.Java相关的 API

Java已经提供了很多API,如BufferedImage、ColorModel、IIOImage、ImageIO、ImageWriter、JPEGImageWriteParam,来帮助进行图像处理。

2.1 BufferedImage

BufferedImage bufferedImage = ImageIO.read(new FileInputStream(filePath));
BufferedImage image = ImageIO.read(new FileInputStream(file)); //获取位图
image.getHeight();//图像的高
image.getWidth();//图像的宽
//获取图像某一像素的值,返回的int型数据(32位)为ARGB格式,其中ARGB各占8bit
int pixel = image.getRGB(x,y);
//返回图像的类型,如TYPE_INT_RGB、TYPE_INT_ARGB,如果是未知的类型,会返回TYPE_CUSTOM
int type = image.getType(); 

2.2 ColorModel

BufferedImage sourceImg = ImageIO.read(new FileInputStream(file));
//通过BufferedImage获得其ColorModel
ColorModel color = sourceImg.getColorModel();
//获得每像素的大小,也即图片的位深度
color.getPixelSize();
//返回一个32位像素值的透明通道分量的值,同理,可获得像素值其他分量的值
color.getAlpha(int pixel);

2.3 ImageIO

//getImageWritersByFormatName方法返回是所有能够对指定格式进行编码的ImageWriter的迭代器(Iterator<ImageWriter>),此行代码获取了一个能够对jpg格式编码的ImageWriter
ImageWriter jpgWriter = ImageIO.getImageWritersByFormatName("jpg").next();
//将一个BufferedImage对象以jpg形式写入jpgFile中,用它可以进行简单的图像格式转换
ImageIO.write(newBufferedImage,"jpg",jpgFile);

2.4 IIOImage

IIOImage(RenderedImaeg image, List<? extends BufferedImage> thumbnails, IIOMetadata metadata);
//如下,就得到了一个与Buffered所关联的IIOImage对象
IIOImage iioImage = new IIOImage(bufferedImage,null,null);

2.5 JPEGImageWriteParam

//初始化,参数Local代表图像的地理、政治、文化等信息,可为空
JPEGImageWriteParam jpegParams = new JPEGImageWriteParam(null);
//如果支持压缩,必须设置压缩模式,MODE_EXPLICIT模式表示会使用此mageWriteParam中指定的压缩类型和质量设置进行压缩。所有之前设置的compression参数都将被丢弃。
jpegParams.setCompressionMode(ImageWriteParam.MODE_EXPLICIT);
//设置压缩质量,取值范围0.0f~1.0f,1.0f代表质量最好;默认0.75f,表示视觉无损
jpegParams.setCompressionQuality(1.0f);

2.6 ImageWriter

//设置输出路径,这里虽然传入的是Object对象,但是一般应该传入以下两种对象:
// 1. FileImageOutputStream,用于写入文件
// 2. MemoryCacheImageOutputStream,用于写入内存中
void setOutput(Object output);
//把IIOImage对象关联的对象直接作为输入,写入到输出对象
void write(IIOImage image);
//同上,写入的时候加上元数据、写入参数,我们就应该调用它来完成格式转换
void write(IIOMetadata metadata, IIOImage image, ImageWriteParam param);
//同上,只是输入的对象是RenderedImage的实现类对象
void write(RenderedImage image);

3.探索过程

我们可以hook Android编译过程,拿到所有的资源文件。由于本文是探索的过程,还未集成到项目里,所以探索的demo是拿的apk包反编译出来的。而且打包过程中已经禁止了AAPT采用内置的压缩算法对图片资源的优化,所以反编译出来的图片资源跟打包前应该是一致的:

aaptOptions {
    cruncherEnabled = false
}

3.1 获取需要处理的PNG图片

3.1.1 根据位深

它只适用于不经过压缩处理的图片,如经过像tinypng、pngguant压缩过的,位深会被压缩,这点千万要注意!如果经过tinypng或者pngquant算法压缩后,是可能出现虽然包含透明通道,但是位深(每像素大小)是4、8、16、24甚至是1的,具体原理涉及到压缩算法,这里不进行深究。

if(file.getName().endsWith(".png") 
    && !file.getName().contains(".9.png")
    && getPngBitDepth(file) != 32) {
        //do convert
    }

private static int getPngBitDepth(File file) throws IOException {
    BufferedImage sourceImg = ImageIO.read(new FileInputStream(file));
    ColorModel color = sourceImg.getColorModel();
    return color.getPixelSize();
}

3.1.2 根据是否包含alpha通道

if(file.getName().endsWith(".png") 
    && !file.getName().contains(".9.png")
    && !constainsAlphaChannel(file) {
        //do convert
    }

private static boolean constainsAlphaChannel(File file)  throws IOException{
    BufferedImage sourceImg = ImageIO.read(new FileInputStream(file));
    ColorModel color = sourceImg.getColorModel();
    return color.hasAlpha();
}

3.1.3 根据是否包含透明度像素

if(file.getName().endsWith(".png") 
    && !file.getName().contains(".9.png")
    && !constainsAlphaChannel(file) {
        //do convert
    }
private static boolean containsTransparency(File file) throws FileNotFoundException, IOException{
    BufferedImage image = ImageIO.read(new FileInputStream(file));
    for (int i = 0; i < image.getHeight(); i++) {
        for (int j = 0; j < image.getWidth(); j++) {
            if (isTransparent(image, j, i)){
                return true;
                }
            }
        }
        return false;
    }

public static boolean isTransparent(BufferedImage image, int x, int y ) {
    int pixel = image.getRGB(x,y);
    return (pixel>>24) == 0x00; //透明通道在高8位,根据其是否为0判断是否包含透明通道
}

3.2 图像转换

本节进行转换的是debug版本的APK反编译出来的目录,release版本的会在下一节阐述。

3.2.1 ImageIO进行转换

private static void convertPNG2JPG(File pngFile, File jpgFile) throws IOException {
    BufferedImage bufferedImage = ImageIO.read(pngFile);
    BufferedImage newBufferedImage = new BufferedImage(bufferedImage.getWidth(),bufferedImage.getHeight(),BufferedImage.TYPE_INT_RGB);
    //创建BufferedImage,并绘制白色的背景
    newBufferedImage.createGraphics().drawImage(bufferedImage, 0, 0, Color.WHITE, null);
    ImageIO.write(newBufferedImage,"jpg",jpgFile);
}

通过上述介绍过API以后,这段代码不难理解了,就是通过ImageIO把创建的BufferedImage以jpg形式写回文件。
通过这次转换以后,输出文件大小对比:

png total size:4226.41KB
jpg total size:1103.19KB

可以看到,大小减少了很多,但是看看成像质量,发现画质损失的有点严重啊:


画质对比

左边是png原图,右边是jpg,放大后可以看到边缘损失很大。

3.2.2 ImageWriter.Write进行转换

private static void convertPNG2JPG_2(File pngFile, File jpgFile) throws FileNotFoundException, IOException {
    BufferedImage bufferedImage = ImageIO.read(pngFile);
    JPEGImageWriteParam jpegParams = new JPEGImageWriteParam(null);
    jpegParams.setCompressionMode(ImageWriteParam.MODE_EXPLICIT);
    //这里设置压缩质量
    jpegParams.setCompressionQuality(1.0f);
    ImageWriter jpgWriter = ImageIO.getImageWritersByFormatName("jpg").next();
    jpgWriter.setOutput(new FileImageOutputStream(jpgFile));
    IIOImage iioImage = new IIOImage(bufferedImage,null,null);
    jpgWriter.write(null, iioImage,jpegParams);
    jpgWriter.dispose();
}

注意,使用上述API,不要采用JDK11,否则会报出以下错误,不好排查,改用JDK1.8后可用。

调用上述API后发生了Native层的奔溃
png total size:4226.41KB
jpg total size:5012.55KB

可以看到大小不减少反而增大了,进一步验证了,并不是所有的情况,png转换为jpg,图像大小都会变小,详细说明可看参考链接。
看看成像质量:


压缩参数是1.0f时的成像对比

左边是png,右边是转换后的jpg,虽然画面的通透性感觉有点改变,画质还是可以的,可是大小却变大了

png total size:4226.41KB
jpg total size:1479.10KB

可以看到,文件大小从原来png的4226KB变成了1479KB,来看看成像质量:


压缩参数是0.75f时的成像对比

左边是png原图,右边是转换成jpg后的图像。同样可以发现存在肉眼可见的画质损失。

png total size:4226.41KB
jpg total size:2036.71KB

画质对比:


压缩参数是0.9f时的成像对比

可以看到图像的质量还是可以的,没有明显的糊边了。

4.进一步探索

上一节的探索都是在debug版本的APK反编译进行的,由于抖音的图片资源在打release包的时候会经过McImage的优化,期间会用pngguant算法进行压缩,思考,如果此时我将压缩后的png的图片进行上述转换,会发生什么情况呢?

4.1 release版本探索

压缩质量设置为0.9,通过上述程序转换,输出文件大小:

png total size:415.09KB
jpg total size:595.10KB

首先看到的是,能检测出来不包含alpha像素的png图片的数量少了很多,猜测这个可能是用pngquant压缩后与Java API的检测有关,具体源码不深究了。
我们来看此时图像的成像质量,扫描出的不包含alpha像素的png图片:


png目录

而发现jpg中有好多转换失败的黑图:


转换后的jpg目录
不用挑样张来对比成像质量了,这是绝对不允许的,所以通过算法压缩后的图像,转换后,不仅体积变大,而且还有很多转换失败的。

所以,上述的转换,一定要是针对未通过其他算法进行压缩后的图像资源。

4.2 转换成jpg后,还可以通过tinypng压缩吗?

依然回到debug版本的资源上,进一步探索,看转换后的图像,是否可以通过tinypng压缩。

step 1 扫出需要转换的png原图 -> 4226KB
step 2 上述png转成jpg -> 2036KB
setp 3 上述的jpg经过tinypng压缩 -> 1099KB

压缩后,发现有些图像也被损坏,很多图片出现了奇怪的背景颜色:


经过tinypng压缩后,有的图像出现了损坏

虽然图片大小体积进一步变小,但是图像出现了损坏,这种情况也是不可取的。

4.3png直接用tinypng压缩

5. 结论与思考

广告时间

字节跳动各Android客户端团队招人火爆进行中,各个级别和应届实习生都需要,业务增长快、日活高、挑战大、待遇给力,各位大佬走过路过千万不要错过!

本科以上学历、对技术有热情,欢迎加我的微信详聊:spq951992006

欢迎来扫

参考链接

Comparison of different image compression formats

上一篇 下一篇

猜你喜欢

热点阅读