文字佈局效能提升 60%,Inline Text 技術原理與實現 | Cube 技術解讀

語言: CN / TW / HK

支付寶客戶端有極強的動態化訴求,不論 iOS 還是 Android 平臺,重新分發軟體包在時間上、效率上都難以滿足產品運營的要求,所以客戶端動態化技術應運而生。

Cube 起源於 Native 頁面的動態化訴求,產品形態表現於Cube 卡片。隨著小程式的出現,Cube 融入了支付寶小程式技術棧,產品形態為輕量級的支付寶小程式解決方案(相對於使用瀏覽作為核心的 Web小程式)。作為一個輕量級引擎,Cube 小程式具有體積小、啟動快、記憶體佔用低的特點,我們使用自研渲染技術,支援 CSS 子集來實現這些特點。

與瀏覽器不同,Cube 小程式引擎的輸入是一個小程式 DSL(可以理解為小程式規範的語言) 構建後的產物(產物主要由一個 JS 檔案以及相關資源組成)輸出為使用者介面以及後續的互動(不斷的使用者輸入和 UI 輸出)。Cube 小程式不斷迭代支援樣式表,Inline Text 能夠做到在較小的包體積(主 so 只有 2.8 MB)的情況下,支援非常多的 CSS  樣式,並且佈局繪製與 Web 瀏覽器幾乎完全一致。

Cube 引擎渲染流程可以參考:

Inline Text

什麼是 Inline Text 呢?這要從 佈局引擎 開始介紹,佈局引擎又稱排版引擎,是一個軟體元件,負責獲取標記或者元素結合樣式產生相應的位置、大小以及層級等排版結果。這個排版結果可以被用於輸出到顯示器也可以被輸出到印表機。Cube 引擎使用了 2 個佈局引擎,卡片使用了 Yoga ,用於高效能佈局場景,小程式使用了 Flow Layout 。其中 Flow Layout 支援 Flow 佈局(流式佈局),具有代表性的樣式為 display:block  ,也就是 Web 中 div 等元素的預設佈局行為。

佈局引擎的一個重要工作就是文字的排版工作,而大多數非 Web 渲染的技術棧(包括老版本 Cube )使用的是平臺層的文字佈局物件,在佈局階段根據計算好的樣式構造平臺層文字物件,計算好寬高位置後再傳遞迴佈局引擎,完成佈局;後續繪製時使用建立好的平臺層物件進行繪製。

使用平臺層物件在佈局階段會產生一定程度的效能損耗,這種損耗主要包括兩部分:一是消耗在作業系統 API 對文字的佈局計算消耗;另外一種是佈局引擎與平臺層互動時產生的效能損耗。這種實現導致文字只能夠以一個一個矩形存在,難以靈活地 圖文混排 ,也難以將一段文字中的部分文字加顏色和樣式,這也是與 Inline 相關的樣式的含義,典型與 I n line 相關的樣式有: display:inline  、 display:inline-block  、 display:inline-flex  、 float:left 。以下2個圖片就是這種能力的典型代表。

除卻非常明顯的能力以外,符合相關 Web 標準一直是 Cube 小程式的技術目標之一,下面舉一個例子來說明。請看如下程式碼( HTML 程式碼 小程式可以換為 view image 標籤)

<div style="font-size: 60px;"><img src="http://xx"/></div>

<img> 是一個可替換元素。它的 display 屬性的預設值是 inline ,但是它的預設解析度是由被嵌入的圖片的原始寬高來確定的,使得它就像 inline-block 一樣,它放置的位置是由文字的基線決定的,這個例子中雖然沒有任何文字,但是 img 在排版佈局的時候是依賴文字的基線,所以 font-size 會影響 img 的位置,又由於 font-size 是繼承的,如果是動態下發的元素,不小心在祖先設定了大小不符合預期的字型就會影響佈局和繪製的結果。這個小例子說明了文字對於一個引擎在佈局時的影響,可見文字對於頁面排版佈局的影響是多方面的。作為一個佈局引擎, Flow Layout 需要完美的復刻這些行為,Cube 面向標準又邁出了一步。

總結以上幾點,Cube 團隊決定在 Cube 引擎上將文字相關能力增強,其中包括了對文字的寬高的測量與計算,排版和佈局,在增強 CSS 能力的同時又可以提升佈局效能,這些文字相關的能力(或者叫 Feature )統一被稱為 Inline Text ,還原前端開發者在使用 Cube 引擎時對文字等樣式的舊有使用習慣,大幅提升開發體驗。進而 Cube小程式能夠承載更多的小程式,進而實現更大的業務價值。

