1. 程式人生 > >Android 關於SVG向量圖支援

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)清晰度

640.png6344240.png

兩張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。