新彈幕引擎架構設計,提高業務開發效率

一、背景簡介

優酷舊有的彈幕功能經過長期業務迭代,添加各種需求,代碼耦合越來越嚴重,一個 View 顯示裏用 if else 兼容了各種各樣的顯示樣式;爲了同時使用在 iPhone 版和 iPad 版 APP 裏,又加了不少機型判斷。最終導致新增功能越來越困難,於是重新設計一套靈活好用的彈幕引擎框架提上日程。

本文主要分享優酷 APP 目前正在使用的新版彈幕引擎庫,與播放器解耦、可獨立使用,無第三方依賴,打包後大小僅有 60 多 KB。

二、定製能力

1)支持定製彈幕子 View 顯示;

2)支持定製各項參數;

3)支持屏蔽某些彈幕;

4)可由業務實現新的彈幕顯示形式,不侷限於默認的從右向左滾動、置頂、置底。

三、架構設計簡介

彈幕引擎庫的主要作用是顯示彈幕的滾動、或者置頂置底然後淡出,它需要知道一批數據、某條數據使用某種子 View 渲染以及使用某種排版形式,同時還需要一些必要的參數,如行高。

首先定義幾個名詞:

排版類型:目前支持從右向左滾動、置頂、置底,業務可自己實現其它特殊的排版;

UI 風格:指單條數據表現出來的 ui 樣式,庫內置一個簡單的 Label 顯示,業務可自己實現其它的 ui 樣式,如左邊一個圖片右邊文字。

爲了提供較強的定製能力,滿足各種需求,引擎庫使用了插件註冊方式提供各種能力,包括默認的排版類型和 UI 風格也是通過插件註冊的,下面是整個庫的架構設計圖:

引擎庫對外導出的內容包含圖裏上層的 DanmakuView、Data Driver,及下層的 DataPlugin、 Setting Plugin、Layout Plugin、Ui Plugin,中間的一層業務不可見。

DanmakuView 是彈幕子 View 顯示的父容器,業務可把它放在任何一個業務頁面,並給它提供合適的 frame,它提供了註冊插件,開始暫停動畫、直接塞數據、獲取當前顯示的彈幕 View、數據、重新佈局等方法。

整個流程是業務通過左側數據驅動或者直接通過 DanmakuView 給到數據後,數據被分類緩存在 Data Cache Manager 裏,內部佈局 Engine 開始啓動內部定時器,從 Cache 拿數據,有了數據後通過 ViewPool 取到一個新建的或者之前用過被回收的相同類型子 View ,然後從 SettingPlugin 拿到當前需要的參數,一併交給子 View 去計算寬高,然後再嘗試調用 LayoutPlugin 的方法去排版,如果此時 DanmakuView 容器裏能排版,則通知子 View 去渲染更新內部元素,如果不能排版則等待下一個定時器事件、或直到有新一批數據給到後被丟棄。

四、插件簡介

所有插件創建後都通過 YKDanmakuView 的 registerPlugin 方法進行註冊。

所有插件的屬性和方法都帶有 ykdm_前綴避免與業務的屬性或方法衝突。

  1. UI 風格插件

Ui Plugin: 提供渲染數據的子 View,ui 插件要實現 YKDanmakuUiStylePlugin 協議,通過 uiType 表明自身負責創建哪種數據的彈幕子 View ,同時子 View 類要實現 YKDanmakuItemViewProtocol 協議,通過 viewSize 方法告知引擎當前子 View 的寬高,引擎確認剩餘空間是否夠排版,如可以則調用 renderView 方法由插件負責顯示子 view 內部元素, viewSize 方法內部不建議去真正的刷新內部 view 顯示,因爲此時可能因爲排版不下而放棄,多餘的刷新顯示動作浪費性能。

上面截圖中可以實現多個 ui 插件用來表現多種不同的風格,有帶圖的,有純文本的,當然相似的也可以用一種 ui 插件,然後 view 內部根據數據顯示隱藏部分子 view。

  1. 排版插件

Layout Plugin: 當 View 創建完成後,佈局插件用來實現具體的排版類型,庫裏已經默認實現了從右向左滾動、置頂、置底,插件需實現 YKDanmakuLayoutPlugin 協議,通過 layoutType 區分是哪種排版類型。

  1. 參數設置插件

Setting Plugin: 用來提供引擎庫必要的一些基本參數以及某條彈幕數據是否需要過濾掉不顯示,需實現 YKDanmakuSettingPlugin 協議,基本參數通過 YKDanmakuSettingParam 對象告知引擎庫,包含行高、顯示幾行、滾動持續時間、置頂置底的淡出動畫時間等參數。

  1. 數據相關插件

顯示子 View、排版、參數都有了,還差一個不可缺少的內容,數據從哪兒來?

1)數據基本格式

數據會通過數組提供給彈幕引擎,數據基類都需實現 YKDanmakuItemInfoProtocol 協議中要求的幾個方法:

a)layoutType 提供佈局類型:告知引擎使用哪種 LayoutPlugin 排版,內置 kYKDanmakuLayoutTypeScroll、kYKDanmakuLayoutTypeTop、kYKDanmakuLayoutTypeBottom,對應着從右向左滾動、置頂、置底;

b)uiType 提供 UI 風格類型,告知引擎使用哪種 UIPlugin 去創建 View 及計算大小、渲染;

c)forceShow 此數據是否是需要強制顯示,用在 vip 用戶、自己發的彈幕等高優先級的情況,當有下一批數據來更新替換上一批數據時,上一批數據中如存在 forceShow 爲 True 的數據,那麼即使排版不下此數據也會強制排版(可能會與之前排版的彈幕部分重疊);