實現細節

對於 Inline Text 在 Cube 引擎中的實現分為兩部分:一部分是在 Flow Layout 去掉對接文字平臺層部分,增強為直接對文字的寬高進行排版測量,我們稱為 文字佈局 。另外一部分是繪製,本次實現依然使用平臺層繪製,只不過使用了更低一級別的API,減少作業系統的計算,儘可能直接呼叫繪製API,我們稱為 文字繪製 。為了更好的理解這兩部分的關係,以及實現細節,我們先了解一下 光柵化 和  文字自繪製

光柵化

我們都知道畫素的概念,計算機世界是離散的,每一條線,每個字繪製出來都需要到一個一個畫素點上,這個從向量圖形到畫素的過程叫做光柵化,就像柵格一樣。可以參考以下圖片:

文字自繪製

與文字自繪製相對應的是現有的渲染流程中對於文字的佈局以及渲染都使用了平臺層(OS)的 API,也就是 Cube 引擎下 platform 模組中的邏輯。佈局時建立對應的平臺層物件,通過平臺層計算出文字矩形的區域然後把結果傳遞迴佈局引擎,拿到結果完成佈局。後續繪製時,使用建立好的平臺層物件進行繪製。

文字自繪製是指通過佈局引擎直接測量文字的寬高,在合適的區域進行擺放,從而完成佈局;後續的繪製也直接通過字型的資訊(一些描述字形的資訊)直接進行光柵化。

Cube 在技術迭代的過程中先進行前半部分佈區域性分,後半部分隨著整個引擎的自繪製一起完成。所以現階段的方式是佈局引擎佈局完成後,使用平臺層 API 進行繪製,過程可以參考以下圖片:

文字佈局

為了將文字正確的放置到螢幕或者說一個矩形的正確位置上,我們分為兩步,第一步先把每一個字元的寬高計算出來,第二步進行佈局計算(擺放文字和其他東西例如圖片等)。需要注意的是真正的程式在執行的時候有時候會採用更高效的計算方式,未必需要完全測量每一個字元的寬高,例如等寬字型。

1 單個字元寬高計算

常見的 sans-serif,serif,monospace 被稱為通用字型族,他們叫通用字型族也就說他們代表了一系列字型族( FontFamily ),交由佈局引擎去作業系統中尋找合適的字型。字型族被稱為字型族說明一個字型有時候會提供多個版本,這是為了滿足不同的樣式需要,那麼選定了一個字型的其中一個版本後我們稱之為 Typeface ,Typeface 包含了很多文字,其中每一個單個文字(針對同一個Unicode字元)我們稱為 Glyph ,每一個  Glyph 中都包含了一個單個文字資訊,對於當代 TrueType 向量字型,單個文字資訊又由多條 貝塞爾曲 線 組成,我們稱之為向量圖形,根據字型的大小可以得到一個明確的大小的文字,再應用上一些樣式以後經過光柵化就可以得到單個字元的寬高。

下面是一個對比的小例子,對於同一個字型,字型的作者會一般來講會提供兩種一種是正常寬度的字型,另外一種是加粗的字型,這個加粗被稱為 font-weight ,那一個加粗一個不加粗就代表了兩種 Typeface ,根據之前的介紹那麼就會有兩套 貝塞爾曲線 來描述同一個 Unicode 字元,以下是不加粗和加粗字元Y的曲線。可以想像出來如果字型的作者針對傾斜(italic)單獨提供了一個 Typeface 也會像加粗一樣擁有單獨的曲線描述。

下面介紹一下 TextStyle 。 根據上文描述,一個字元最後確定大小除了  Typeface 以外還需要加上一些文字的樣式最後才能確定字元寬高,這些樣式我們統稱為 TextStyle 。瞭解了 Typeface 等概念以後,我們就會發現繪製一個字型除了 Typeface、文字大小還需要其他引數,比如: color font-size ,是否需要應用 FakeItalic,是否需要 FakeBold(稍後解釋),等很多引數。這些除去了 Typeface 以外的資訊被抽象為 TextStyle。在確定了 Typeface 以後還需要 TextStyle 資訊再經過光柵化後才能夠知道一個字形最終渲染的寬高(由於向量 Typeface 的特點,取寬高未必一定需要光柵化,經過特定的比例計算亦可)。

