對於WebP格式入門解讀

cryAllen發表於2020-04-30

因為專案中需要用到大量動畫效果,前期嘗試過幾種方案,比如GIF、幀動畫、lottie、SVGA等格式的動畫渲染方案,發現都存在各式各樣的問題。比如:

1,GIF格式。5秒的動畫,一張圖大小可能就會達到5-10M,然後UI那邊製作背景需要透明的效果做不了,打包下載壓縮包所需要更多的流量。

2,幀動畫。簡單說就是把GIF圖片給拆開為一張張圖,比如一秒20幀的GIF圖被拆開為20張靜態圖,然後用程式程式碼組成一幀一幀渲染效果動畫,但是缺點也是很明顯,做不到動態更新,只能提前整合在本地資源中,這個方案也被否決掉。

3,第三方動畫渲染庫。比如基於Airbnb開源的lottie庫和YY出品的SVGA解析庫,lottie解析格式是以字尾為.json檔案,相比GIF檔案,大小是小10倍以上,但是在CPU佔用上卻奇高無比。因為我們的專案針對沒有GPU能力的車機系統,車機上的內建晶片效能比目前主流手機效能差很多。同樣SVGA庫也是因為CPU佔用率高的問題被否決掉。

基於目前已有的硬體條件,可能最希望是升級硬體裝置,那樣的話無論是對於UI和開發來說,都是皆大歡喜,UI可基於lottie做炫酷的動效,而開發也不會因為效能問題而進行各種評估。但現實往往是殘酷的,只能基於目前車機條件進行開發,那麼作為開發人員,當然是得想各種方法去滿足產品需求了,那就把目光轉移,後來轉移到一種叫做「WebP」格式的圖片。

基於WebP格式做出來的圖片,UI那邊可以做透明的背景動效,我們開發這邊測了下效能,發現CPU和記憶體佔用也滿足產品測的要求,正好折中是我們想要選擇的解決方案。既然之前是沒怎麼聽過,那麼就有必須去了解下「WebP」是什麼東西了。

介紹

對於之前沒接觸過的知識點,首先第一步是打Google,輸入webp這四個字母,Google搜尋出來的首頁就會告訴你這是什麼了,也就是What的定義。引用「WebP」官網定義的一句話:

WebP is a modern image format that provides superior lossless and lossy compression for images on the web. Using WebP, webmasters and web developers can create smaller, richer images that make the web faster.

進一步說,「WebP」是一種新的圖片格式,可提供出色的無損和有失真壓縮,對於Web開發來說,可以建立更小和更豐富的影像。根據官網測試,WebP無失真壓縮的圖片比PNG格式圖片,檔案大小上少 26%,WebP有損圖片在同樣 SSIM 質量指標上比JPEG格式圖片少25~34%,SSIM是一種衡量兩張數字影像相似的指標。

官網給出有失真壓縮測試方法:

  1. 將PNG圖片設定不同的壓縮引數壓縮成JPEG圖片,記錄壓縮後的對比的SSIM。
  2. 將同一張PNG圖片壓縮成WebP圖片,壓縮的WebP圖片的SSIM指標必須比1中記錄的SSIM高。

對比圖如下:

對比圖

同樣WebP與JPG格式進行載入時間對比,可以發現WebP優秀很多。

圖片數量

載入時間

從圖中可以看到大小和圖片載入速度上比jpg格式優勝很多,對於web頁面來說,檔案體積減少了,載入時間縮短了,那麼頁面的渲染速度加快了,特別是圖片越來越多的情況下,能對效能進行提升和頻寬節省。

對比GIF

對於專案中要用到各種動效圖片,大部分人首先想到是GIF格式的圖片,那麼相比GIF,WebP有什麼優勢呢?

  1. 支援有損和無失真壓縮,並且可以合併有損和無損圖片幀。
  2. 體積會更小,這點是很關鍵,親測下來有損的圖片可以減少60%的體積,而無損可以減少20%的體積。
  3. 與GIF的8位顏色和1位alpha相比,支援24-bitRGB顏色和Alpha通道,對於UI設計來說更友好和更少限制,做出更炫酷的動效。
  4. 有動畫、關鍵幀、metadate、顏色配置檔案等資料,有失真壓縮是調節的。

WebP一些劣勢

  1. WebP的直線解碼比GIF佔用更多的CPU資源,有損WebP的解碼時間是GIF的2.2倍,而無損WebP的解碼時間是GIF的1.5倍,因此在客戶端來說,對比GIF格式,WebP解碼需要更多CPU計算資源。
  2. 相比GIF來說,使用的普遍性不高,相關資料比較少,需要去解讀官方文件。
  3. 各個端支援情況不一,需要自己寫個直譯器去渲染WebP格式的圖片。
  4. 如果要遷移的話,遷移成本較大,需要對所有圖片重新編碼,考慮到對舊版的支援,需要額外開闢空間存兩種格式的圖片。

解碼器設計