d)contentText 彈幕的文本內容,當未提供 UIPlugin 時,默認使用此屬性顯示一個文本 View。

2)數據提供形式

數據的提供形式可以分爲兩大類,一種是一次性的簡單給予,一種是持續不斷的給予或者有定製需求。

a)如果業務是在圖文如漫畫業務上把用戶評論作爲彈幕顯示,數據一般是一次性給的,那麼可以直接通過 DanmakuView 的 setDataArray 方法將彈幕數據提供給引擎,引擎按序用完數據結束顯示;

b)除上面這種簡單情況,如果數據需要一批批的給,比如像視頻播放器一樣每秒更新數據,或者數據更新不是替換而是追加,或者數據要循環使用,都可以用下面這種用法:

對於第二種稍複雜的情況,需創建一個 Data Driver(繼承自:YKDanmakuBaseDataDriver)用來驅動數據更新,配合 DataPlugin 提供數據,DataPlugin 需實現協議 YKDanmakuDataPlugin。

Data Driver 可以接受播放器的通知(比如每秒一次播放進度更新)、或者內置定時器、或從實時通道接收原始數據、或一次性驅動,總之當需要更新數據時調用基類的 triggerFetchDataWithParams 方法帶上業務自己規則的參數,此時 DataPlugin 的 triggerFetchDataWithParams 實現會被調用,參數也會傳遞過來, DataPlugin 根據參數的不同通過不同的方式取得解析好的數據,如訪問後端 api、讀取離線緩存、使用參數中提供的原始 json 數據等等,然後通過 callback 同步或者異步返回給彈幕引擎。

DataPlugin 其中幾個方法的作用:

a)dataDriver: 引擎內部關聯 DataEngine 使用,通過這個方法返回 Data Driver 實例即可;

b) dataRefreshMode: 可選,表示此數據更新是清除舊數據再添加(YKDmDataRefreshMode_Replace)、或是追加(YKDmDataRefreshMode_Append),默認 Replace;

c)recyclableData:可選,表示提供的數據使用完成後是否繼續從頭開始使用,比如漫畫或者圖文型彈幕可能提供一批數據後,後續反覆使用。默認 NO。

五、交互需求建議

如上圖點擊後被點擊的彈幕暫停,同時顯示一個小面板(小面板業務創建並添加到父 View 容器中)。

1)如果彈幕子 View 需要點擊交互,那麼此子 View 需要重載 hitTest:withEvent:方法,不然無法響應點擊;

- (UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event {
   point = [self convertPoint:point toView:self.superview];
   if ([self.layer.presentationLayer hitTest:point] != nil) {
      return self;
   }
   return nil;
}

2)業務可在 UI Plugin 裏 constructView 方法或者 View 創建 init 代碼裏註冊各種點擊事件,當用戶點擊後進行需要的操作,如喚起輸入框回覆此彈幕,或者顯示一個小面板;

3)YKDanmakuItemViewProtocol 裏 renderView 方法裏 YKDanmakuLayoutParam 參數的有屬性表明是否是重新使用此子 view 顯示一個新數據,當更新參數或者父 View frame 變化導致當前在屏幕上的子 View 重新刷新時此參數是 false,此參數可用來做曝光埋點等業務。同時可選方法 notifyShowComplete 表示此子 view 顯示完成即將被回收,可以用來清除 text、image 或者配合計算某種 text 顯示了多少次,做些彩蛋之類的需求。

六、性能優化建議

1)Setting Plugin 的 settingParamByLayoutParam 方法調用很頻繁,實現需儘可能高效,比如計算一次後緩存起來供下次使用;

2)當 YKDanmakuView 的 frame 改變後,會自動對當前已經顯示的彈幕子 View 重新排版和刷新,比如一些業務橫豎屏不同狀態下子 View 文字大小、滾動速度會有不同,此時業務無需再調用 YKDanmakuView 裏的 reloadSetting 方法重複刷新;

3)參數設置中的滾動時間變化後後續排版會自動使用新參數,無需調用 YKDanmakuView 裏的 reloadSetting 方法;

4)在 frame 不變的情況下,如要單獨更新文字顏色、字體大小、顯示行數、過濾彈幕等可調用 reloadSetting 方法,爲提搞性能,刷新標記參數可選擇以下其一:

a)kYKDanmakuLayoutFlagColor 只重繪顏色,不影響大小,此模式跳過大小計算邏輯,提高性能;

b)kYKDanmakuLayoutFlagSize 更新了字體大小,一般要影響子 view 的整體佈局,但不更新顏色;

c)kYKDanmakuLayoutFlagFilter 通過 settingPlugin 的 needFilter 方法控制整個 view 顯示或隱藏、或者調整了整體顯示行數,只隱藏或者顯示子 View,不重新計算大小及刷新顯示;

d)kYKDanmakuLayoutFlagAll 需要完整計算位置及渲染邏輯。

5)與上述對應,子 View 的重新渲染代碼建議這樣寫:

- (void)ykdm_renderViewWithLayoutParam:layoutParam...... {
   if ([layoutParam needChangeAll]) {
      //對內容修改,如label 賦值,對imageView 設置圖片等等
   }

   if ([layoutParam needChangeColor]) {
      //需要更新顏色,可設置文字顏色,各種背景色等等
   }

   if ([layoutParam needChangeSize]) {
      //需要調整view 各項大小,如字號更新,圖片大小更新
   }

   if (layoutParam.firstLayout) {
      //用新數據進行渲染,埋點等
   }
}

作者 | 阿里文娛高級無線開發工程師 趨勢

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章