Android 關於SVG向量圖支援
資源向量化
“清晰”和“體積”的矛盾與麻煩
面對android的各種dpi某事,想要所有裝置上的圖片都能有最清晰的效果,就意味著每種dpi模式都必須提供一份對應尺寸的資源,除非你不在乎安裝包的體積有多大,所以這顯然是不可能去做的。
在過去的幾年裡andorid從mdpi發展到xxxhdpi,每當微信想讓相同的圖片在更清晰的螢幕上顯示我們想要的效果時,我們總要重新提供一份體積更大的高清png並且刪掉可能不太多使用的小解析度圖片。
只保留一種解析度圖片的方法確實比所有dpi都來一份體積要小一點,然後只是用一份資源還需要承擔的負面效果則是當向其他dpi模式scale時,圖片也會變得模糊,並且你還要決定自己什麼時候該更換上更大解析度的圖片了。
向量圖SVG
柵格圖自身特點導致了高清資源同安裝包體積之間的矛盾。這方面向量圖存在明顯的優勢,它可以在表達清晰圖片的同時,不增加檔案體積。而且只要你不重新設計圖片,就用不著再去適配高dpi模式,向量圖什麼解析度都可以自適應。
我們認為SVG是比較合適的向量化資源方案,因為它相比目前android上的一些向量化方案更成熟、周邊工具支援更好。像VectorDrawable、ttf這樣的方案總有這不盡人意的地方,對於UI同學來說這兩個模式也不太好操作,不能輕易生成的資源會犧牲大家的工作效率是明顯得不償失的。(另外,VectorDrawable經過我們測試發現效能並不理想,這受限於他的實現方法。)
微信上的SVG
亟需解決的問題
想在微信裡用SVG,必然要面臨的兩個問題:
1) 效能問題
理論上講,SVG的效率可能會不如PNG好,這是因為它需要執行時的計算和對應平臺的渲染繪製。而且對於PNG來說的另一優勢是在開啟硬體加速的裝置上,繪製Bitmap一個非常快速的過程。可以想象,讓SVG不比PNG慢將是一件很有挑戰的事情。
2) 開發者的使用成本問題
SVG並不是android支援的標準資源格式,android資源框架自然不可能天然支援SVG的資源載入,而修改框架和提供支援很可能意味著會增加後面使用SVG的開發同學的學習成本和使用成本。因此必須要考慮如何即可以用SVG但又不增加開發負擔。
經過一番努力我們得到的結果
1)清晰度
兩張xxhdpi資源在OPPO R7Plus上的顯示結果。左邊SVG,右邊PNG。(公眾號的圖片壓縮。。。)
2)體積
在之前的一次灰度中我們替換了130個資源,這使得最終體積減小了211KB,平均每個減小1.6KB。後面微信會將所有可以向量化的資源全部替換成SVG,預計這將減小大約1.5MB左右的體積,對比目前壓縮後全部約7MB的png,這是個不小的節約。
3)效能
*經過10w使用者的灰度統計後得到的SVG和PNG平均時間,單位us
拆開來看:
SVG在載入的過程中得到非常大優勢,而Draw的時候因為沒有硬體渲染導致效能遠不如PNG。但通過在載入階段的大幅提升,讓SVG在整體耗時上贏了PNG。
為什麼我們可以將“載入”和“渲染”相加在一起來比較?
事實上,SVG渲染過程使用了Picture進行繪製。Picture並不支援硬體加速,因此必須要將View的LayerType設為Software,而這個操作的意義就是為View建立了一個Bitmap將Picture繪製其上,同時快取起來。所以,我們可以將“載入”和“渲染”放在一起進行比較,就是因為只有第一次的載入和渲染上我們同PNG是不同的。在這之後,一旦建立好了SoftwareLayer用的Bitmap,繪製過程就同PNG圖片一樣,可以用硬體渲染來畫Bitmap了。
所以,我們得到了比PNG快上70%的SVG向量化資源。
一些犧牲
由於我們實現方式的原因,啟動程序時每個SVG將額外消耗掉280us左右的時間。大概就是當我們替換完1000個資源後,我們的啟動時間可能會增加280ms。
這樣做是有原因的,一方面是因為我們必須這麼做來實現框架的無感知,另外也是為了使SVG的整體效率更高(因為生成了一些程式碼使得後面通過ResourceID免除了反射查詢一些類的時間)。而事實上即便我們把這個時間加回到每次載入平均值中,SVG也依舊領先於PNG的整體耗時。
4)好用的框架
與其說框架好用,不如說這個框架是不需要被感知到的。在android上用SVG,最理想的方式是隻要把drawable目錄的png直接換成SVG檔案就萬事大吉,這樣就最好了。而實際上我們也是這麼做的,只不過SVG是放在raw目錄下。
我們如何讓SVG比PNG更快
微信的SVG方案實際上是一個嘗試和逐步追求極致的過程,實現方案經過了幾個階段的演進。
一般來說SVG的實現方式是Parser + Render的組合,通過XML格式SVG的輸入解析,最終在介面上計算並繪製出圖形。
我們對已有的各種SVG實現方案進行對比,發現大部分無法在android上很好的應用起來,要麼實現不完整,要麼效能偏差,要麼過於複雜。
於是我們決定從一個叫svgandroid的可用SVG渲染庫入手。該庫是純java實現,這導致了其效能實在難以接受,一般耗時是PNG的十多倍。
經過我們分析發現SVG的整個流程中Parser部分耗時嚴重,例如在svgandroid上佔比超過80%。
因此基於首先優化Parser的思路,我們進行了第一個嘗試。
早期SVGProtoc方案
Parser部分的主要工作是解析xml並且將對應的節點和屬性變成一個特定的樹形中間結構。我們希望能找到辦法直接得到這個中間結構,這樣能省掉非常多的Parser時間。
經過嘗試,我們用protobuf構建了一個新的中間結構體,壓縮了各種欄位屬性的佔用空間,扁平化了一些資料結構,同時讓Render部分能支援我們這個結構。
意料之中的,使用的這種SVGProtoc的中間格式儲存下來的檔案,比xml小了非常多,甚至比之後的其他方案得到的體積都要小。
然而意料之外的是,效能的提升遠沒有達到我們的目標,百分之幾十的提升實在是有限,灰度的結果是平均值上扔落後PNG很多。Java層的賦值操作和物件建立操作消耗了異常多的時間。為此我們還曾更換過protobuf,使用flatbuff來實現,但依舊是C++表現優異而Java表現很差,沒能得到提升。
JNI渲染庫WeChatSVGLibrary
因為Java的效能問題,我們開始考慮WeChatSVGLibrary庫的開發,它是基於已有android庫的C++改寫,重新實現了parser部分的中間結構和部分邏輯。
使用native的結果是效能極大的提升,尤其是parser部分,變得在整個過程中只佔七分之一的時間,更多的時間則被Render佔用。
因為這個時候的Render是在Native完成的,在呼叫Skia API時,必須重新回到Dalvik,這個過程導致了額外的消耗。
最後的結果是WeChatSVGLibraryd耗時大致是PNG的1~5倍。
使用WeChatSVGLibrary後我們進行了幀率測試,結果是這樣的一般效率不會明顯影響到我們的列表幀率。
按理說這樣子已經夠了,但我們還沒有滿足。能不能做的更快一點?
最終方案WeChatSVGCode
前面講過SVG從檔案到螢幕上,一般要經過Parser和Render兩個階段,Parser通過把XML變成一個樹形中間物件,解析了數值和一些運算,Render通過遍歷這個樹形中間物件來達到渲染的目的。
如果換個角度思考,Render最後的繪製呼叫都會落在android的Skia API上,僅把API的呼叫記錄下來,去掉Parser和其他Render中執行時的各種運算等等,這樣渲染的速度將是最快的。而記錄之後的API呼叫最好的儲存方式就是生成可以直接繪製團的Java程式碼,於是我們實現WeChatSVGCode達到這個目的。
經過測試,我們生成的WeChatSVGCode程式碼,平均每個SVG在dex載入時增加150us的耗時,相對於微信計劃替換的1000個左右的資源,耗時是可以接受的。同時體積增長也不多,比SVG壓縮後的XML檔案還要小。
依賴WeChatSVGCode最低限度的繪製呼叫,讓我們實現比PNG更好的效能資料。
微信的向量化解決方案——WeChatSVGCode
為了實現完整的WeChatSVGCode向量化資源,我們需要“資源框架”和“編譯工具”。
資源框架
資源框架力圖解決SVG對於開發者便捷開發的使用問題上,我們遵循無感知的設計目標,替換SVG圖片而不增加開發者的開發成本,甚至不會感知到WeChatSVGCode這種特殊實現方式的存在。
我們的用法很簡單:
第一步,拿到.svg字尾的資原始檔(UI很容匯出這種圖片),放在raw目錄下而不是drawable目錄。
第二步,把 R.drawable.xxx 換成 R.raw.xxx;把 @drawable/xxx 換成 @raw/xxx。
此外,如果你想利用SVG一張圖多個尺寸的特性,可以通過SVGCompat.getDrawable傳入Scale比例的float值,就可以得到按比例放大縮小後的SVGDrawble了。SVGCompat和SVGDrawble這兩個是僅有的對外API類。
如何實現資源攔截
當打算實現資源框架時,我們發現以前的Override Resources loadDrawable函式的方式過時不能再用,並且也沒有能在所有android版本上進行java函式攔截的AOP方案。因此為了達到無感知的設計目標,我們只能另闢蹊徑。
通過看Resources原始碼我們發現sPreloadDrawable的陣列可以被利用。通過預先向裡面插入ConstantsState物件,從而在loadDrawable時命中並攔截掉後面的載入。(這也是我們為什麼要預載入的一個原因)
程式碼如下:
通過這樣的手段我們實現了資源的攔截。
編譯工具
WeChatSVGCode的效能提升實際上是將Parser和計算部分轉移到編譯階段,將最終生成的程式碼打進安裝包中。所以如何在各種編譯環境下實現真實SVG的渲染是最需要解決的問題。
我們想到的方法是將skia庫、android的Skia API介面以及WeChatSVGLibrary移植到目標編譯環境中,再通過程式碼生成邏輯將三個編好的庫整合在一起,按部就班的,讀取SVG檔案、渲染SVG、記錄API呼叫和最後輸出程式碼檔案。
目前我們支援Linux和MacOSX上的編譯環境
最後生成的程式碼
最後生成的程式碼大概是這樣的:
至此,我們就實現了比PNG更快更小更清晰的向量化“資源”,WeChatSVGCode。