對於WebP格式入門解讀


因為項目中需要用到大量動畫效果,前期嘗試過幾種方案,比如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


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM