優化包大小-PNG部分

背景

PNG 圖片相對於 JPEG 圖片來講,它是一種無損的圖像存儲格式,同時多了一條透明度通道,因此通常狀況下,PNG 圖片要比 JPEG 圖片要大,而且 PNG 圖片每每仍是 APK 圖片資源中的大頭,因此優化 PNG 圖片的大小,對於減少包的體積來講,是比較有回報的事情。android

關於 PNG 的 wiki:git

便攜式網絡圖形(英語:Portable Network Graphics,PNG)是一種無損壓縮位圖圖形格式,支持索引、灰度RGB三種顏色方案以及Alpha通道等特性。github

關於 JPEG 的 wiki:算法

聯合圖像專家小組(英語:Joint Photographic Experts Group,縮寫:JPEG)是一種針對照片影像而普遍使用的有損壓縮標準方法。數組

經常使用的壓縮算法

關於 PNG 的壓縮算法有不少,這裏咱們只說兩種比較經常使用的:Indexed_colorColor_quantization。這兩種也是 Google 在 Android 開發者網站上推薦的,具體能夠看 network-xfer網絡

下面咱們會簡單說下這兩種算法的大概原理,更深刻的知識請移步 Google 或者 Wiki。app

Indexed_color

字面意思就是索引顏色,經過將具體的 ARGB 顏色存儲轉換成索引下表,來減小文件的大小。咱們知道 ARGB 中,每一個通道的存儲都須要 8 位,也就是 1 字節,一個 ARGB 存儲就須要 4 字節,而索引的存儲只須要 1 字節。而索引指向的顏色會存放在一個叫 palette(調色板)的數組裏面。ide

wiki 定義:工具

在計算中,索引顏色是一種以有限的方式管理數字圖像顏色的技術,以節省計算機內存文件存儲空間,同時加快顯示刷新和文件傳輸的速度。它是矢量量化壓縮的一種形式。源碼分析

圖片來自於 wiki:

Indexed_color

這種算法很好,但也有缺點,調色板的大小一般只支持 4,16,256 這幾種,也就是說最大不會超過 256 個,因此能應用這種算法的 PNG 圖片中的顏色使用不能超過 256 個。

Color_quantization

字面意思就是顏色矢量化,經過使用類似顏色來減小圖像中使用的顏色種類,再配合調色板,來達到減小圖片文件大小的目的,這是一種有損的壓縮算法。

圖片來自於 wiki:

這是使用標準的 24 位 RGB 顏色的圖像:

24位RGB

這是優化成只使用 16 種顏色的圖像:

16種顏色

這種算法的缺點就是質量有損,因此如何在質量和大小中間達到一個平衡點,這個相當重要。

談談 AAPT

AAPT 是 Android 的資源打包工具,咱們經常使用的 R 文件就是用它生存的,除此以外,它還有壓縮 PNG 圖片的功能。AAPT 如今有 AAPT 和 AAPT2 了,默認咱們都是說的是 AAPT2。

關於 AAPT2 對於 PNG 圖片的壓縮知識,能夠看下 Colt McAnlis 的這篇文章,Smaller PNGs, and Android’s AAPT tool

PS:做者就是 Android 性能寶典裏面那個光頭。。。

AAPT2 對於 PNG 圖片的壓縮能夠分爲三個方面:

  • RGB 是否能夠轉化成灰度
  • 透明通道是否能夠刪除
  • 是否是最多隻有 256 色(Indexed_color 優化)

接下來,咱們從源碼入手,只看看上面所說的這幾點吧。

源碼分析用的是 Android 6.0 也就是 marshmallow 的版本:

android.googlesource.com/platform/fr…

對於 PNG 的分析代碼位於 analyze_image() 這個方法中,國際慣例,刪除一些不影響分析的代碼。

