Android包大小優(yōu)化之無(wú)Alpha通道PNG轉(zhuǎn)JPG的探索

apk的大小與推廣成本、轉(zhuǎn)化率有著密不可分的關(guān)系,所以對(duì)包大小的優(yōu)化,應(yīng)做到謂錙銖必較,特別像抖音這樣上億DAU的應(yīng)用,追求到極極極極極致都不為過(guò)。除了常見(jiàn)的APK瘦身方式,還有哪些方式呢?本文是對(duì)其中一個(gè)想法的探索過(guò)程。

1.問(wèn)題引入

  • PNG圖片支持alpha通道,JPG不支持alpha通道,所以PNG圖片的位深可能會(huì)比JEG的位深大;
  • PNG格式采用的是無(wú)損數(shù)據(jù)壓縮算法,JPG采用的壓縮比更好的有損數(shù)據(jù)壓縮算法,在有鮮艷明亮的色彩和紋理的圖像中,JPG通常比PNG具有更高的壓縮比;
  • 綜上猜測(cè),如果把無(wú)alpha通道的PNG(甚至是有alpha通道但是無(wú)透明度的PNG)轉(zhuǎn)變?yōu)镴PG格式,是否可以使得圖片的體積變小,于是有了本文的探索過(guò)程。

2.Java相關(guān)的 API

Java已經(jīng)提供了很多API,如BufferedImage、ColorModel、IIOImage、ImageIO、ImageWriter、JPEGImageWriteParam,來(lái)幫助進(jìn)行圖像處理。

2.1 BufferedImage

  • BufferedImage是Image的一個(gè)子類,Image和BufferedImage的主要作用就是將一副圖片加載到內(nèi)存中。BufferedImage生成的圖片在內(nèi)存里有一個(gè)圖像緩沖區(qū),利用這個(gè)緩沖區(qū)我們可以很方便的操作這個(gè)圖片,通常用來(lái)做圖片修改操作如大小變換、圖片變灰、設(shè)置圖片透明或不透明等。
  • Java將一副圖片加載到內(nèi)存中的方法是:
BufferedImage bufferedImage = ImageIO.read(new FileInputStream(filePath));
  • 通過(guò)BufferedImage得到內(nèi)存中一張圖片到數(shù)據(jù)實(shí)體后,便可以通過(guò)它獲得圖片基本信息,如長(zhǎng)、寬、每個(gè)像素的值等
BufferedImage image = ImageIO.read(new FileInputStream(file)); //獲取位圖
image.getHeight();//圖像的高
image.getWidth();//圖像的寬
//獲取圖像某一像素的值,返回的int型數(shù)據(jù)(32位)為ARGB格式,其中ARGB各占8bit
int pixel = image.getRGB(x,y);
//返回圖像的類型,如TYPE_INT_RGB、TYPE_INT_ARGB,如果是未知的類型,會(huì)返回TYPE_CUSTOM
int type = image.getType(); 

2.2 ColorModel

  • ColorModel抽象類封裝了一系列把像素值轉(zhuǎn)換為色彩分量(R、G、B)和透明度分量(alpha)的方法。
BufferedImage sourceImg = ImageIO.read(new FileInputStream(file));
//通過(guò)BufferedImage獲得其ColorModel
ColorModel color = sourceImg.getColorModel();
//獲得每像素的大小,也即圖片的位深度
color.getPixelSize();
//返回一個(gè)32位像素值的透明通道分量的值,同理,可獲得像素值其他分量的值
color.getAlpha(int pixel);

2.3 ImageIO

  • ImageIO是一個(gè)輔助類,提供了一系列的靜態(tài)方法,可以來(lái)獲取已經(jīng)注冊(cè)了的 ImageReader和 ImageWriter的對(duì)對(duì)象,以及執(zhí)行簡(jiǎn)單編碼和解碼。
//getImageWritersByFormatName方法返回是所有能夠?qū)χ付ǜ袷竭M(jìn)行編碼的ImageWriter的迭代器(Iterator<ImageWriter>),此行代碼獲取了一個(gè)能夠?qū)pg格式編碼的ImageWriter
ImageWriter jpgWriter = ImageIO.getImageWritersByFormatName("jpg").next();
//將一個(gè)BufferedImage對(duì)象以jpg形式寫入jpgFile中,用它可以進(jìn)行簡(jiǎn)單的圖像格式轉(zhuǎn)換
ImageIO.write(newBufferedImage,"jpg",jpgFile);

