Android包大小優化之無Alpha通道PNG轉JPG的探索

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

1.問題引入

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

2.Java相關的 API

Java已經提供了很多API,如BufferedImage、ColorModel、IIOImage、ImageIO、ImageWriter、JPEGImageWriteParam,來幫助進行圖像處理。

2.1 BufferedImage

  • BufferedImage是Image的一個子類,Image和BufferedImage的主要作用就是將一副圖片加載到內存中。BufferedImage生成的圖片在內存里有一個圖像緩沖區,利用這個緩沖區我們可以很方便的操作這個圖片,通常用來做圖片修改操作如大小變換、圖片變灰、設置圖片透明或不透明等。
  • Java將一副圖片加載到內存中的方法是:
BufferedImage bufferedImage = ImageIO.read(new FileInputStream(filePath));
  • 通過BufferedImage得到內存中一張圖片到數據實體后,便可以通過它獲得圖片基本信息,如長、寬、每個像素的值等
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

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

2.3 ImageIO

  • ImageIO是一個輔助類,提供了一系列的靜態方法,可以來獲取已經注冊了的 ImageReader和 ImageWriter的對對象,以及執行簡單編碼和解碼。
//getImageWritersByFormatName方法返回是所有能夠對指定格式進行編碼的ImageWriter的迭代器(Iterator<ImageWriter>),此行代碼獲取了一個能夠對jpg格式編碼的ImageWriter
ImageWriter jpgWriter = ImageIO.getImageWritersByFormatName("jpg").next();
//將一個BufferedImage對象以jpg形式寫入jpgFile中,用它可以進行簡單的圖像格式轉換
ImageIO.write(newBufferedImage,"jpg",jpgFile);

2.4 IIOImage

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

2.5 JPEGImageWriteParam

  • 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

  • ImageWriter是用來編碼和寫入圖像的抽象超類。我們進行圖片格式轉換的時候,主要就是通過它的子類進行寫入的。上邊介紹類那么多API,其實就是要給它轉換圖片來用的。
  • 重點關注它的setOutput方法和一系列的write方法:
//設置輸出路徑,這里雖然傳入的是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 根據位深

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

它只適用于不經過壓縮處理的圖片,如經過像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通道

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

  • 有的png圖片雖然包含了透明通道,但并未使用,可遍歷每張圖片上的像素點,把不包含透明度的圖片全部找出來,進行轉換,擴大轉換范圍。
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判斷是否包含透明通道
}
  • 本文進行的驗證都是通過第三種來的,需要轉換的png圖片范圍會比不包含alpha通道的圖片集稍微大些

3.2 圖像轉換

本節進行轉換的是debug版本的APK反編譯出來的目錄,release版本的會在下一節闡述。

3.2.1 ImageIO進行轉換

  • 最開始找到的png轉jpg的方法是使用ImageIO,這種方式也是網上能找到的比較多的方法,使用它轉換的時候要注意,由于jpg是不包含alpha通道的,所以轉換過程中需要先畫一個背景,具體顏色自己可以設置:
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,放大后可以看到邊緣損失很大。

  • 跟蹤源碼發現,ImageIO.write()方法的內部其實也是通過IIOImage調用了ImageWrite.write方法,只不是壓縮質量設置的是默認的0.75f,那有沒有可以設置壓縮質量的轉換方法呢?

3.2.2 ImageWriter.Write進行轉換

  • 經過調研,找到了以下方式進行圖片格式轉換。通過上述API的講解,代碼很好理解,這種方式對圖像的操作也更加靈活:
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層的奔潰
  • 先把壓縮質量的參數設置為1.0f,把PNG圖像轉換為JPG,輸入文件大小:
png total size:4226.41KB
jpg total size:5012.55KB

可以看到大小不減少反而增大了,進一步驗證了,并不是所有的情況,png轉換為jpg,圖像大小都會變小,詳細說明可看參考鏈接。
看看成像質量:


壓縮參數是1.0f時的成像對比

左邊是png,右邊是轉換后的jpg,雖然畫面的通透性感覺有點改變,畫質還是可以的,可是大小卻變大了

  • 再按照默認的0.75的質量進行轉換,輸出文件大小:
png total size:4226.41KB
jpg total size:1479.10KB

可以看到,文件大小從原來png的4226KB變成了1479KB,來看看成像質量:


壓縮參數是0.75f時的成像對比

左邊是png原圖,右邊是轉換成jpg后的圖像。同樣可以發現存在肉眼可見的畫質損失。

  • 通過反復測試,發現當壓縮質量參數為0.9f的時候,畫質與大小得到了平衡。體積大小相比0.75f時增加不多。
    大小輸出對比:
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壓縮。

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


    轉換為jpg后又通過tinypng進行壓縮后的大小

    整個過程的大小變化:

step 1 掃出需要轉換的png原圖 -> 4226KB
step 2 上述png轉成jpg -> 2036KB
setp 3 上述的jpg經過tinypng壓縮 -> 1099KB

壓縮后,發現有些圖像也被損壞,很多圖片出現了奇怪的背景顏色:


經過tinypng壓縮后,有的圖像出現了損壞

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

4.3png直接用tinypng壓縮

  • png轉jpg再進行tinypng壓縮后,大小雖然小了很多,但是圖像在tinypng壓縮的時候失敗了。那么直接把png進行tinypng進行壓縮,大小和成像質量會怎么樣呢?
    通過tinypng將png壓縮后,得到對大小如圖,換算成KB,是1300KB。


    直接把需要轉換的圖片放到tinypng上進行壓縮后的目錄大小

    大小從原來的4226KB減小到了1300KB,減小了很多,現在來看下成像質量:


    png原圖與tinypng壓縮后的成像對比

    同樣,左邊是png原圖,右邊是tinypng壓縮后的,不得不承認,tinypng壓縮真的很優秀,肉眼看去,跟原圖無異啊。

5. 結論與思考

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

廣告時間

字節跳動各Android客戶端團隊招人火爆進行中,各個級別和應屆實習生都需要,業務增長快、日活高、挑戰大、待遇給力,各位大佬走過路過千萬不要錯過!

本科以上學歷、對技術有熱情,歡迎加我的微信詳聊:spq951992006

歡迎來掃

參考鏈接

Comparison of different image compression formats

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 227,818評論 6 531
  • 序言:濱河連續發生了三起死亡事件,死亡現場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發現死者居然都...
    沈念sama閱讀 98,185評論 3 414
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 175,656評論 0 373
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 62,647評論 1 309
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 71,446評論 6 405
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發上,一...
    開封第一講書人閱讀 54,951評論 1 321
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,041評論 3 440
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 42,189評論 0 287
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當地人在樹林里發現了一具尸體,經...
    沈念sama閱讀 48,718評論 1 333
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 40,602評論 3 354
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發現自己被綠了。 大學時的朋友給我發了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 42,800評論 1 369
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,316評論 5 358
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發生泄漏。R本人自食惡果不足惜,卻給世界環境...
    茶點故事閱讀 44,045評論 3 347
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 34,419評論 0 26
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 35,671評論 1 281
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 51,420評論 3 390
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 47,755評論 2 371

推薦閱讀更多精彩內容