static void analyze_image() {
    int w = imageInfo.width;
    int h = imageInfo.height;
    uint32_t colors[256], col;
    int num_colors = 0;
    bool isOpaque = true;
    bool isPalette = true;
    bool isGrayscale = true;
    // Scan the entire image and determine if:
    // 1. Every pixel has R == G == B (grayscale)
    // 2. Every pixel has A == 255 (opaque)
    // 3. There are no more than 256 distinct RGBA colors
    for (j = 0; j < h; j++) {
        const png_byte* row = imageInfo.rows[j];
        png_bytep out = outRows[j];
        for (i = 0; i < w; i++) {
            rr = *row++;
            gg = *row++;
            bb = *row++;
            aa = *row++;
            int odev = maxGrayDeviation;
            maxGrayDeviation = MAX(ABS(rr - gg), maxGrayDeviation);
            maxGrayDeviation = MAX(ABS(gg - bb), maxGrayDeviation);
            maxGrayDeviation = MAX(ABS(bb - rr), maxGrayDeviation);
          
            // Check if image is really grayscale
            if (isGrayscale) {
                if (rr != gg || rr != bb) {
                  // ==>> Code 1
                    isGrayscale = false;
                }
            }
            // Check if image is really opaque
            if (isOpaque) {
                if (aa != 0xff) {
                  // ==>> Code 2
                    isOpaque = false;
                }
            }
            // Check if image is really <= 256 colors
            if (isPalette) {
                col = (uint32_t) ((rr << 24) | (gg << 16) | (bb << 8) | aa);
                bool match = false;
                for (idx = 0; idx < num_colors; idx++) {
                    if (colors[idx] == col) {
                        match = true;
                        break;
                    }
                }
                if (!match) {
                    if (num_colors == 256) {
                      // ==>> Code 3
                        isPalette = false;
                    } else {
                        colors[num_colors++] = col;
                    }
                }
            }
        }
    }
    *paletteEntries = 0;
    *hasTransparency = !isOpaque;
    int bpp = isOpaque ? 3 : 4;
    int paletteSize = w * h + bpp * num_colors;
    
    // Choose the best color type for the image.
    // 1. Opaque gray - use COLOR_TYPE_GRAY at 1 byte/pixel
    // 2. Gray + alpha - use COLOR_TYPE_PALETTE if the number of distinct combinations
    // is sufficiently small, otherwise use COLOR_TYPE_GRAY_ALPHA
    // 3. RGB(A) - use COLOR_TYPE_PALETTE if the number of distinct colors is sufficiently
    // small, otherwise use COLOR_TYPE_RGB{_ALPHA}
    if (isGrayscale) {
        if (isOpaque) {
          // ==>> Code 4
            *colorType = PNG_COLOR_TYPE_GRAY; // 1 byte/pixel
        } else {
            // Use a simple heuristic to determine whether using a palette will
            // save space versus using gray + alpha for each pixel.
            // This doesn't take into account chunk overhead, filtering, LZ
            // compression, etc.
            if (isPalette && (paletteSize < 2 * w * h)) {
              // ==>> Code 5
                *colorType = PNG_COLOR_TYPE_PALETTE; // 1 byte/pixel + 4 bytes/color
            } else {
              // ==>> Code 6
                *colorType = PNG_COLOR_TYPE_GRAY_ALPHA; // 2 bytes per pixel
            }
        }
    } else if (isPalette && (paletteSize < bpp * w * h)) {
      // ==>> Code 7
        *colorType = PNG_COLOR_TYPE_PALETTE;
    } else {
        if (maxGrayDeviation <= grayscaleTolerance) {
          // ==>> Code 8
            *colorType = isOpaque ? PNG_COLOR_TYPE_GRAY : PNG_COLOR_TYPE_GRAY_ALPHA;
        } else {
          // ==>> Code 9
            *colorType = isOpaque ? PNG_COLOR_TYPE_RGB : PNG_COLOR_TYPE_RGB_ALPHA;
        }
    }
    
}
複製代碼

首先定義了 3 個變量分別表示:isOpaque(是否不透明),isPalette(是否支持調色板),isGrayscale(是否可轉化爲灰度)。

首先看 Code 1 處的代碼:

if (rr != gg || rr != bb) {
                  // ==>> Code 1
                    isGrayscale = false;
                }
複製代碼

只有 RGB 三種通道的顏色都同樣,才能轉化成灰度。

Code 2 處的代碼是判斷透明通道是否爲 0,也就是是否是不透明:

if (aa != 0xff) {
                  // ==>> Code 2
                    isOpaque = false;
                }
複製代碼

而 Code 3 處則是判斷是否能夠用 256 色的調色板:

if (!match) {
                    if (num_colors == 256) {
                      // ==>> Code 3
                        isPalette = false;
                    } else {
                        colors[num_colors++] = col;
                    }
                }
複製代碼

colors 是一個數組,裏面存放的是圖片中已經出現的(不重複)顏色,當顏色的數量大於 256 即表示不支持調色板模式。

而後根據這些條件,來判斷使用哪一種存儲模式,AAPT 中支持的存儲模式有如下幾種:

  • PNG_COLOR_TYPE_PALETTE

    使用調色板模式,最終圖片的大小就是 一個像素 1 字節 + 調色板中一個顏色 4 字節

  • PNG_COLOR_TYPE_GRAY

    灰度模式,這種是最節省的模式,一個像素 1 字節

  • PNG_COLOR_TYPE_GRAY_ALPHA

    灰度模式,同時存在透明通道,一個像素 2 字節

  • PNG_COLOR_TYPE_RGB

    RGB 模式,刪除了透明通道,一個像素 3 字節

  • PNG_COLOR_TYPE_RGB_ALPHA

    ARGB 模式,一個像素 4 字節

