文本布局性能提升 60%,Inline Text 技術(shù)原理與實現(xiàn)
支付寶客戶端有極強(qiáng)的動態(tài)化訴求,不論 iOS 還是 Android 平臺,重新分發(fā)軟件包在時間上、效率上都難以滿足產(chǎn)品運(yùn)營的要求,所以客戶端動態(tài)化技術(shù)應(yīng)運(yùn)而生。
Cube 起源于 Native 頁面的動態(tài)化訴求,產(chǎn)品形態(tài)表現(xiàn)于Cube 卡片。隨著小程序的出現(xiàn),Cube 融入了支付寶小程序技術(shù)棧,產(chǎn)品形態(tài)為輕量級的支付寶小程序解決方案(相對于使用瀏覽作為核心的 Web小程序)。作為一個輕量級引擎,Cube 小程序具有體積小、啟動快、內(nèi)存占用低的特點,我們使用自研渲染技術(shù),支持 CSS 子集來實現(xiàn)這些特點。
與瀏覽器不同,Cube 小程序引擎的輸入是一個小程序 DSL(可以理解為小程序規(guī)范的語言) 構(gòu)建后的產(chǎn)物(產(chǎn)物主要由一個 JS 文件以及相關(guān)資源組成)輸出為用戶界面以及后續(xù)的交互(不斷的用戶輸入和 UI 輸出)。Cube 小程序不斷迭代支持樣式表,Inline Text 能夠做到在較小的包體積(主 so 只有 2.8 MB)的情況下,支持非常多的 CSS 樣式,并且布局繪制與 Web 瀏覽器幾乎完全一致。
Inline Text
什么是 Inline Text 呢?這要從 布局引擎 開始介紹,布局引擎又稱排版引擎,是一個軟件組件,負(fù)責(zé)獲取標(biāo)記或者元素結(jié)合樣式產(chǎn)生相應(yīng)的位置、大小以及層級等排版結(jié)果。這個排版結(jié)果可以被用于輸出到顯示器也可以被輸出到打印機(jī)。Cube 引擎使用了 2 個布局引擎,卡片使用了 Yoga ,用于高性能布局場景,小程序使用了 Flow Layout 。其中 Flow Layout 支持 Flow 布局(流式布局),具有代表性的樣式為 display:block ,也就是 Web 中 div 等元素的默認(rèn)布局行為。
布局引擎的一個重要工作就是文字的排版工作,而大多數(shù)非 Web 渲染的技術(shù)棧(包括老版本 Cube )使用的是平臺層的文本布局對象,在布局階段根據(jù)計算好的樣式構(gòu)造平臺層文本對象,計算好寬高位置后再傳遞回布局引擎,完成布局;后續(xù)繪制時使用創(chuàng)建好的平臺層對象進(jìn)行繪制。
使用平臺層對象在布局階段會產(chǎn)生一定程度的性能損耗,這種損耗主要包括兩部分:一是消耗在操作系統(tǒng) API 對文字的布局計算消耗;另外一種是布局引擎與平臺層交互時產(chǎn)生的性能損耗。這種實現(xiàn)導(dǎo)致文本只能夠以一個一個矩形存在,難以靈活地 圖文混排 ,也難以將一段文字中的部分文字加顏色和樣式,這也是與 Inline 相關(guān)的樣式的含義,典型與 I n line 相關(guān)的樣式有: display:inline 、 display:inline-block 、 display:inline-flex 、 float:left 。以下2個圖片就是這種能力的典型代表。
除卻非常明顯的能力以外,符合相關(guān) Web 標(biāo)準(zhǔn)一直是 Cube 小程序的技術(shù)目標(biāo)之一,下面舉一個例子來說明。請看如下代碼( HTML 代碼 小程序可以換為 view 和 image 標(biāo)簽)
<div style="font-size: 60px;"><img src="http://xx"/></div>
<img> 是一個可替換元素。它的 display 屬性的默認(rèn)值是 inline ,但是它的默認(rèn)分辨率是由被嵌入的圖片的原始寬高來確定的,使得它就像 inline-block 一樣,它放置的位置是由文本的基線決定的,這個例子中雖然沒有任何文字,但是 img 在排版布局的時候是依賴文字的基線,所以 font-size 會影響 img 的位置,又由于 font-size 是繼承的,如果是動態(tài)下發(fā)的元素,不小心在祖先設(shè)置了大小不符合預(yù)期的字體就會影響布局和繪制的結(jié)果。這個小例子說明了文字對于一個引擎在布局時的影響,可見文字對于頁面排版布局的影響是多方面的。作為一個布局引擎, Flow Layout 需要完美的復(fù)刻這些行為,Cube 面向標(biāo)準(zhǔn)又邁出了一步。
總結(jié)以上幾點,Cube 團(tuán)隊決定在 Cube 引擎上將文本相關(guān)能力增強(qiáng),其中包括了對文字的寬高的測量與計算,排版和布局,在增強(qiáng) CSS 能力的同時又可以提升布局性能,這些文字相關(guān)的能力(或者叫 Feature )統(tǒng)一被稱為 Inline Text ,還原前端開發(fā)者在使用 Cube 引擎時對文字等樣式的舊有使用習(xí)慣,大幅提升開發(fā)體驗。進(jìn)而 Cube小程序能夠承載更多的小程序,進(jìn)而實現(xiàn)更大的業(yè)務(wù)價值。
實現(xiàn)細(xì)節(jié)
對于 Inline Text 在 Cube 引擎中的實現(xiàn)分為兩部分:一部分是在 Flow Layout 去掉對接文本平臺層部分,增強(qiáng)為直接對文字的寬高進(jìn)行排版測量,我們稱為 文本布局 。另外一部分是繪制,本次實現(xiàn)依然使用平臺層繪制,只不過使用了更低一級別的API,減少操作系統(tǒng)的計算,盡可能直接調(diào)用繪制API,我們稱為 文本繪制 。為了更好的理解這兩部分的關(guān)系,以及實現(xiàn)細(xì)節(jié),我們先了解一下 光柵化 和 文本自繪制 。
光柵化
我們都知道像素的概念,計算機(jī)世界是離散的,每一條線,每個字繪制出來都需要到一個一個像素點上,這個從矢量圖形到像素的過程叫做光柵化,就像柵格一樣??梢詤⒖家韵聢D片:
文本自繪制
與文本自繪制相對應(yīng)的是現(xiàn)有的渲染流程中對于文字的布局以及渲染都使用了平臺層(OS)的 API,也就是 Cube 引擎下 platform 模塊中的邏輯。布局時創(chuàng)建對應(yīng)的平臺層對象,通過平臺層計算出文本矩形的區(qū)域然后把結(jié)果傳遞回布局引擎,拿到結(jié)果完成布局。后續(xù)繪制時,使用創(chuàng)建好的平臺層對象進(jìn)行繪制。
文本自繪制是指通過布局引擎直接測量文本的寬高,在合適的區(qū)域進(jìn)行擺放,從而完成布局;后續(xù)的繪制也直接通過字體的信息(一些描述字形的信息)直接進(jìn)行光柵化。
Cube 在技術(shù)迭代的過程中先進(jìn)行前半部分布局部分,后半部分隨著整個引擎的自繪制一起完成。所以現(xiàn)階段的方式是布局引擎布局完成后,使用平臺層 API 進(jìn)行繪制,過程可以參考以下圖片:
文本布局
為了將文字正確的放置到屏幕或者說一個矩形的正確位置上,我們分為兩步,第一步先把每一個字符的寬高計算出來,第二步進(jìn)行布局計算(擺放文字和其他東西例如圖片等)。需要注意的是真正的程序在運(yùn)行的時候有時候會采用更高效的計算方式,未必需要完全測量每一個字符的寬高,例如等寬字體。
1 單個字符寬高計算
常見的 sans-serif,serif,monospace 被稱為通用字體族,他們叫通用字體族也就說他們代表了一系列字體族( FontFamily ),交由布局引擎去操作系統(tǒng)中尋找合適的字體。字體族被稱為字體族說明一個字體有時候會提供多個版本,這是為了滿足不同的樣式需要,那么選定了一個字體的其中一個版本后我們稱之為 Typeface ,Typeface 包含了很多文字,其中每一個單個文字(針對同一個Unicode字符)我們稱為 Glyph ,每一個 Glyph 中都包含了一個單個文字信息,對于當(dāng)代 TrueType 矢量字體,單個文字信息又由多條 貝塞爾曲 線 組成,我們稱之為矢量圖形,根據(jù)字體的大小可以得到一個明確的大小的文字,再應(yīng)用上一些樣式以后經(jīng)過光柵化就可以得到單個字符的寬高。
下面是一個對比的小例子,對于同一個字體,字體的作者會一般來講會提供兩種一種是正常寬度的字體,另外一種是加粗的字體,這個加粗被稱為 font-weight ,那一個加粗一個不加粗就代表了兩種 Typeface ,根據(jù)之前的介紹那么就會有兩套 貝塞爾曲線 來描述同一個 Unicode 字符,以下是不加粗和加粗字符Y的曲線??梢韵胂癯鰜砣绻煮w的作者針對傾斜(italic)單獨(dú)提供了一個 Typeface 也會像加粗一樣擁有單獨(dú)的曲線描述。
下面介紹一下 TextStyle 。 根據(jù)上文描述,一個字符最后確定大小除了 Typeface 以外還需要加上一些文本的樣式最后才能確定字符寬高,這些樣式我們統(tǒng)稱為 TextStyle 。了解了 Typeface 等概念以后,我們就會發(fā)現(xiàn)繪制一個字體除了 Typeface、文字大小還需要其他參數(shù),比如: color , font-size ,是否需要應(yīng)用 FakeItalic,是否需要 FakeBold(稍后解釋),等很多參數(shù)。這些除去了 Typeface 以外的信息被抽象為 TextStyle。在確定了 Typeface 以后還需要 TextStyle 信息再經(jīng)過光柵化后才能夠知道一個字形最終渲染的寬高(由于矢量 Typeface 的特點,取寬高未必一定需要光柵化,經(jīng)過特定的比例計算亦可)。
根據(jù)之前的描述,假如字體的作者沒有為字體設(shè)計 font-weight 對應(yīng)的 Typeface,也沒有為 italic 設(shè)計Typeface,那么在用戶指定相關(guān)樣式的時候應(yīng)該怎么辦,這時候 Fake 相關(guān)變換就上場了,他可以直接變換原有的字體的曲線來達(dá)到看起來變粗和變傾斜的了效果,這時候 FakeItalic 以及 FakeBold 就會應(yīng)用到字體上,通過提前預(yù)制的變換函數(shù),此時 TextStyle 就包含了 FakeItalic 和 FakeBold,反之則不包含。有時候是否應(yīng)用 Fake 相關(guān)變換由布局引擎自行決定。
為了計算文字相關(guān)能力我們引入了一些Library,主要為 Skia 和 Harfbuzz,其中 Skia 裁剪掉了所有繪制部分只保留針對文本的相關(guān)抽象,而 Harfbuzz 用于最終測量文本的寬高的庫,Harfbuzz 使用 Skia 提供的關(guān)于 Typeface 以及 Glyph 的接口。以下簡單介紹以下 SkTypeface 對于 Typeface 的抽象,以及字體在各個平臺初始化的細(xì)節(jié)。
在 Skia 庫中, SkTypeface 抽象了 Typeface 下面的細(xì)節(jié)實現(xiàn)庫 FreeType (Android), CoreText (iOS),提供了最基礎(chǔ)的功能,比如把 Unicode 轉(zhuǎn)換為字體中具體字形的 index(針對 Glyph 的抽象),比如:直接通過index 取或者計算單個文字輪廓的抽象,在 Android 通過 FreeType 庫讀取系統(tǒng)的字體文件,在 IOS 上通過 CoreTextAPI 讀取相關(guān) table 數(shù)據(jù)(Typeface 元數(shù)據(jù))直接構(gòu)造。
字體初始化細(xì)節(jié)同瀏覽器實現(xiàn)邏輯一致,在 Android 上使用 /etc/fonts.xml 中的字體信息,老版本的Android 系統(tǒng)使用 /system/etc/fonts.xml 等類似的4個位置。其中定義了操作系統(tǒng)提供的字體,以及對應(yīng)的 font-family 、 font-weight 、 font-style , language 初始化后準(zhǔn)備好后續(xù)布局時候找到合適的 Typeface。IOS 上直接使用 CoreText API獲取相關(guān)字體列表以及信息。
至此我們就擁有了測量單個文字的能力,通過系統(tǒng)以及用戶輸入找到對應(yīng)的 Typeface 配合 TextStyle ,最后使用Harfbuzz 將文字寬高計算出來。(這里是為了方便理解,實際上可以一次測量多個文字的寬高)
2 布局計算
首先是布局樹和樣式表,這里不過多介紹,通過 CSS 樣式加上相關(guān) Element,構(gòu)建出布局樹,在進(jìn)行布局之前每一個元素都通過樣式表計算有了自己的 ComputedStyle ,block 元素,flex 元素按照其默認(rèn)行為開始布局。inline 元素在自己的 LayoutBlockFlow 中開始布局,根據(jù) ComputedStyle 中的 font-family 確定一個 FallbackList ,然后根據(jù)字體以及文本以及 white-space 等相關(guān)信息邊測量文本的寬度,此處引入了 ICU ,這個庫用于分割不同的語言,以及確定是否需要斷句,標(biāo)點符號是否放到下一行或者留在上一行,然后根據(jù)Unicode字符所在的區(qū)間,最后確定出 TextRun ,最后組成一個一個 LineBox ,用于后續(xù)繪制使用。此處描述較為精簡,后續(xù)會有單獨(dú)一篇文章展開講布局過程。
文本繪制
1 對接平臺層繪制
由于 Inline Text 的特性,必須進(jìn)行繪制鏈路的改造。整個渲染鏈路不過多介紹(請參考其他相關(guān)文章),Cube 繪制流程主要的結(jié)構(gòu)是渲染樹(內(nèi)部稱為 RenderTree ),這棵樹有很多節(jié)點,用于描述父子關(guān)系,其中文本節(jié)點在現(xiàn)階段屬于葉子節(jié)點, 在遞歸進(jìn)行繪制的時候,原有由于文字是一個一個矩形,所以整個 Text 是使用同一個border 繪制流程以及背景繪制流程,由于 inline 特點,繪制到文本節(jié)點時,對于背景以及 border 的繪制流程需要根據(jù)每行來進(jìn)行繪制,增加了一層循環(huán),對于背景以及 border 折行等有諸多細(xì)節(jié)要處理,此處要對齊 Web 的繪制效果。后續(xù)如果要支持更復(fù)雜的文字特性,讓文本節(jié)點變?yōu)榉侨~子節(jié)點,還需要進(jìn)一步增強(qiáng)繪制流程。
文本經(jīng)過上一階段布局計算以后,通過 LineBox 上的信息針對每一個 Text 節(jié)點(span)產(chǎn)生了三個數(shù)據(jù)結(jié)構(gòu)用于繪制:
class TextStyle {
int textColor;
float textSize;
int fontWeight;
int textDecoration;
int fontStyle;
TextShadow textShadow; // 用于描述TextShadow相關(guān)屬性
float alpha;
Padding padding; // 包含left top right bottom
} style;
class TextRun {
int typefaceId;
int start;
int end;
float width;
};
class TextLine {
String text;
float originX;
float originY;
float ascent;
TextRun runs[];
} lines;
展示以上偽代碼是為了更好的理解對接平臺層繪制的細(xì)節(jié), TextStyle 就是之前介紹過的除 Typeface 以外繪制需要用的其他信息,由于 Element 的特點,同一個 Element 下的樣式是一致的,所以一個 Text 節(jié)點一份 TextStyle ,然后是一堆 TextLineInfo ,每一個代表一行的數(shù)據(jù),每一行里面有好多個 TextRun ,代表著每段對應(yīng)的 Typeface 以及子串的始末,在布局的時候已經(jīng)提前進(jìn)行平臺側(cè) Typeface 的創(chuàng)建,等到繪制階段直接通過 typefaceId 拿到對應(yīng)的對象繪制即可。其中 Android 平臺在布局的時候直接使用 Android 的 API android.graphics.Typeface 創(chuàng)建,iOS 平臺由于 SkTypeface 是直接針對 CoreText 對象進(jìn)行抽象,所以繪制的時候直接使用包裹的 CoreText 相關(guān)字體對象進(jìn)行繪制即可。
包體積
根據(jù) Cube 引擎自身的定位以及小程序的對于文本渲染能力的訴求,Inline Text 對 Skia、Harfbuzz、ICU、Freetype 等進(jìn)行了深度優(yōu)化和定制。Cube 在包體積上面做了大量的工作,針對引入的庫均做了大量的裁剪,例如 Skia 的繪制部分,去掉一些 Harfbuzz 中不必要的邏輯,針對 ICU 還定制實現(xiàn)了自己的部分,Inline Text 的實現(xiàn)(包含所有依賴庫)最終將 Cube 包體積的增加控制在 170kb 左右。
體驗與應(yīng)用
豐富的 CSS 樣式與能力
在 Cube 引擎支持 Inline Text 以后,樣式 float 、 display:inline-block 以及 flex 布局中多個元素的基線對齊等細(xì)節(jié)都得到了完善,做到幾乎和瀏覽器引擎布局結(jié)果完全一致。
以下是一些 Inline Text 能力表現(xiàn),布局引擎具有遞歸以及套娃特點,例子中也有體現(xiàn):
- 大段文本中的部分文本使用不一樣的樣式:
- float:left
- float:left “套娃”
- CJK 文本 配合 word-break:keep-all , 無數(shù)個風(fēng)暴折行不會斷開:
- font-family Fallback 機(jī)制
當(dāng)我們在 CSS 中寫如下代碼的時候意味著:
<style>
div {
font-family: alipay-number; serif;
}
</style>
<body>
<div>123中國456</div>
</body>
其中 123 , 456 使用 alipay-number, 但是 中國 使用系統(tǒng)的 serif 字體。原因是 alipay-number 字體沒有提供 中國 這兩個 Unicode 的 Glyph。
這個特性好多非 Web 的渲染引擎支持得都不完善,它涉及到 Typeface 的選擇規(guī)則以及繪制,Inline Text 完美復(fù)刻了 Web 渲染引擎的 Fallback 機(jī)制。
- font-face 的支持
第三方字體是常見需求,很多業(yè)務(wù)都無法滿足于系統(tǒng)字體,一般來講可以內(nèi)置在包里,或者是通過 URL 獲取。瀏覽器的 CSS 在第三方字體準(zhǔn)備好以后,通過 CSS 選擇器重新觸發(fā)匹配規(guī)則通過 FontSelector 重新選擇對應(yīng)的 Typeface,完成重繪。
與瀏覽器不同,Cube 引擎的樣式匹配非常精簡,在第三方字體下載以后,清理掉之前的對應(yīng)的 font-face 的文本緩存,重新觸發(fā)布局繪制達(dá)到同樣的效果。又由于樣式表的加持,以及 Inline Text 對于 ICU 的接入, font-icon 可以使用偽元素(content)加上私有 Unicode 的方式直接使用,和 Web 體驗完全一致。
性能提升
文本布局的性能提升是由于使用了文本測量由布局引擎進(jìn)行排版以后,以前的與平臺層的 JNI 交互沒有了,平臺層對象的創(chuàng)建與布局邏輯也沒有了,文本布局的性能有了大幅提升:
應(yīng)用場景
目前在優(yōu)酷 OTT 上 90% 由搭建平臺產(chǎn)生的產(chǎn)物都默認(rèn)開啟了 Inline Text,使用了相關(guān)能力,提升布局的性能,由于協(xié)議頁面的需求,開發(fā)者無需再使用 Javascript 進(jìn)行分詞更換顏色,直接使用引擎能力,可以參考以下頁面,此頁面應(yīng)用 Inline Text 后頁面加載時間僅為原來的 1/3:
未來與展望
當(dāng)前 Inline Text 是 1.0 版本實現(xiàn),2.0 版本的規(guī)劃如下:
- 根據(jù)目前 Cube 繪制模型,增強(qiáng)支持 textNest(可以理解為 span 標(biāo)簽的嵌套);
- 采用自繪制后端(比如:Skia),直接通過字體信息光柵化達(dá)到全平臺一致性;
- 支持豎向文本布局;
- 支持阿拉伯等雙向文本;
- 支持更多的富文本特性;
- 進(jìn)一步優(yōu)化超長文本布局計算耗時。
附錄:Inline Text 支持的樣式
圖例:支持 部分支持 不支持
? ?