根據之前的描述,假如字型的作者沒有為字型設計 font-weight 對應的 Typeface,也沒有為 italic 設計Typeface,那麼在使用者指定相關樣式的時候應該怎麼辦,這時候 Fake 相關變換就上場了,他可以直接變換原有的字型的曲線來達到看起來變粗和變傾斜的了效果,這時候 FakeItalic 以及 FakeBold 就會應用到字型上,通過提前預製的變換函式,此時 TextStyle 就包含了 FakeItalic 和 FakeBold,反之則不包含。有時候是否應用 Fake 相關變換由佈局引擎自行決定。

為了計算文字相關能力我們引入了一些Library,主要為 Skia 和 Harfbuzz,其中 Skia 裁剪掉了所有繪製部分只保留針對文字的相關抽象,而 Harfbuzz 用於最終測量文字的寬高的庫,Harfbuzz 使用 Skia 提供的關於 Typeface 以及 Glyph 的介面。以下簡單介紹以下 SkTypeface 對於 Typeface 的抽象,以及字型在各個平臺初始化的細節。

在 Skia 庫中, SkTypeface 抽象了 Typeface 下面的細節實現庫 FreeType (Android), CoreText (iOS),提供了最基礎的功能,比如把 Unicode 轉換為字型中具體字形的 index(針對 Glyph 的抽象),比如:直接通過index 取或者計算單個文字輪廓的抽象,在 Android 通過 FreeType 庫讀取系統的字型檔案,在 IOS 上通過 CoreTextAPI 讀取相關 table 資料(Typeface 元資料)直接構造。

字型初始化細節同瀏覽器實現邏輯一致,在 Android 上使用 /etc/fonts.xml 中的字型資訊,老版本的Android 系統使用 /system/etc/fonts.xml 等類似的4個位置。其中定義了作業系統提供的字型,以及對應的 font-family font-weight font-style ,  language 初始化後準備好後續佈局時候找到合適的 Typeface。IOS 上直接使用 CoreText API獲取相關字型列表以及資訊。

至此我們就擁有了測量單個文字的能力,通過系統以及使用者輸入找到對應的 Typeface 配合 TextStyle ,最後使用Harfbuzz 將文字寬高計算出來。(這裡是為了方便理解,實際上可以一次測量多個文字的寬高)

2 佈局計算

首先是佈局樹和樣式表,這裡不過多介紹,通過 CSS 樣式加上相關 Element,構建出佈局樹,在進行佈局之前每一個元素都通過樣式表計算有了自己的 ComputedStyle ,block 元素,flex 元素按照其預設行為開始佈局。inline 元素在自己的 LayoutBlockFlow 中開始佈局,根據 ComputedStyle 中的 font-family 確定一個 FallbackList ,然後根據字型以及文字以及 white-space 等相關資訊邊測量文字的寬度,此處引入了 ICU ,這個庫用於分割不同的語言,以及確定是否需要斷句,標點符號是否放到下一行或者留在上一行,然後根據Unicode字元所在的區間,最後確定出 TextRun ,最後組成一個一個 LineBox ,用於後續繪製使用。此處描述較為精簡,後續會有單獨一篇文章展開講佈局過程。

文字繪製

1 對接平臺層繪製

由於 Inline Text 的特性,必須進行繪製鏈路的改造。整個渲染鏈路不過多介紹(請參考其他相關文章),Cube 繪製流程主要的結構是渲染樹(內部稱為 RenderTree ),這棵樹有很多節點,用於描述父子關係,其中文字節點在現階段屬於葉子節點, 在遞迴進行繪製的時候,原有由於文字是一個一個矩形,所以整個 Text 是使用同一個border 繪製流程以及背景繪製流程,由於 inline 特點,繪製到文字節點時,對於背景以及 border 的繪製流程需要根據每行來進行繪製,增加了一層迴圈,對於背景以及 border 折行等有諸多細節要處理,此處要對齊 Web 的繪製效果。後續如果要支援更復雜的文字特性,讓文字節點變為非葉子節點,還需要進一步增強繪製流程。

文字經過上一階段佈局計算以後,通過 LineBox 上的資訊針對每一個 Text 節點(span)產生了三個資料結構用於繪製:

class TextStyle {
int textColor;
float textSize;
int fontWeight;
int textDecoration;
int fontStyle;
TextShadow textShadow; // 用於描述TextShadow相關屬性
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;

展示以上虛擬碼是為了更好的理解對接平臺層繪製的細節, TextStyle 就是之前介紹過的除 Typeface 以外繪製需要用的其他資訊,由於 Element 的特點,同一個 Element 下的樣式是一致的,所以一個 Text 節點一份 TextStyle ,然後是一堆 TextLineInfo ,每一個代表一行的資料,每一行裡面有好多個 TextRun ,代表著每段對應的 Typeface 以及子串的始末,在佈局的時候已經提前進行平臺側 Typeface 的建立,等到繪製階段直接通過 typefaceId 拿到對應的物件繪製即可。其中 Android 平臺在佈局的時候直接使用 Android 的 API android.graphics.Typeface 建立,iOS 平臺由於 SkTypeface 是直接針對 CoreText 物件進行抽象,所以繪製的時候直接使用包裹的 CoreText 相關字型物件進行繪製即可。

包體積

根據 Cube 引擎自身的定位以及小程式的對於文字渲染能力的訴求,Inline Text 對 Skia、Harfbuzz、ICU、Freetype 等進行了深度優化和定製。Cube 在包體積上面做了大量的工作,針對引入的庫均做了大量的裁剪,例如 Skia 的繪製部分,去掉一些 Harfbuzz 中不必要的邏輯,針對 ICU 還定製實現了自己的部分,Inline Text 的實現(包含所有依賴庫)最終將 Cube 包體積的增加控制在 170kb 左右。

體驗與應用

豐富的 CSS 樣式與能力

在 Cube 引擎支援 Inline Text 以後,樣式 float display:inline-block 以及 flex 佈局中多個元素的基線對齊等細節都得到了完善,做到幾乎和瀏覽器引擎佈局結果完全一致。

以下是一些 Inline Text 能力表現,佈局引擎具有遞迴以及套娃特點,例子中也有體現:

  • 大段文字中的部分文字使用不一樣的樣式:

  • float:left

  • float:left “套娃”

  • CJK 文字 配合 word-break:keep-all , 無數個風暴折行不會斷開:

  • font-family Fallback 機制

當我們在 CSS 中寫如下程式碼的時候意味著:

<style> 
div {
font-family: alipay-number; serif;
}
</style>
<body>
<div>123中國456</div>
</body>

其中 123 , 456 使用 alipay-number, 但是 中國 使用系統的 serif 字型。原因是 alipay-number 字型沒有提供 中國 這兩個 Unicode 的 Glyph。

這個特性好多非 Web 的渲染引擎支援得都不完善,它涉及到 Typeface 的選擇規則以及繪製,Inline Text 完美復刻了 Web 渲染引擎的 Fallback 機制。

  • font-face 的支援

第三方字型是常見需求,很多業務都無法滿足於系統字型,一般來講可以內建在包裡,或者是通過 URL 獲取。瀏覽器的 CSS 在第三方字型準備好以後,通過 CSS 選擇器重新觸發匹配規則通過 FontSelector 重新選擇對應的 Typeface,完成重繪。

與瀏覽器不同,Cube 引擎的樣式匹配非常精簡,在第三方字型下載以後,清理掉之前的對應的 font-face 的文字快取,重新觸發佈局繪製達到同樣的效果。又由於樣式表的加持,以及 Inline Text 對於 ICU 的接入, font-icon 可以使用偽元素(content)加上私有 Unicode 的方式直接使用,和 Web 體驗完全一致。

效能提升

文字佈局的效能提升是由於使用了文字測量由佈局引擎進行排版以後,以前的與平臺層的 JNI 互動沒有了,平臺層物件的建立與佈局邏輯也沒有了,文字佈局的效能有了大幅提升:

應用場景

目前在優酷 OTT 上 90% 由搭建平臺產生的產物都預設開啟了 Inline Text,使用了相關能力,提升佈局的效能,由於協議頁面的需求,開發者無需再使用 Javascript 進行分詞更換顏色,直接使用引擎能力,可以參考以下頁面,此頁面應用 Inline Text 後頁面載入時間僅為原來的 1/3:

未來與展望

當前 Inline Text 是 1.0 版本實現,2.0 版本的規劃如下:

  • 根據目前 Cube 繪製模型,增強支援 textNest(可以理解為 span 標籤的巢狀);

  • 採用自繪製後端(比如:Skia),直接通過字型資訊光柵化達到全平臺一致性;

  • 支援豎向文字佈局;

  • 支援阿拉伯等雙向文字;

  • 支援更多的富文字特性;

  • 進一步優化超長文字佈局計算耗時。

附錄:Inline Text 支援的樣式

圖例:支援      部分支援       不支援     

關注我們,阿里前沿移動技術實踐&乾貨給你思考