PNG_COLOR_TYPE_PALETTE

要使用這種模式,須要知足如下兩個條件,分別是 Code 5 和 Code 7:

Code 5

if (isGrayscale) {
        if (isOpaque) {
        } else {
          if (isPalette && (paletteSize < 2 * w * h)) {
            // ==>> Code 5
                *colorType = PNG_COLOR_TYPE_PALETTE; // 1 byte/pixel + 4 bytes/color
            } 
        }
 }
複製代碼

在支持灰度模式的前提下,有透明通道,支持調色板模式,同時調色板的長度小於 2 * w * h

Code 7

if (isGrayscale) {
  
} else {
  if (isPalette && (paletteSize < bpp * w * h)) {
    // Code ==>> 7
        *colorType = PNG_COLOR_TYPE_PALETTE;
    }
}
複製代碼

若是不支持灰度模式,但支持調色板,同時調色板長度小於 bpp * w * h,其中 bpp 的大小根據是否爲不透明爲決定:

int bpp = isOpaque ? 3 : 4;
複製代碼

PNG_COLOR_TYPE_GRAY

要使用這種模式,須要知足支持灰度模式,同時不透明。代碼位於 Code 4:

if (isGrayscale) {
        if (isOpaque) {
          // ==>> Code 4
            *colorType = PNG_COLOR_TYPE_GRAY; // 1 byte/pixel
        }
 }
複製代碼

###PNG_COLOR_TYPE_GRAY_ALPHA

灰度,同時存在透明通道的模式。代碼位於 Code 6 和 Code 8:

Code 6

if (isGrayscale) {
        if (isOpaque) {
        } else {
          if (isPalette && (paletteSize < 2 * w * h)) {
            } else {
            // ==>> Code 6
            *colorType = PNG_COLOR_TYPE_GRAY_ALPHA; // 2 bytes per pixel
          }
        }
 }
複製代碼

Code 8

if (isGrayscale) {
        
    } else if (isPalette && (paletteSize < bpp * w * h)) {
    } else {
        if (maxGrayDeviation <= grayscaleTolerance) {
          // ==>> Code 8
            *colorType = isOpaque ? PNG_COLOR_TYPE_GRAY : PNG_COLOR_TYPE_GRAY_ALPHA;
        } else {
        }
    }

複製代碼

maxGrayDeviation 是計算 RGB 通道直接的差值,若是小於 grayscaleTolerance 這個闕值,那麼也能夠轉成灰度。

PNG_COLOR_TYPE_RGB

不透明圖片能夠刪除透明通道,代碼位於 Code 9:

if (isGrayscale) {
        
    } else if (isPalette && (paletteSize < bpp * w * h)) {
    } else {
        if (maxGrayDeviation <= grayscaleTolerance) {
        } else {
          // ==>> Code 9
                      *colorType = isOpaque ? PNG_COLOR_TYPE_RGB : PNG_COLOR_TYPE_RGB_ALPHA;
        }
    }


複製代碼

PNG_COLOR_TYPE_RGB_ALPHA

這個沒什麼好說的,最後的兜底模式。

小結

AAPT 對 PNG 的優化,主要是 Indexed_color 算法,這也是一種保守的選擇,由於這種是無損的,若是咱們想要更高的壓縮率,可使用一些其餘的壓縮工具,來集成到咱們的編譯打包流程中。

PNG 壓縮工具對比

首先,在咱們選擇其餘 PNG 壓縮工具其餘,咱們須要先禁用 AAPT 的默認壓縮模式,由於對於 PNG 的壓縮,並非 1 + 1 > 2,能夠經過如下代碼關閉:

android {
     aaptOptions {
        cruncherEnabled = false
    }
}
複製代碼

如今經常使用的 PNG 壓縮工具備 pngcrushpngquantzopflitinypng 等,這裏咱們就先不考慮 tinypng,由於這個只提供了 HTTP 接口的形式,而且有次數限制。

使用 Gradle 集成

爲了將 PNG 壓縮工具集成到 APK 的構建編譯流程,咱們使用 Gradle 來實現。做者當前使用 Android Gradle 插件版本爲 3.5.0,在這個版本咱們能夠經過 ApplicationVariant.allRawAndroidResources 獲取全部的資源目錄。

須要注意的是,咱們這個 Task 須要在 MergeResources 這個 Task 以前執行,這樣咱們能夠將壓縮後的同名覆蓋,再合併到 APK 文件中。