2.4 IIOImage

  • IIOImage是一個(gè)簡(jiǎn)單的容器類,它聚合了一張圖像的圖像數(shù)據(jù)(RenderedImage)、一系列的縮略圖以及與圖像關(guān)聯(lián)的其他元數(shù)據(jù)(IIOMetadata,非圖像信息)。
  • 構(gòu)造方法:創(chuàng)建的時(shí)候,需要把相關(guān)的參數(shù)進(jìn)行注入:
    • RenderedImage image:代表圖像的圖像信息,RenderedImage是個(gè)接口,需要傳入其實(shí)現(xiàn)類。BufferedImage實(shí)現(xiàn)了RenderedImage接口,其對(duì)象可作為參數(shù)傳入。
    • List<? extends BufferedImage> thumbnails:圖像的縮略圖信息,可為null
    • IIOMetadata metadata:與圖像相關(guān)聯(lián)的其他非圖像數(shù)據(jù)的元數(shù)據(jù),可為null
IIOImage(RenderedImaeg image, List<? extends BufferedImage> thumbnails, IIOMetadata metadata);
//如下,就得到了一個(gè)與Buffered所關(guān)聯(lián)的IIOImage對(duì)象
IIOImage iioImage = new IIOImage(bufferedImage,null,null);

2.5 JPEGImageWriteParam

  • JPEGImageWriteParam是圖像寫入文件時(shí)的一個(gè)參數(shù)類,可以通過(guò)它設(shè)置圖像的壓縮質(zhì)量等參數(shù)。
//初始化,參數(shù)Local代表圖像的地理、政治、文化等信息,可為空
JPEGImageWriteParam jpegParams = new JPEGImageWriteParam(null);
//如果支持壓縮,必須設(shè)置壓縮模式,MODE_EXPLICIT模式表示會(huì)使用此mageWriteParam中指定的壓縮類型和質(zhì)量設(shè)置進(jìn)行壓縮。所有之前設(shè)置的compression參數(shù)都將被丟棄。
jpegParams.setCompressionMode(ImageWriteParam.MODE_EXPLICIT);
//設(shè)置壓縮質(zhì)量,取值范圍0.0f~1.0f,1.0f代表質(zhì)量最好;默認(rèn)0.75f,表示視覺(jué)無(wú)損
jpegParams.setCompressionQuality(1.0f);

2.6 ImageWriter

  • ImageWriter是用來(lái)編碼和寫入圖像的抽象超類。我們進(jìn)行圖片格式轉(zhuǎn)換的時(shí)候,主要就是通過(guò)它的子類進(jìn)行寫入的。上邊介紹類那么多API,其實(shí)就是要給它轉(zhuǎn)換圖片來(lái)用的。
  • 重點(diǎn)關(guān)注它的setOutput方法和一系列的write方法:
//設(shè)置輸出路徑,這里雖然傳入的是Object對(duì)象,但是一般應(yīng)該傳入以下兩種對(duì)象:
// 1. FileImageOutputStream,用于寫入文件
// 2. MemoryCacheImageOutputStream,用于寫入內(nèi)存中
void setOutput(Object output);
//把IIOImage對(duì)象關(guān)聯(lián)的對(duì)象直接作為輸入,寫入到輸出對(duì)象
void write(IIOImage image);
//同上,寫入的時(shí)候加上元數(shù)據(jù)、寫入?yún)?shù),我們就應(yīng)該調(diào)用它來(lái)完成格式轉(zhuǎn)換
void write(IIOMetadata metadata, IIOImage image, ImageWriteParam param);
//同上,只是輸入的對(duì)象是RenderedImage的實(shí)現(xiàn)類對(duì)象
void write(RenderedImage image);

3.探索過(guò)程

