因?yàn)轫?xiàng)目中需要用到大量動(dòng)畫效果齐莲,前期嘗試過幾種方案痢站,比如GIF、幀動(dòng)畫选酗、lottie阵难、SVGA等格式的動(dòng)畫渲染方案,發(fā)現(xiàn)都存在各式各樣的問題芒填。比如:
1呜叫,GIF格式。5秒的動(dòng)畫殿衰,一張圖大小可能就會(huì)達(dá)到5-10M朱庆,然后UI那邊制作背景需要透明的效果做不了,打包下載壓縮包所需要更多的流量闷祥。
2娱颊,幀動(dòng)畫。簡(jiǎn)單說就是把GIF圖片給拆開為一張張圖蜀踏,比如一秒20幀的GIF圖被拆開為20張靜態(tài)圖,然后用程序代碼組成一幀一幀渲染效果動(dòng)畫掰吕,但是缺點(diǎn)也是很明顯果覆,做不到動(dòng)態(tài)更新,只能提前集成在本地資源中殖熟,這個(gè)方案也被否決掉局待。
3,第三方動(dòng)畫渲染庫。比如基于Airbnb開源的lottie庫和YY出品的SVGA解析庫钳榨,lottie解析格式是以后綴為.json文件舰罚,相比GIF文件,大小是小10倍以上薛耻,但是在CPU占用上卻奇高無比营罢。因?yàn)槲覀兊捻?xiàng)目針對(duì)沒有GPU能力的車機(jī)系統(tǒng),車機(jī)上的內(nèi)置芯片性能比目前主流手機(jī)性能差很多饼齿。同樣SVGA庫也是因?yàn)镃PU占用率高的問題被否決掉饲漾。
基于目前已有的硬件條件,可能最希望是升級(jí)硬件設(shè)備缕溉,那樣的話無論是對(duì)于UI和開發(fā)來說考传,都是皆大歡喜,UI可基于lottie做炫酷的動(dòng)效证鸥,而開發(fā)也不會(huì)因?yàn)樾阅軉栴}而進(jìn)行各種評(píng)估僚楞。但現(xiàn)實(shí)往往是殘酷的,只能基于目前車機(jī)條件進(jìn)行開發(fā)枉层,那么作為開發(fā)人員泉褐,當(dāng)然是得想各種方法去滿足產(chǎn)品需求了,那就把目光轉(zhuǎn)移返干,后來轉(zhuǎn)移到一種叫做「WebP」格式的圖片兴枯。
基于WebP格式做出來的圖片,UI那邊可以做透明的背景動(dòng)效矩欠,我們開發(fā)這邊測(cè)了下性能财剖,發(fā)現(xiàn)CPU和內(nèi)存占用也滿足產(chǎn)品測(cè)的要求,正好折中是我們想要選擇的解決方案癌淮。既然之前是沒怎么聽過躺坟,那么就有必須去了解下「WebP」是什么東西了。
介紹
對(duì)于之前沒接觸過的知識(shí)點(diǎn)乳蓄,首先第一步是打Google咪橙,輸入webp這四個(gè)字母,Google搜索出來的首頁就會(huì)告訴你這是什么了虚倒,也就是What的定義美侦。引用「WebP」官網(wǎng)定義的一句話:
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.
進(jìn)一步說,「WebP」是一種新的圖片格式魂奥,可提供出色的無損和有損壓縮菠剩,對(duì)于Web開發(fā)來說,可以創(chuàng)建更小和更豐富的圖像耻煤。根據(jù)官網(wǎng)測(cè)試具壮,WebP無損壓縮的圖片比PNG格式圖片准颓,文件大小上少 26%,WebP有損圖片在同樣 SSIM 質(zhì)量指標(biāo)上比JPEG格式圖片少25~34%棺妓,SSIM是一種衡量?jī)蓮垟?shù)字影像相似的指標(biāo)攘已。
官網(wǎng)給出有損壓縮測(cè)試方法:
- 將PNG圖片設(shè)置不同的壓縮參數(shù)壓縮成JPEG圖片,記錄壓縮后的對(duì)比的SSIM怜跑。
- 將同一張PNG圖片壓縮成WebP圖片样勃,壓縮的WebP圖片的SSIM指標(biāo)必須比1中記錄的SSIM高。
對(duì)比圖如下:
同樣WebP與JPG格式進(jìn)行加載時(shí)間對(duì)比妆艘,可以發(fā)現(xiàn)WebP優(yōu)秀很多彤灶。
從圖中可以看到大小和圖片加載速度上比jpg格式優(yōu)勝很多,對(duì)于web頁面來說批旺,文件體積減少了幌陕,加載時(shí)間縮短了,那么頁面的渲染速度加快了汽煮,特別是圖片越來越多的情況下搏熄,能對(duì)性能進(jìn)行提升和帶寬節(jié)省。
對(duì)比GIF
對(duì)于項(xiàng)目中要用到各種動(dòng)效圖片暇赤,大部分人首先想到是GIF格式的圖片心例,那么相比GIF,WebP有什么優(yōu)勢(shì)呢鞋囊?
- 支持有損和無損壓縮止后,并且可以合并有損和無損圖片幀。
- 體積會(huì)更小溜腐,這點(diǎn)是很關(guān)鍵译株,親測(cè)下來有損的圖片可以減少60%的體積,而無損可以減少20%的體積挺益。
- 與GIF的8位顏色和1位alpha相比歉糜,支持24-bitRGB顏色和Alpha通道,對(duì)于UI設(shè)計(jì)來說更友好和更少限制望众,做出更炫酷的動(dòng)效匪补。
- 有動(dòng)畫、關(guān)鍵幀烂翰、metadate夯缺、顏色配置文件等數(shù)據(jù),有損壓縮是調(diào)節(jié)的甘耿。
WebP一些劣勢(shì)
- WebP的直線解碼比GIF占用更多的CPU資源踊兜,有損WebP的解碼時(shí)間是GIF的2.2倍,而無損WebP的解碼時(shí)間是GIF的1.5倍棵里,因此在客戶端來說润文,對(duì)比GIF格式,WebP解碼需要更多CPU計(jì)算資源殿怜。
- 相比GIF來說典蝌,使用的普遍性不高,相關(guān)資料比較少头谜,需要去解讀官方文檔骏掀。
- 各個(gè)端支持情況不一,需要自己寫個(gè)解釋器去渲染W(wǎng)ebP格式的圖片柱告。
- 如果要遷移的話截驮,遷移成本較大,需要對(duì)所有圖片重新編碼际度,考慮到對(duì)舊版的支持葵袭,需要額外開辟空間存兩種格式的圖片。
解碼器設(shè)計(jì)
對(duì)于Android系統(tǒng)來說乖菱,WebP 在Android 4.0及以上原生支持坡锡,對(duì)于4.0以下可以使用官方提供提供的編解碼庫,但現(xiàn)在主流的手機(jī)上窒所,Android 4.0以下已經(jīng)可以忽略不計(jì)了鹉勒,反而對(duì)于在IOT設(shè)備上,則有可能存在低版本吵取,因此對(duì)于此類開發(fā)項(xiàng)目禽额,如果選擇WebP格式則需要事先評(píng)估下了。
從官網(wǎng)的描述來看皮官,WebP是使用VP8關(guān)鍵幀編碼以有損方式進(jìn)行圖像數(shù)據(jù)壓縮脯倒,也就是說如果要支持解碼的話,我們需要對(duì)這個(gè)VP8算法進(jìn)行解碼臣疑。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文件的基本元素是一個(gè)塊郁岩。它包括了Chunk FourCC 、 Chunk Size缺狠、 Chunk Payload三部分 问慎。其中Chunk FourCC是一個(gè)32位ASCII編碼的塊文件的唯一標(biāo)識(shí)。 Chunk Size則代表該塊文件的大小挤茄, Chunk Payload則是數(shù)據(jù)有效承載如叼,如果“塊大小”為奇數(shù),則添加一個(gè)填充字節(jié)(應(yīng)為0)穷劈。
我們常用ChunkHeader('ABCD')來描述RIFF文件笼恰,這里ABCD則是FourCC單個(gè)塊踊沸,則該元素大小為8個(gè)字節(jié)。
那么接下去看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開始的文件大小谬返,以字節(jié)為單位之斯。此字段的最大值為2 ^ 32減去10個(gè)字節(jié),因此遣铝,整個(gè)文件的大小最多為4GiB減去2個(gè)字節(jié)吊圾。
3,'WEBP': 32 bits:ASCII字符“ W”翰蠢,“ E”项乒,“ B”,“ P”梁沧。
那么對(duì)于包含多幀動(dòng)畫為主的圖片檀何,它的頭文件如何呢,具體如下:
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:畫布的默認(rèn)背景顏色廷支,以[B频鉴,G,R恋拍,Alpha]字節(jié)順序排列垛孔,此顏色可用于填充框架周圍畫布上未使用的空間,以及第一幀的透明像素施敢。處置方法為1時(shí)也使用背景色周荐。
Loop Count:循環(huán)播放動(dòng)畫的次數(shù)。 0表示無限循環(huán)僵娃。
除了這幾個(gè)文件頭格式之外概作,還有其他幾個(gè)文件頭格式,比如VP8X默怨、VP8讯榕、VP8L、ANMF、ICCP等愚屁,具體格式可以在 Extended File Format 查看济竹。基于Android系統(tǒng)的話霎槐,主要是以VP8X规辱、VP8、VP8算法解碼栽燕,對(duì)塊文件進(jìn)行解析,代碼如下:
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;
}
在對(duì)算法解碼之前改淑,需要把WebP格式文件加載到內(nèi)存中去碍岔,此時(shí)就需要用到Reader這個(gè)讀寫器,我們從官網(wǎng)的定義可以看到朵夏,讀取WebP文件的代碼稱為讀取器蔼啦,而寫入WebP文件的代碼稱為寫入器。那么這個(gè)涉及到文件I/O的讀寫仰猖,數(shù)據(jù)流的讀取和寫入問題捏肢。
具體定義讀取器的接口代碼如下:
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;
}
具體文件讀取可以從文件、字節(jié)流等地方獲取饥侵。讀取數(shù)據(jù)之后鸵赫,就需要對(duì)數(shù)據(jù)進(jìn)行解析,我們知道如果是動(dòng)畫效果的圖片躏升,本質(zhì)是以幀集合組成的內(nèi)容辩棒,無論是GIF圖支持WebP格式的動(dòng)畫圖,本質(zhì)也是一幀一幀進(jìn)行渲染膨疏。好比我們看到的Android渲染視圖是以一秒60幀一睁,所以我們看到如果每幀超過16ms的話,就容易引起卡頓的原因佃却。
因此對(duì)于幀渲染接口的定義就顯得很關(guān)鍵了者吁,具體接口定義如下:
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);
}
一幀可以理解為一張靜態(tài)圖,如果有20幀組成的動(dòng)畫饲帅,可以理解成有20張圖片按照連貫順序一張張過一遍复凳,那就形成了有動(dòng)畫的效果。所以我們要解析動(dòng)畫灶泵,本質(zhì)是還是去解析每張靜態(tài)圖染坯,通過每張圖的繪制,把整個(gè)動(dòng)畫給繪制出來丘逸。這一張圖片就包括寬度单鹿、高度、在屏幕上的橫向深纲、縱向坐標(biāo)仲锄、運(yùn)行時(shí)間等劲妙,但最關(guān)鍵還是需要把圖會(huì)繪制出來,這里面就是draw方法的重寫儒喊。
關(guān)于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哈垢,分別用于支持從文件系統(tǒng)、資源扛拨、輸入流以及字節(jié)數(shù)組中加載出一個(gè)Bitmap對(duì)象耘分,其中decodeFile和decodeResource又間接調(diào)用了decodeStream方法,這四類方法最終是在Android的底層實(shí)現(xiàn)的绑警,對(duì)應(yīng)著BitmapFactory類的幾個(gè)native方法求泰。
那么該高效地加載Bitmap呢名挥,其實(shí)核心思也很簡(jiǎn)單武通,就是采用BitmapFactory.Options來加載所需尺寸的圖片。主要是用到它的inSampleSize參數(shù)将塑,即采樣率北启。當(dāng)inSampleSize為1時(shí)枉氮,采樣后的圖片大小為圖片的原始大小,當(dāng)inSampleSize大于1時(shí)暖庄,比如為2聊替,那么采樣后的圖片其寬/寬均為原圖大小的1/2,而像素?cái)?shù)為原圖的1/4培廓,其占有的內(nèi)存大小也為原圖的1/4惹悄。從最新官方文檔中指出,inSampleSize的取值應(yīng)該是2的指數(shù)肩钠,比如1泣港、2、4价匠、8当纱、16等等。
通過采樣率即可有效地加載圖片踩窖,那么到底如何獲取采樣率呢坡氯,獲取采樣率也很簡(jiǎn)單,循序如下流程:
- 將BitmapFactory.Options的inJustDecodeBounds參數(shù)設(shè)為True并加載圖片
- 從BitmapFactory.Options中取出圖片的原始寬高信息,他們對(duì)應(yīng)于outWidth和outHeight參數(shù)
- 根據(jù)采樣率的規(guī)則并結(jié)合目標(biāo)View的所需大小計(jì)算出采樣率inSampleSize
- 將BitmapFactory.Options的inJustDecodeBounds參數(shù)設(shè)為False箫柳,然后重新加載圖片手形。
你看設(shè)計(jì)到最后,本質(zhì)還是把由很多幀組成的動(dòng)畫格式悯恍,拆分到具體每一幀的圖片库糠,針對(duì)圖片進(jìn)行圖片幀繪制,進(jìn)而把動(dòng)畫的效果給渲染出來涮毫。
總結(jié)
總的來說瞬欧,不同圖片顯示選擇是根據(jù)具體業(yè)務(wù)場(chǎng)景來做評(píng)估,像我們最近在開發(fā)的項(xiàng)目中罢防,主要是以圖片形象為主艘虎,那么就會(huì)過多關(guān)注有關(guān)圖片的CPU使用率和內(nèi)存占用率的比例。如果發(fā)現(xiàn)常規(guī)的圖片格式不滿足需求篙梢,那么就是需要調(diào)研和尋找不同的解決方案。這本來就是沒有固定的一套解決方案美旧,只有相對(duì)合適的解決方案渤滞,因此,無論是從UI角度榴嗅,還是從開發(fā)角度妄呕,甚至是產(chǎn)品角度,都得尋得整個(gè)產(chǎn)品中平衡度嗽测,尋找合適點(diǎn)绪励,是能滿足各方需求,進(jìn)而打造更完善的產(chǎn)品應(yīng)用唠粥。
參考地址:
1疏魏,https://developers.google.cn/speed/webp
2,https://developers.google.cn/speed/webp/docs/riff_container