由於項目中須要用到大量動畫效果,前期嘗試過幾種方案,好比GIF、幀動畫、lottie、SVGA等格式的動畫渲染方案,發現都存在各式各樣的問題。好比:java
1,GIF格式。5秒的動畫,一張圖大小可能就會達到5-10M,而後UI那邊製做背景須要透明的效果作不了,打包下載壓縮包所須要更多的流量。android
2,幀動畫。簡單說就是把GIF圖片給拆開爲一張張圖,好比一秒20幀的GIF圖被拆開爲20張靜態圖,而後用程序代碼組成一幀一幀渲染效果動畫,可是缺點也是很明顯,作不到動態更新,只能提早集成在本地資源中,這個方案也被否決掉。git
3,第三方動畫渲染庫。好比基於Airbnb開源的lottie庫和YY出品的SVGA解析庫,lottie解析格式是之後綴爲.json文件,相比GIF文件,大小是小10倍以上,可是在CPU佔用上卻奇高無比。由於咱們的項目針對沒有GPU能力的車機系統,車機上的內置芯片性能比目前主流手機性能差不少。一樣SVGA庫也是由於CPU佔用率高的問題被否決掉。github
基於目前已有的硬件條件,可能最但願是升級硬件設備,那樣的話不管是對於UI和開發來講,都是皆大歡喜,UI可基於lottie作炫酷的動效,而開發也不會由於性能問題而進行各類評估。但現實每每是殘酷的,只能基於目前車機條件進行開發,那麼做爲開發人員,固然是得想各類方法去知足產品需求了,那就把目光轉移,後來轉移到一種叫作「WebP」格式的圖片。web
基於WebP格式作出來的圖片,UI那邊能夠作透明的背景動效,咱們開發這邊測了下性能,發現CPU和內存佔用也知足產品測的要求,正好折中是咱們想要選擇的解決方案。既然以前是沒怎麼聽過,那麼就有必須去了解下「WebP」是什麼東西了。算法
介紹
對於以前沒接觸過的知識點,首先第一步是打Google,輸入webp這四個字母,Google搜索出來的首頁就會告訴你這是什麼了,也就是What的定義。引用「WebP」官網定義的一句話:json
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.canvas
進一步說,「WebP」是一種新的圖片格式,可提供出色的無損和有損壓縮,對於Web開發來講,能夠建立更小和更豐富的圖像。根據官網測試,WebP無損壓縮的圖片比PNG格式圖片,文件大小上少 26%,WebP有損圖片在一樣 SSIM 質量指標上比JPEG格式圖片少25~34%,SSIM是一種衡量兩張數字影像類似的指標。數組
官網給出有損壓縮測試方法:框架
- 將PNG圖片設置不一樣的壓縮參數壓縮成JPEG圖片,記錄壓縮後的對比的SSIM。
- 將同一張PNG圖片壓縮成WebP圖片,壓縮的WebP圖片的SSIM指標必須比1中記錄的SSIM高。
對比圖以下:
一樣WebP與JPG格式進行加載時間對比,能夠發現WebP優秀不少。
從圖中能夠看到大小和圖片加載速度上比jpg格式優勝不少,對於web頁面來講,文件體積減小了,加載時間縮短了,那麼頁面的渲染速度加快了,特別是圖片愈來愈多的狀況下,能對性能進行提高和帶寬節省。
對比GIF
對於項目中要用到各類動效圖片,大部分人首先想到是GIF格式的圖片,那麼相比GIF,WebP有什麼優點呢?
- 支持有損和無損壓縮,而且能夠合併有損和無損圖片幀。
- 體積會更小,這點是很關鍵,親測下來有損的圖片能夠減小60%的體積,而無損能夠減小20%的體積。
- 與GIF的8位顏色和1位alpha相比,支持24-bitRGB顏色和Alpha通道,對於UI設計來講更友好和更少限制,作出更炫酷的動效。
- 有動畫、關鍵幀、metadate、顏色配置文件等數據,有損壓縮是調節的。
WebP一些劣勢
- WebP的直線解碼比GIF佔用更多的CPU資源,有損WebP的解碼時間是GIF的2.2倍,而無損WebP的解碼時間是GIF的1.5倍,所以在客戶端來講,對比GIF格式,WebP解碼須要更多CPU計算資源。
- 相比GIF來講,使用的廣泛性不高,相關資料比較少,須要去解讀官方文檔。
- 各個端支持狀況不一,須要本身寫個解釋器去渲染WebP格式的圖片。
- 若是要遷移的話,遷移成本較大,須要對全部圖片從新編碼,考慮到對舊版的支持,須要額外開闢空間存兩種格式的圖片。
解碼器設計
對於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、VP八、VP8L、ANMF、ICCP等,具體格式能夠在 Extended File Format 查看。基於Android系統的話,主要是以VP8X、VP八、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的指數,好比一、二、四、八、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