我們可以hook Android編譯過(guò)程,拿到所有的資源文件。由于本文是探索的過(guò)程,還未集成到項(xiàng)目里,所以探索的demo是拿的apk包反編譯出來(lái)的。而且打包過(guò)程中已經(jīng)禁止了AAPT采用內(nèi)置的壓縮算法對(duì)圖片資源的優(yōu)化,所以反編譯出來(lái)的圖片資源跟打包前應(yīng)該是一致的:

aaptOptions {
    cruncherEnabled = false
}

3.1 獲取需要處理的PNG圖片

3.1.1 根據(jù)位深

  • 由于常見(jiàn)的圖片色彩模式中,只有ARGB是包含透明通道的,所以可以獲取圖片的位深,也即是每像素的大小,根據(jù)其大小來(lái)獲取是否包含透明通道。

它只適用于不經(jīng)過(guò)壓縮處理的圖片,如經(jīng)過(guò)像tinypng、pngguant壓縮過(guò)的,位深會(huì)被壓縮,這點(diǎn)千萬(wàn)要注意!如果經(jīng)過(guò)tinypng或者pngquant算法壓縮后,是可能出現(xiàn)雖然包含透明通道,但是位深(每像素大?。┦?、8、16、24甚至是1的,具體原理涉及到壓縮算法,這里不進(jìn)行深究。

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 根據(jù)是否包含alpha通道

  • 從上述API的介紹里也了解到了,可以通過(guò)ColorModel直接獲取圖片各個(gè)分量上的值的,當(dāng)然也就可以通過(guò)它判斷圖片否包含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 根據(jù)是否包含透明度像素

  • 有的png圖片雖然包含了透明通道,但并未使用,可遍歷每張圖片上的像素點(diǎn),把不包含透明度的圖片全部找出來(lái),進(jìn)行轉(zhuǎn)換,擴(kuò)大轉(zhuǎn)換范圍。
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位,根據(jù)其是否為0判斷是否包含透明通道
}
  • 本文進(jìn)行的驗(yàn)證都是通過(guò)第三種來(lái)的,需要轉(zhuǎn)換的png圖片范圍會(huì)比不包含alpha通道的圖片集稍微大些

3.2 圖像轉(zhuǎn)換

本節(jié)進(jìn)行轉(zhuǎn)換的是debug版本的APK反編譯出來(lái)的目錄,release版本的會(huì)在下一節(jié)闡述。

3.2.1 ImageIO進(jìn)行轉(zhuǎn)換

  • 最開始找到的png轉(zhuǎn)jpg的方法是使用ImageIO,這種方式也是網(wǎng)上能找到的比較多的方法,使用它轉(zhuǎn)換的時(shí)候要注意,由于jpg是不包含alpha通道的,所以轉(zhuǎn)換過(guò)程中需要先畫一個(gè)背景,具體顏色自己可以設(shè)置:
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);
    //創(chuàng)建BufferedImage,并繪制白色的背景
    newBufferedImage.createGraphics().drawImage(bufferedImage, 0, 0, Color.WHITE, null);
    ImageIO.write(newBufferedImage,"jpg",jpgFile);
}

通過(guò)上述介紹過(guò)API以后,這段代碼不難理解了,就是通過(guò)ImageIO把創(chuàng)建的BufferedImage以jpg形式寫回文件。
通過(guò)這次轉(zhuǎn)換以后,輸出文件大小對(duì)比:

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

可以看到,大小減少了很多,但是看看成像質(zhì)量,發(fā)現(xiàn)畫質(zhì)損失的有點(diǎn)嚴(yán)重啊:


畫質(zhì)對(duì)比

左邊是png原圖,右邊是jpg,放大后可以看到邊緣損失很大。

  • 跟蹤源碼發(fā)現(xiàn),ImageIO.write()方法的內(nèi)部其實(shí)也是通過(guò)IIOImage調(diào)用了ImageWrite.write方法,只不是壓縮質(zhì)量設(shè)置的是默認(rèn)的0.75f,那有沒(méi)有可以設(shè)置壓縮質(zhì)量的轉(zhuǎn)換方法呢?

3.2.2 ImageWriter.Write進(jìn)行轉(zhuǎn)換

  • 經(jīng)過(guò)調(diào)研,找到了以下方式進(jìn)行圖片格式轉(zhuǎn)換。通過(guò)上述API的講解,代碼很好理解,這種方式對(duì)圖像的操作也更加靈活:
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);
    //這里設(shè)置壓縮質(zhì)量
    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,否則會(huì)報(bào)出以下錯(cuò)誤,不好排查,改用JDK1.8后可用。

調(diào)用上述API后發(fā)生了Native層的奔潰
  • 先把壓縮質(zhì)量的參數(shù)設(shè)置為1.0f,把PNG圖像轉(zhuǎn)換為JPG,輸入文件大小:
png total size:4226.41KB
jpg total size:5012.55KB

可以看到大小不減少反而增大了,進(jìn)一步驗(yàn)證了,并不是所有的情況,png轉(zhuǎn)換為jpg,圖像大小都會(huì)變小,詳細(xì)說(shuō)明可看參考鏈接。
看看成像質(zhì)量:


壓縮參數(shù)是1.0f時(shí)的成像對(duì)比

左邊是png,右邊是轉(zhuǎn)換后的jpg,雖然畫面的通透性感覺(jué)有點(diǎn)改變,畫質(zhì)還是可以的,可是大小卻變大了

  • 再按照默認(rèn)的0.75的質(zhì)量進(jìn)行轉(zhuǎn)換,輸出文件大?。?/li>
png total size:4226.41KB
jpg total size:1479.10KB

可以看到,文件大小從原來(lái)png的4226KB變成了1479KB,來(lái)看看成像質(zhì)量:


壓縮參數(shù)是0.75f時(shí)的成像對(duì)比

左邊是png原圖,右邊是轉(zhuǎn)換成jpg后的圖像。同樣可以發(fā)現(xiàn)存在肉眼可見(jiàn)的畫質(zhì)損失。

  • 通過(guò)反復(fù)測(cè)試,發(fā)現(xiàn)當(dāng)壓縮質(zhì)量參數(shù)為0.9f的時(shí)候,畫質(zhì)與大小得到了平衡。體積大小相比0.75f時(shí)增加不多。
    大小輸出對(duì)比:
png total size:4226.41KB
jpg total size:2036.71KB

畫質(zhì)對(duì)比:


壓縮參數(shù)是0.9f時(shí)的成像對(duì)比

可以看到圖像的質(zhì)量還是可以的,沒(méi)有明顯的糊邊了。

4.進(jìn)一步探索

上一節(jié)的探索都是在debug版本的APK反編譯進(jìn)行的,由于抖音的圖片資源在打release包的時(shí)候會(huì)經(jīng)過(guò)McImage的優(yōu)化,期間會(huì)用pngguant算法進(jìn)行壓縮,思考,如果此時(shí)我將壓縮后的png的圖片進(jìn)行上述轉(zhuǎn)換,會(huì)發(fā)生什么情況呢?

4.1 release版本探索

壓縮質(zhì)量設(shè)置為0.9,通過(guò)上述程序轉(zhuǎn)換,輸出文件大小:

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

首先看到的是,能檢測(cè)出來(lái)不包含alpha像素的png圖片的數(shù)量少了很多,猜測(cè)這個(gè)可能是用pngquant壓縮后與Java API的檢測(cè)有關(guān),具體源碼不深究了。
我們來(lái)看此時(shí)圖像的成像質(zhì)量,掃描出的不包含alpha像素的png圖片:


png目錄

而發(fā)現(xiàn)jpg中有好多轉(zhuǎn)換失敗的黑圖:


轉(zhuǎn)換后的jpg目錄

不用挑樣張來(lái)對(duì)比成像質(zhì)量了,這是絕對(duì)不允許的,所以通過(guò)算法壓縮后的圖像,轉(zhuǎn)換后,不僅體積變大,而且還有很多轉(zhuǎn)換失敗的。

所以,上述的轉(zhuǎn)換,一定要是針對(duì)未通過(guò)其他算法進(jìn)行壓縮后的圖像資源。

4.2 轉(zhuǎn)換成jpg后,還可以通過(guò)tinypng壓縮嗎?

