一、背景
在 App 使用過程中,頁面流暢性是僅次于 Crash 的影響用戶體驗的指標。在蘋果新推出的 13 Pro 和 Max 上支持了 ,最大刷新率達到 120Hz,這使得用戶對頁面流暢性導致的刷新率變化更為敏感。本文總結了雪球 iOS 客戶端在社區業務中 feed 流頁面和正文頁流暢性優化方面的工作,主要包括識別/測試卡頓工具使用和卡頓優化實踐兩方面內容。
二、工具定義卡頓
非高刷 的刷新率為 60Hz,也就是 VSync 信號的頻率,這要求一幀內容需要在 16.67ms 之內完成渲染。如果下一幀 B 的渲染時間超過了 16.67ms,即在 VSync 信號到來之后才完成渲染,那么當前幀 A 會滯留在屏幕上,幀 B 需要再等待一次 VSync 信號才能渲染給用戶。蘋果將幀錯過預期 VSync 信號稱之為卡頓( )[1]。
當用戶在頁面上操作時,比如上下滑動頁面或者頁面跳轉時,主要焦點集中在手勢的交互上,卡頓表現為用戶可感知的“抖動”。良好的交互體驗是提供流暢的響應速度,反之用戶將會感知到明顯的卡頓,卡頓會影響用戶體驗,甚至讓用戶失去對 App 的興趣。
識別卡頓
中的 模板可以檢查卡頓,下圖中 一欄展示了發生的卡頓,點擊其中一個卡頓,下面會顯示該卡頓類型,下圖中卡頓6的 Hitch Type 為 ,說明是 階段的耗時過長導致了該卡頓。
在左上角篩選框中輸入當前項目名字,并圈選中造成卡頓的 ,左下角切換為 ,通過 Time 工具查看該 中耗時的調用。
工具可以檢測到卡頓,并結合 Time 可以分析造成卡頓的調用耗時。Time 的原理是采集運行線程的調用棧,然后以統計學的方式匯總,所以 Time 展示的并不是實際代碼執行時間,只是棧在采樣統計中出現的時間,如下圖所示。所以 Time 只適用于粗粒度的分析。
火焰圖
如果需要精細化分析造成卡頓的耗時調用, 是一個可靠的選擇,但是手動插入大量 代碼統計函數耗時效率比較低。hook 能夠統計消息發送中的函數耗時,而在分析 CPU 耗時調用中,火焰圖是非常有效的工具,我們將兩者結合起來作為函數耗時細粒度分析的工具。
Trace Event [2] 定義了一種火焰圖數據格式,結合 hook 方案,在方法調用開始和結束的地方打點,即可生成火焰圖展示數據 [3]。下圖是一段測試代碼的火焰圖,函數內部的子函數調用表現為垂直方向向下的“火焰”,函數調用棧越深,則向下延伸越深。在火焰圖中,較平的底層"火焰"表示該函數可能存在性能問題。在測試代碼中,-1 和 - 函數內部線程睡眠了一段時間,在示意圖中表現為兩個較平的底部,也就是說優先優化這兩個函數獲得的收益最大。此外,控制火焰圖的函數調用深度限制和最低函數耗時限制兩個變量可以控制統計細化程度。
Hitch ratio
減少函數消耗調用并不能直接轉化為頁面流暢性指標,需要一個客觀的指標來評價優化工作效果, [1] 定義了 Hitch time 和 Hitch ratio,Hitch time 是幀延遲顯示的時間(以 ms 為單位),Hitch ratio 是頁面滑動或其他動畫過程中每秒內 Hitch time 的比率(以 ms/s 為單位)。蘋果采用 Hitch ratio 來量化頁面卡頓,并給出了 Hitch ratio 的建議數值,認為 Hitch ratio 低于 5ms/s 時用戶體驗比較好。
框架中的 UI 測試可以搜集 Hitch ratio,我們測試了 feed 流頁面和正文頁優化前后的 Hitch ratio 來評判優化效果。
三、優化實踐
通過上述工具分析,雪球社區 feed 流頁面和正文頁的卡頓主要集中在富文本的解析和繪制階段,以及隨著頁面樣式復雜性增加而積累的約束 ,下面主要針對這幾項進行優化。
富文本優化
雪球的社區業務主要是圍繞著富文本處理展開的,當富文本較為復雜時,解析和繪制均耗費大量時間,是導致頁面卡頓的最主要因素,下面主要從解析和繪制兩方面介紹富文本方面的優化。
富文本解析
上圖為原有富文本解析流程,對特殊 標簽處插入
標簽頁面加載完成后執行多個函數,以及去除 HTML 標簽括號等流程,需要多次遍歷富文本。而當富文本中包含大量 標簽或
標簽時,占用了大量主線程時間,造成了嚴重的卡頓。
一次性遍歷解析
我們使用 對現有富文本解析流程進行優化。 是開源的 iOS 富文本組件,可以一次性將 HTML+CSS 富文本轉化為 。 數據解析的流程如上圖所示:
富文本 字符串傳遞入 ilder,ilder 接收 的回調生成 DOM 樹,在 的回調中可以增加處理特殊 標簽的流程。生成的 DOM 樹種每個節點都是自定義的 ,通過 解析每個元素對應的樣式,這時每個 已經包含了節點的內容和樣式,最后從 生成 。
在解析富文本時,把解析過程暴露給使用者,通過回調函數告訴調用者當前解析到什么元素,讓使用者決定怎么處理。所以 是邊解析邊處理,只需要遍歷一次富文本,因此我們可以高效地完成在特殊 標簽插入
標簽等需求。
異步解析
ilder 創建了 3 個隊列:解析 html 的 ,生成 DOM 樹的 ,以及組裝 的 ,將解析過程分派到 3 個隊列,通過 阻塞等待所有任務完成后返回結果。所以解析過程是在非主線程上完成的,可以異步解析富文本進一步減少主線程上的時間消耗。
富文本繪制自定義文本異步繪制
為了滿足業務需求,feed 流頁面使用 來實現富文本繪制。當富文本內容比較復雜,尤其是包含表情較多時,在主線程繪制時會造成嚴重卡頓,因此采用異步繪制來進行優化。
iOS 中 負責處理事件傳遞,而繪制是通過 來完成的, 通過“-(void)”方法進行繪制。異步繪制就是通過繼承 并重寫“-(void)”方法,在內部將繪制任務放在非主線程來實現。 [4] 是一個實現了異步繪制的 ,當它需要顯示內容時,它會向 ,也就是 請求一個異步繪制的任務。
我們使用 對 feed 流富文本進行異步繪制, 為富文本顯示組件,實現了 定義的協議 gate,通過“-( *)”創建異步繪制任務,在異步繪制任務內部調用了 封裝的原富文本繪制流程。這種異步繪制改造,對原繪制流程侵入性較小,減少了測試回歸點并保證上線質量。
異步繪制優化
正文頁的評論區使用了 , 提供了 ly 屬性來控制是否開啟異步繪制。但是當 開啟異步繪制之后,評論區在加載下一頁 存在閃動 [5],這是由于 的 屬性默認為 YES,在 重寫的修改屬性函數內,如果 ly 和 同時為YES,則會先清理掉原有的內容。所以 時即使 cell 中 的文字內容不變,還是會先清理掉已有的 layer.,然后再異步繪制出新的 layer.,由于是異步的原因,中間會有一段時間 內容是清空的,導致表現為閃動。
//?YYLabel.m
-?(void)setTextColor:(UIColor?*)textColor?{
??if?(!textColor)?{
????textColor?=?[UIColor?blackColor];
??}
??if?(_textColor?==?textColor?||?[_textColor?isEqual:textColor])?return;
??_textColor?=?textColor;
??_innerText.yy_color?=?textColor;
??if?(_innerText.length?&&?!_ignoreCommonProperties)?{
????if?(_displaysAsynchronously?&&?_clearContentsBeforeAsynchronouslyDisplay)?{
??????[self?_clearContents];??//?清理掉原有內容
????}
????[self?_setLayoutNeedUpdate];
??}
}
所以在當前使用場景下,將 的 ly 設置為 YES 時,同時將 設置為 NO,避免出現閃動。
約束布局優化減少 次數
雪球 feed 流頁面的 cell 承載著很多業務,存在著大量的 代碼,是除了富文本繪制和解析之外最耗時的函數調用。改成 Frame 布局可以消除掉這部分耗時,但是涉及到大量測試回歸點,風險比較大。從另一個角度出發,在大多數情況下,feed 流 cell 顯示的 UI 組件是一樣的頁面加載完成后執行多個函數,并不需要每次設置數據時對各個子視圖進行 。
所以如下面代碼所示,對視圖組件 viewX,每次設置數據時檢查已綁定的歷史數據 和新數據model 的區別,判斷數據是否發生了需要更新 viewX 約束的變化,從而減少 的次數。
-?(void)setModel:(Model?*)model
{
????BOOL?needReLayoutViewX?=?[self?measureRelayoutViewNecessary:model];
????if?(needReLayoutViewX)?{
????????[self.viewX?mas_remakeConstraints:^(MASConstraintMaker?*make)?{
????????????//?設置約束
????????}];
????}
????_model?=?model;
}
-?(BOOL)measureRelayoutViewNecessary:(Model?*)model
{
????if?(model?&&?self.model?&&?'UI組件顯示變更不滿足')?{
????????return?NO;
????}
????return?YES;
}
其他優化減少視圖創建和移除的次數四、實驗結果和總結
通過 框架測試了 feed 流頁面和正文頁在模擬極端復雜數據下的 Hitch ratio。經過多個版本的優化,在 上,feed 流頁面的 Hitch ratio 由 16ms/s 降低到了 3.5ms/s;在 上,正文頁的 Hitch ratio 由優化前的 60ms/s 降低到了 5.5ms/s。
在雪球 iOS 社區頁面的流暢性優化實踐中,通過 和火焰圖可以定位 階段耗時較多的函數調用,并針對幾個頭部耗時函數調用進行了優化,并通過 Hitch ratio 指標量化了優化效果,在低端手機上實際使用體驗也得到了很大的提升。本文涉及到的優化點主要是 階段的耗時優化,關于渲染階段的優化可以更多地參考蘋果的技術分享 [6]。
五、引用
[1] 10077 - with
[2]Trace Event
[3]
[4]iOS 保持界面流暢的技巧
[5]
[6]WWDC and in the phase