對於Android系統來說,WebP 在Android 4.0及以上原生支援,對於4.0以下可以使用官方提供提供的編解碼庫,但現在主流的手機上,Android 4.0以下已經可以忽略不計了,反而對於在IOT裝置上,則有可能存在低版本,因此對於此類開發專案,如果選擇WebP格式則需要事先評估下了。

從官網的描述來看,WebP是使用VP8關鍵幀編碼以有損方式進行影像資料壓縮,也就是說如果要支援解碼的話,我們需要對這個VP8演算法進行解碼。WebP容器,也就是WebP的RIFF容器是支援在WebP的基本用例的功能。

WebP檔案格式基於RIFF(資源交換檔案格式)文件格式。具體格式定義如下:

 0                   1                   2                   3
 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|                         Chunk FourCC                          |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|                          Chunk Size                           |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|                         Chunk Payload                         |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+

RIFF檔案的基本元素是一個塊。它包括了Chunk FourCC 、 Chunk Size、 Chunk Payload三部分 。其中Chunk FourCC是一個32位ASCII編碼的塊檔案的唯一標識。 Chunk Size則代表該塊檔案的大小, Chunk Payload則是資料有效承載,如果“塊大小”為奇數,則新增一個填充位元組(應為0)。

我們常用ChunkHeader('ABCD')來描述RIFF檔案,這裡ABCD則是FourCC單個塊,則該元素大小為8個位元組。

那麼接下去看WebP檔案頭,具體格式如下:

 0                   1                   2                   3
 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|      'R'      |      'I'      |      'F'      |      'F'      |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|                           File Size                           |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|      'W'      |      'E'      |      'B'      |      'P'      |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+

1,'RIFF': 32 bits:32位 ASCII字元“ R”,“ I”,“ F”,“ F”。

2,檔案大小,32位,從偏移量8開始的檔案大小,以位元組為單位。此欄位的最大值為2 ^ 32減去10個位元組,因此,整個檔案的大小最多為4GiB減去2個位元組。

3,'WEBP': 32 bits:ASCII字元“ W”,“ E”,“ B”,“ P”。

那麼對於包含多幀動畫為主的圖片,它的標頭檔案如何呢,具體如下:

 0                   1                   2                   3
 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|                      ChunkHeader('ANIM')                      |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|                       Background Color                        |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|          Loop Count           |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+

Background Color:畫布的預設背景顏色,以[B,G,R,Alpha]位元組順序排列,此顏色可用於填充框架周圍畫布上未使用的空間,以及第一幀的透明畫素。處置方法為1時也使用背景色。

Loop Count:迴圈播放動畫的次數。 0表示無限迴圈。

除了這幾個檔案頭格式之外,還有其他幾個檔案頭格式,比如VP8X、VP8、VP8L、ANMF、ICCP等,具體格式可以在 Extended File Format 檢視。基於Android系統的話,主要是以VP8X、VP8、VP8演算法解碼,對塊檔案進行解析,程式碼如下:

static BaseChunk parseChunk(WebPReader reader) throws IOException {
        //@link {https://developers.google.com/speed/webp/docs/riff_container#riff_file_format}
        int offset = reader.position();
        int chunkFourCC = reader.getFourCC();
        int chunkSize = reader.getUInt32();
        BaseChunk chunk;
        if (VP8XChunk.ID == chunkFourCC) {
            chunk = new VP8XChunk();
        } else if (ANIMChunk.ID == chunkFourCC) {
            chunk = new ANIMChunk();
        } else if (ANMFChunk.ID == chunkFourCC) {
            chunk = new ANMFChunk();
        } else if (ALPHChunk.ID == chunkFourCC) {
            chunk = new ALPHChunk();
        } else if (VP8Chunk.ID == chunkFourCC) {
            chunk = new VP8Chunk();
        } else if (VP8LChunk.ID == chunkFourCC) {
            chunk = new VP8LChunk();
        } else if (ICCPChunk.ID == chunkFourCC) {
            chunk = new ICCPChunk();
        } else if (XMPChunk.ID == chunkFourCC) {
            chunk = new XMPChunk();
        } else if (EXIFChunk.ID == chunkFourCC) {
            chunk = new EXIFChunk();
        } else {
            chunk = new BaseChunk();
        }
        chunk.chunkFourCC = chunkFourCC;
        chunk.payloadSize = chunkSize;
        chunk.offset = offset;
        chunk.parse(reader);
        return chunk;
    }

在對演算法解碼之前,需要把WebP格式檔案載入到記憶體中去,此時就需要用到Reader這個讀寫器,我們從官網的定義可以看到,讀取WebP檔案的程式碼稱為讀取器,而寫入WebP檔案的程式碼稱為寫入器。那麼這個涉及到檔案I/O的讀寫,資料流的讀取和寫入問題。

具體定義讀取器的介面程式碼如下:

public interface Reader {
    long skip(long total) throws IOException;

    byte peek() throws IOException;

    void reset() throws IOException;

    int position();

    int read(byte[] buffer, int start, int byteCount) throws IOException;

    int available() throws IOException;

    /**
     * close io
     */
    void close() throws IOException;

    InputStream toInputStream() throws IOException;
}

具體檔案讀取可以從檔案、位元組流等地方獲取。讀取資料之後,就需要對資料進行解析,我們知道如果是動畫效果的圖片,本質是以幀集合組成的內容,無論是GIF圖支援WebP格式的動畫圖,本質也是一幀一幀進行渲染。好比我們看到的Android渲染檢視是以一秒60幀,所以我們看到如果每幀超過16ms的話,就容易引起卡頓的原因。

因此對於幀渲染介面的定義就顯得很關鍵了,具體介面定義如下:

public abstract class Frame<R extends Reader, W extends Writer> {
    protected final R reader;
    public int frameWidth;
    public int frameHeight;
    public int frameX;
    public int frameY;
    public int frameDuration;

    public Frame(R reader) {
        this.reader = reader;
    }

    public abstract Bitmap draw(Canvas canvas, Paint paint, int sampleSize, Bitmap reusedBitmap, W writer);
}

一幀可以理解為一張靜態圖,如果有20幀組成的動畫,可以理解成有20張圖片按照連貫順序一張張過一遍,那就形成了有動畫的效果。所以我們要解析動畫,本質是還是去解析每張靜態圖,通過每張圖的繪製,把整個動畫給繪製出來。這一張圖片就包括寬度、高度、在螢幕上的橫向、縱向座標、執行時間等,但最關鍵還是需要把圖會繪製出來,這裡面就是draw方法的重寫。

關於draw方法過載,還是以繪製圖片為主,具體程式碼如下:

public Bitmap draw(Canvas canvas, Paint paint, int sampleSize, Bitmap reusedBitmap, WebPWriter writer) {
        BitmapFactory.Options options = new BitmapFactory.Options();
        options.inJustDecodeBounds = false;
        options.inSampleSize = sampleSize;
        options.inMutable = true;
        options.inBitmap = reusedBitmap;
        int length = encode(writer);
        byte[] bytes = writer.toByteArray();
        Bitmap bitmap = BitmapFactory.decodeByteArray(bytes, 0, length, options);
        assert bitmap != null;
        if (blendingMethod) {
            paint.setXfermode(null);
        } else {
            paint.setXfermode(PORTERDUFF_XFERMODE_SRC_OVER);
        }
        canvas.drawBitmap(bitmap, (float) frameX * 2 / sampleSize, (float) frameY * 2 / sampleSize, paint);
        return bitmap;
    }

我們知道Bitmap在Android中指的是一張圖片,可以是png格式也可以是jpg等其他常見的圖片格式。BitmapFactory類提供了四類方法:decodeFile、decodeResource、decodeStream和decodeByteArray,分別用於支援從檔案系統、資源、輸入流以及位元組陣列中載入出一個Bitmap物件,其中decodeFile和decodeResource又間接呼叫了decodeStream方法,這四類方法最終是在Android的底層實現的,對應著BitmapFactory類的幾個native方法。

那麼該高效地載入Bitmap呢,其實核心思也很簡單,就是採用BitmapFactory.Options來載入所需尺寸的圖片。主要是用到它的inSampleSize引數,即取樣率。當inSampleSize為1時,取樣後的圖片大小為圖片的原始大小,當inSampleSize大於1時,比如為2,那麼取樣後的圖片其寬/寬均為原圖大小的1/2,而畫素數為原圖的1/4,其佔有的記憶體大小也為原圖的1/4。從最新官方文件中指出,inSampleSize的取值應該是2的指數,比如1、2、4、8、16等等。

通過取樣率即可有效地載入圖片,那麼到底如何獲取取樣率呢,獲取取樣率也很簡單,循序如下流程:

  • 將BitmapFactory.Options的inJustDecodeBounds引數設為True並載入圖片
  • 從BitmapFactory.Options中取出圖片的原始寬高資訊,他們對應於outWidth和outHeight引數
  • 根據取樣率的規則並結合目標View的所需大小計算出取樣率inSampleSize
  • 將BitmapFactory.Options的inJustDecodeBounds引數設為False,然後重新載入圖片。

你看設計到最後,本質還是把由很多幀組成的動畫格式,拆分到具體每一幀的圖片,針對圖片進行圖片幀繪製,進而把動畫的效果給渲染出來。

總結

總的來說,不同圖片顯示選擇是根據具體業務場景來做評估,像我們最近在開發的專案中,主要是以圖片形象為主,那麼就會過多關注有關圖片的CPU使用率和記憶體佔用率的比例。如果發現常規的圖片格式不滿足需求,那麼就是需要調研和尋找不同的解決方案。這本來就是沒有固定的一套解決方案,只有相對合適的解決方案,因此,無論是從UI角度,還是從開發角度,甚至是產品角度,都得尋得整個產品中平衡度,尋找合適點,是能滿足各方需求,進而打造更完善的產品應用。

參考地址:

1,https://developers.google.cn/speed/webp

2,https://developers.google.cn/speed/webp/docs/riff_container

2,https://github.com/penfeizhou/APNG4Android

相關文章