依然回到debug版本的資源上,進(jìn)一步探索,看轉(zhuǎn)換后的圖像,是否可以通過(guò)tinypng壓縮。

  • 把壓縮后的jpg,通過(guò)tinypng進(jìn)行壓縮(一次20張,分批次進(jìn)行壓縮)
    壓縮后,得到的圖像大小如圖,換算成KB是1099KB


    轉(zhuǎn)換為jpg后又通過(guò)tinypng進(jìn)行壓縮后的大小

    整個(gè)過(guò)程的大小變化:

step 1 掃出需要轉(zhuǎn)換的png原圖 -> 4226KB
step 2 上述png轉(zhuǎn)成jpg -> 2036KB
setp 3 上述的jpg經(jīng)過(guò)tinypng壓縮 -> 1099KB

壓縮后,發(fā)現(xiàn)有些圖像也被損壞,很多圖片出現(xiàn)了奇怪的背景顏色:


經(jīng)過(guò)tinypng壓縮后,有的圖像出現(xiàn)了損壞

雖然圖片大小體積進(jìn)一步變小,但是圖像出現(xiàn)了損壞,這種情況也是不可取的。

4.3png直接用tinypng壓縮

  • png轉(zhuǎn)jpg再進(jìn)行tinypng壓縮后,大小雖然小了很多,但是圖像在tinypng壓縮的時(shí)候失敗了。那么直接把png進(jìn)行tinypng進(jìn)行壓縮,大小和成像質(zhì)量會(huì)怎么樣呢?
    通過(guò)tinypng將png壓縮后,得到對(duì)大小如圖,換算成KB,是1300KB。


    直接把需要轉(zhuǎn)換的圖片放到tinypng上進(jìn)行壓縮后的目錄大小

    大小從原來(lái)的4226KB減小到了1300KB,減小了很多,現(xiàn)在來(lái)看下成像質(zhì)量:


    png原圖與tinypng壓縮后的成像對(duì)比

    同樣,左邊是png原圖,右邊是tinypng壓縮后的,不得不承認(rèn),tinypng壓縮真的很優(yōu)秀,肉眼看去,跟原圖無(wú)異啊。

5. 結(jié)論與思考

  • 如果不考慮使用其他算法對(duì)圖片進(jìn)行壓縮,把不包含透明度的png轉(zhuǎn)換為jpg,體積大小通常情況下會(huì)大大減少;
  • 壓縮質(zhì)量參數(shù)可根據(jù)成像質(zhì)量自行設(shè)定,官方建議0.75f,屬于視覺(jué)無(wú)損;
  • 如果要用壓縮算法對(duì)圖片進(jìn)行壓縮,不建議進(jìn)行格式轉(zhuǎn)換,無(wú)論是轉(zhuǎn)換前壓縮還是轉(zhuǎn)換后壓縮,圖像都可能會(huì)損壞;
  • tinypng壓縮算法還是相當(dāng)優(yōu)秀的,體積大小縮小很多,畫質(zhì)肉眼幾乎看不到損失,良心推薦??;
  • 通過(guò)上述介紹的幾個(gè)Java API,我們對(duì)圖片對(duì)控制是可以達(dá)到每個(gè)像素的粒度,拿到這些信息后是可以做很多事情的,比如:結(jié)合圖像識(shí)別算法,可以判斷圖片的相似度。
  • jpg不包含透明通道,png包含透明通道。通常情況下,在有明亮的色彩與紋理的圖像中,位深相同的情況下,jpg比png圖像擁有更高的壓縮比。我個(gè)人的理解是,圖片位深越大,壓縮比越大,詳情可查看下方鏈接。

廣告時(shí)間

字節(jié)跳動(dòng)各Android客戶端團(tuán)隊(duì)招人火爆進(jìn)行中,各個(gè)級(jí)別和應(yīng)屆實(shí)習(xí)生都需要,業(yè)務(wù)增長(zhǎng)快、日活高、挑戰(zhàn)大、待遇給力,各位大佬走過(guò)路過(guò)千萬(wàn)不要錯(cuò)過(guò)!

本科以上學(xué)歷、對(duì)技術(shù)有熱情,歡迎加我的微信詳聊:spq951992006

歡迎來(lái)掃

參考鏈接

Comparison of different image compression formats

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容