afterEvaluate {

        applicationVariants.all { ApplicationVariant variant ->

            if (variant.buildType.name != "release") {
                return
            }


            def compressPngTask = task('compressPng')
            compressPngTask.doLast {
                List<File> allResource = []

                variant.allRawAndroidResources.files.forEach { dir ->

                    if (!dir.exists()) {
                        return
                    }

                    if (dir.isFile()) {
                        if (dir.name.endsWith(".png")) {
                            allResource.add(file)
                        }
                    } else {
                        dir.traverse { file ->
                            if (file.name.endsWith(".png")) {
                                allResource.add(file)
                            }
                        }
                    }


                }


                allResource.forEach { file ->
                    // kb
                    def oldSize = file.size() / 1024f

                    println "path = ${file.path}"
                    try {
                       // TODO 這裏就是咱們執行壓縮的邏輯
                    } catch (Throwable ignore) {
                        println "file: ${file.name} error: ${ignore.message}"
                    }


                    println "${file.name}: $oldSize KB ==> ${file.size() / 1024f} KB"

                }


            }


            Task mergeResourcesTask = variant.mergeResourcesProvider.get()
            mergeResourcesTask.dependsOn(compressPngTask)
        }


    }
複製代碼

首先咱們建立一個名爲 compressPng 的 Task,這個 Task 的任務就是收集全部 PNG 文件路徑,這裏咱們過濾掉 BuildType 不是 release 的 Variant,最後讓 MergeResources Task 依賴於 compressPng Task。

記得先關掉 AAPT 默認的 PNG 壓縮。

這裏咱們先記錄下關閉 AAPT 默認 PNG 壓縮後的 APK 文件大小,和使用默認 PNG 壓縮後的 APK 文件大小:

這裏我找的是一個直播的項目,裏面有比較多的圖片資源文件,過濾了 JPEG 和 .9 圖,大概有 1000多張 PNG 圖片,只使用一個壓縮線程。

壓縮工具 APK 大小(MB) 耗時
81.9 29s
AAPT 78.9 1m 18s

pngquant

首先測試 pngquant,咱們將pngquant 集成進去:

exec { ExecSpec spec ->
                            spec.workingDir(project.file('.'))
                            spec.commandLine("./pngquant", "--skip-if-larger", "--speed", "1", "--strip", "--nofs", "--force", "--output", file.path, "--", file.path)
                        }
複製代碼

這裏咱們用的基本都是默認配置:

  • --skip-if-larger 表示若是壓縮後的圖片更大了,就跳過
  • --speed 1 表示使用最慢的壓縮速度,來換取更好的壓縮質量
  • --strip 刪除圖片的一些元數據
  • --nofs 禁用 Floyd–Steinberg dithering 圖像抖動處理
  • --force 覆蓋源文件
  • --output 輸出文件的路徑

先執行 Clean Task,再從新執行打包,最終結果以下:

壓縮工具 APK 大小(MB) 耗時
81.9 29s
AAPT 78.9 32s
pngquant 73.7 1m 18s

zopflipng

集成進去:

exec { ExecSpec spec ->
                            spec.workingDir(new File('/Users/leo/Documents/projects/zopfli'))
                            spec.commandLine("./zopflipng", "-y", "-m", file.path, file.path)

                        }
複製代碼

使用的也是基本配置:

  • -y 覆蓋原文件,不須要詢問
  • -m 儘量多壓縮,依賴於文件的大小

先執行 Clean Task,再從新執行打包,最終結果以下:

壓縮工具 APK 大小(MB) 耗時
81.9 29s
AAPT 78.9 32s
pngquant 73.7 1m 18s
zopflipng 78 36m 17s

pngcursh

集成進去:

exec { ExecSpec spec ->
                            spec.workingDir(new File('/Users/leo/Documents/projects/pngcrush-1.8.13'))
                            spec.commandLine("./pngcrush", "-ow","-reduce", file.path)
                        }
複製代碼

使用基本配置:

  • -ow 表示覆蓋原文件
  • -reduce 減小無損顏色值的類型和位深度
  • -brute 嘗試 176 種壓縮方法

先執行 Clean Task,再從新執行打包,最終結果以下:

壓縮工具 APK 大小(MB) 耗時
81.9 29s
AAPT 78.9 32s
pngquant 73.7 1m 18s
zopflipng 78 36m 17s
pngcursh 78.7 13m 56s

小結

雖然從結果上來看,pngquant 是最好的選擇,但由於 pngcursh 這塊使用的只是默認的壓縮配置,並且 pngcursh 提供的參數是最多的,因此具體哪一個更優,只能靠調參數了,衆所周知,調參數也是技術活。

總結

推薦使用 WebP 和 SVG 來代替 PNG 和 JPEG 圖片的使用,但有些三方庫的圖片是咱們沒辦法控制的,這塊就能夠用 PNG 壓縮工具來進行優化。至於若是平衡壓縮率、壓縮耗時,這就是要靠你們去調參數了。

相關文章
相關標籤/搜索