播放器簡介
播放器是西瓜視頻等視頻類 App 最主要的業務場景,也是最主要的流量入口,其承載包括下層基礎播放,上層的各種播放業務:狀態欄、彈幕、音量、亮度、評論、點贊、進度、倍速、清晰度、選集、合集、商業化等。
西瓜對整個業務播放器做了整體抽象,提供了一套可插拔,可復用的播放器業務框架,包括:視頻播放、播控交互、業務拓展。
本文播放器是指業務播放器,主要包括視頻播放、播控交互、播放業務拓展,本播放器旨在提供一套完整的架構來包容播放器所有業務,實現播放業務可插拔。
現狀分析
現有播放架構
- 播放器架構圖
- 現有架構存在的問題:
- 播放架構以 Redux 為核心,播放器與業務播放器強耦合,也會存在 Redux 套 Redux 的現況。
- 業務播放器使用 KVC 的方式對播放器進行操作。
- 播放器和業務播放器大量使用 Category 來對業務進行解耦。
Redux 播放器架構分析
Redux 架構介紹
- 關于 Redux
- 什么是 Redux?“一種可預測的狀態容器”。他其實也是 Flux 里面“單向數據流”的思想,只是充分利用了函數的特性。
- 為什么使用 Redux?為了解決組件之間的通信問題。(用戶操作繁瑣,導致組件間需要有狀態依賴、客戶端權限較多且有大量交互、客戶端與服務端有大量交互)
- Redux 是怎么工作的?↓
Redux 三大原則
單一數據源
- Store 全局唯一的一個對象,就把 TA 當成一個容器,所有的狀態都在 Store 下進行統一“配置”。(Redux 狀態管理 => 一個全局對象 Store => 所有狀態都在全局 Store 下統一配置,為了統一管理)
State 是只讀的
- Action 是用來描述發生了什么的“關鍵詞”,而 Redux 唯一改變 State 的方法就是觸發 Action,而具體使 Action 在 State 上更新生效的是 Reducer---用來描述發生的詳細過程,他充當了一個 Action 到 State 的橋梁。
- (狀態 => 直接改變 State 不能觸發 Render => 唯一改變的方式是觸發 Action)
- (Action => 描述事件發生的詳細過程)
- (Reducer => 充當發起 Action 到 State 的橋梁)
- (修改狀態 => 當試圖修改狀態時,Redux 會記錄這個動作是什么類型、具體完成什么功能,調試時為開發者提供完整的數據流路徑)
Reducer 必須是一個純函數
- 描述 Action 是怎么改變 State 的。接收舊 State 和 Action,返回新 State。
- (Reducer => 內部執行必須無副作用,不能直接修改 State => 狀態發生變化時,要返回一個全新的對象代表新的 State)
Middleware
- Middleware 異步數據流--沒有返回值
- 用戶觸發點贊->產生 Digg Action -> Action 經過 Middleware 異步處理(請求點贊接口)->異步完成(API 請求成功)->Action 傳給 Reducer->Action+OldState->newState->Store 中的 State 更新->通知 View
- 高階函數
- (ReduxNSArrayReduce)redux_reduce {
__weak NSArray* warray = self;
return ^id(id initial, ReduxNSArrayReduceOperator ro) {
__strong NSArray *array = warray;
id result = initial;
for(id object in array) {
result = ro(result, object);
}
return result;
};
}
self.dispatchFunction = middlewares.redux_reverse.redux_reduce(defaultDispatch, ^id(ReduxDispatchFunc df, ReduxMiddleware mw){
return mw([retryDispatch copy], [getState copy])(df);
});
- (id)reduce:(ReduxNSArrayReduceOperator)ro initial:(id)initial {
id result = initial;
for (id obj in self) {
result = ro(result, obj);
}
return result;
}
[middlewares.redux_reverse reduce:^id(ReduxDispatchFunc df, ReduxMiddleware mv) {
return mv([retryDispatch copy], [getState copy])(df);
} initial:defaultDispatch];
- 現存播放架構問題
- 通過 Instruments 測試可以看到,Redux 狀態模型成復制本過高,在復雜業務中性能表現欠佳。
- Reducer 更新完 State 后,需要遍歷所有 Part 通知狀態變化,效率很低。
- Part 之間耦合嚴重,存在 Redux 套 Redux 的情況,維護成本高。
- 播放器業務層使用 KVC 獲取底層播放實例,底層播放實例與業務耦合嚴重。
- 框架層和業務層 Player 都使用大量的 Category 來開發業務 UI,播放器框架層與業務邏輯耦合。
Redux 播放器性能分析
通過線下性能分析,將播放流程中所有的耗時點、卡頓點整體歸類,總結出卡頓耗時任務,其中 Redux 和播放業務導致的卡頓占大多數。
新方案
目標
針對現存播放器問題,重新設計了播放架構,以解決播放器上手成本高、不能方便插拔業務、復雜業務性能差的問題。
- 對播放器進行分層設計,將底層、框架層、業務層隔離開。
- 重新設計業務層框架,降低業務耦合,真正實現業務可插拔的同時提升業務播放器整體性能。
- 新框架將復雜邏輯封裝,提供友好的對外接口,使用簡便。
播放器架構設計方案
架構設計圖
架構設計思路
- 播放器整體將分為 3 層:極簡播放器、基礎播放器、業務播放器。
- 極簡播放器:播放器最底層封裝,提供播放、監控、播放狀態等,可以獨立播放視頻。
- 基礎播放器:播放器基礎框架,提供播放,播控,監控,上下文,任務管理,優化等。
- 業務播放器:最上層業務播放器,可以根據需要將業務進行組合。
- 新架構各層之間耦合度低,極簡播放器、Context、DI 等模塊都可以獨立使用。
- 將眾多業務進行抽象,設計好生命周期,高內聚低耦合,各業務之間互相獨立。
- 將眾多業務任務化,在播放器框架層實現整體調度。
- 采用 Module 的方式與業務交互,寫 Module 就像寫一個普通的 VC 一樣,上手成本低,也能與現有架構進行融合,實現最小限度影響業務。
播放器框架方案
Player
Player 是西瓜播放器的主容器,會封裝 AMPlayer、Context&DI、Interaction、Module、生命周期、任務調度、面板管理、品質擴展等模塊,會對外提供使用接口。
- AMPlayer:極簡(輕量)播放器,對播放流程進行封裝,可以獨立播放視頻。
- Context&DI:播放器狀態同步、服務解耦框架,可以高性能同步播放狀態&業務狀態。
- Interaction:播放器交互層,提供了包括播控、手勢、Module 框架承載等能力。
- Module:業務框架層,將業務按模塊進行封裝,模塊之間相互獨立,模塊之間可插拔、可定制。
- 生命周期:對播放器和 Module 分別劃分生命周期,定義好播放步驟。
- 任務調度:對加載進入播放器的業務進行打散、延時非必要的模塊加載,優先核心播放流程。
- 面板管理:為業務彈框&面板提供的統一接口,可以供業務以播放器為基礎展示相關內容。
AMPlayer
極簡播放器
- 架構設計圖
- 核心邏輯圖
- 基礎播放器提供了:
- 播放源解析能力
- 播放信息邏輯處理能力
- 網絡代理能力
- 播放器 Action、State 等管理能力
- 預加載能力
- 極簡播放器封裝播放底層,包括:Engine 封裝、播放狀態封裝、網絡封裝、播放信息封裝、預加載封裝、核心播放邏輯封裝、播放核心接口封裝。
- 使用
/// 初始化播放信息
PlayerInfo *playerInfo = [PlayerInfo playerInfoWithVideoEngineModel:videoModel];
/// 初始化播放器
AMEnginePlayer *player = [[AMEnginePlayer alloc] init];
player.delegate = self;
[player setPlayerInfo:playerInfo];
[self addSubView:player.playerView];
player.playerView.frame = self.bounds;
[player play];
Context&DI
狀態同步&解依賴模塊
Context
- Context 是狀態同步模塊,用來高效同步播放器的播放狀態、業務狀態。其底層是存儲在 Storage 中。
DI
- 局部依賴注入框架,以 Context 為基礎,對外提供宏綁定、宏鏈接等服務。
- 通過 DI,可以對播放器內部各服務進行解依賴,只需要提供接口就可以使用。
存儲層設計圖
Interaction
播放器交互能力封裝,包含視圖管理、播控、手勢管理、Module 管理
視圖管理:ActionView
- 視圖管理容器,提供根據 ViewType 管理視圖層級的能力
ActionView 接口
@interface PlayerActionView : UIView
/// 添加視圖,并根據viewType插入到合適層級
/// @param view subview
/// @param viewType 視圖類型
- (void)addSubview:(UIView *)view viewType:(ViewType)viewType;
@end
播控視圖:PlayerControlView
播控結構示意圖
- 不同的業務場景,播控式樣雖然各不相同,結構上基本都是劃分出一些布局區域,各個區域負責自身的布局。
- 下圖結構只是講解示例
相關定義介紹
播控視圖:ControlView
- ControlView 是 ActionView 的一個子視圖。
- ControlView 對外提供播控元素添加、視圖管理等能力。
播控元素及 ViewType
- 每一個播控元素都要求有一個對應的 ViewType,用于標識該視圖的類型,便于 ControlView 對其管理。
播控區域布局容器:AreaView
- 業務上可以根據實際情況,將播控劃分成一個或多個 AreaView。
- 每一個 AreaView 聲明該區域支持的 ViewType 列表,并負責相關播控元素的布局。
- ControlView 添加播控元素時,會根據其 ViewType 將其添加至對應 AreaView。
/// 布局容器視圖協議
@protocol PlayerControlItemContainerViewProtocol <NSObject>
@required
/// container支持的item類型順序列表
- (NSArray<ViewType> * _Nonnull)itemViewTypeList;
/// container屬于播控的哪一層
- (PlayerControlViewLayer)atLayer;
/// container在播控上的哪一區域
- (PlayerControlViewArea)atPosition;
/// 移除所有元素
- (NSArray<UIView *> *)removeAllItemViews;
@end
播控 Layer 定義:PlayerControlViewLayer
- 播控上的每一個 AreaView 需要聲明其所屬 Layer,ControlView 根據 Layer 信息控制 AreaView 的顯藏。
- Layer 為 OPTIONS 類型,支持同時設置多個 Layer。
- Layer 內置定義如下,業務可根據自身情況進一步擴展。
/// 播控分層定義
typedef NS_OPTIONS(NSUInteger, XX_Layer) {
XX_Layer_None = 0,
XX_Layer_NormalShow = 1 << 0, // 播控顯示層
XX_Layer_NormalHidden = 1 << 1, // 播控隱藏層
XX_Layer_LockShow = 1 << 2, // 播控鎖定顯示層
XX_Layer_LockHidden = 1 << 3, // 播控鎖定隱藏層
};
播控區域定義:PlayerControlViewArea
- 播控上的每一個 AreaView 需要聲明其所在 Area,ControlView 根據 area 信息對其管理(播控局部隱藏、播控局部淡化)
- PlayerControlViewArea 為 OPTIONS 類型,支持同時設置多個 Area。
- Area 內置定義 Top、Left、Right、Bottom、Center,業務可根據自身情況進一步擴展。
/// 播控區域劃分定義
typedef NS_OPTIONS(NSUInteger, XX_Area) {
XX_Area_None = 0,
XX_Area_Top = 1 << 0,
XX_Area_Left = 1 << 1,
XX_Area_Bottom = 1 << 2,
XX_Area_Right = 1 << 3,
XX_Area_Center = 1 << 4,
};
播控模版
- 播控模版是對播控結構的定義,業務根據實際情況劃分、定義 AreaView。
- ControlView 通過切換模版來整體更新播控結構。
// 播控模版定義
@protocol PlayerControlViewTemplate <NSObject>
@required
/// 播控模版中的布局容器
@property (nonatomic, copy, readonly) NSArray<UIView<PlayerControlItemContainerViewProtocol> *> *itemContainerViews;
/// 播控模版支持的浮動視圖(不需要布局容器承載)
@property (nonatomic, copy, readonly) NSArray<PlayerViewType> *supportFloatItemTypes;
/// 播控模版完成加載
- (void)controlViewDidLoadTemplate:(PlayerControlView *)controlView;
/// controlView添加Container中的itemView
/// @param controlView controlView description
/// @param itemView itemView description
/// @param viewType 視圖類型
- (void)controlView:(PlayerControlView *)controlView didAddItemview:(__kindof UIView *)itemView viewType:(ViewType)viewType;
/// controlView添加浮動子視圖
/// @param controlView controlView description
/// @param floatItemView 浮動子視圖
/// @param viewType 視圖類型
- (void)controlView:(PlayerControlView *)controlView didAddFloatItemview:(__kindof UIView *)floatItemView viewType:(ViewType)viewType;
/// 布局controlView中的容器視圖
- (void)controlViewLayoutItemContainerViews:(PlayerControlView *)controlView;
/// 播控模版完成卸載
- (void)controlViewDidUnloadTemplate:(PlayerControlView *)controlView;
@end
功能總結
- 布局模版切換。
- 顯示圖層切換。
- 屏蔽指定類型視圖。
- 隱藏指定區域視圖。
- 半透明指定區域視圖。
@interface TTVPlayerControlView : UIView
@property (nonatomic, weak, nullable) id<PlayerControlViewDelegate> delegate;
/// 當前播控模版
@property (nonatomic, strong, readonly) id<PlayerControlViewTemplate> controlTemplate;
/// 當前展示層級
@property (nonatomic, assign, readonly) PlayerControlViewLayer currentLayer;
/// 更新播控模版
/// @param controlTemplateClass 新播控模版
- (BOOL)updateControlViewTemplate:(Class<PlayerControlViewTemplate>)controlTemplateClass;
/// 添加播控元素視圖,
- (void)addItemView:(UIView *)itemView viewType:(ViewType)viewType;
/// 切換展示層(備注:支持同時展示多個層,可靈活使用)
/// @param layer 播控層
/// @param duration 動畫時長,0表示無動畫
- (void)showLayer:(PlayerControlViewLayer)layer animateWithDuration:(CGFloat)duration;
/// 設置某一區域的alpha值,達到淡化某一區域的UI效果
/// @param alpha alpha description
/// @param position 作用位置
- (void)setAlpha:(CGFloat)alpha forPosition:(PlayerControlViewArea)position;
/// 屏蔽掉指定位置的布局容器視圖
/// @param positions 位置
/// @param key key description
- (void)setPositionsMask:(PlayerControlViewArea)positions forKey:(NSString *)key;
/// 獲取key對應位置屏蔽信息
/// @param key key description
- (NSArray<ViewType> *)controlItemTypeMaskForKey:(NSString *)key;
/// 移除位置屏蔽
/// @param key key description
- (void)removePositionsMaskForKey:(NSString *)key;
/// 清除所有位置屏蔽
- (void)removeAllPositionsMask;
/// 屏蔽掉指定類型的ItemView(記得移除??????)
/// @param itemTypes 要屏蔽的item集合
/// @param key key
- (void)setItemTypeMask:(NSArray<ViewType> *)itemTypes forKey:(NSString *)key;
/// 移除ItemType屏蔽
- (void)removeItemTypeMaskForKey:(NSString *)key;
/// 清除掉所有ItemType屏蔽
- (void)removeAllItemTypeMask;
@end
中視頻播控的鎖定、顯隱控制實現
PlayerControlView 中只有 Layer 的概念,沒有鎖定、顯示、隱藏的概念,為了更好滿足中、長視頻中播控需求,內置有PlayerControlViewModule(支持單擊手勢呼起、隱藏播控,播控自動隱藏,播控鎖定、解鎖等能力),業務可以選擇使用
/// 播控業務邏輯接口
@protocol PlayerControlViewInterface <NSObject>
/// 播控是否鎖定
- (BOOL)locked;
/// 鎖定/解鎖 播控
- (void)lockControlView:(BOOL)lock animation:(BOOL)animation;
/// 播控是否顯示
- (BOOL)isShowing;
/// 顯示/隱藏播控
- (void)showControlView:(BOOL)show animation:(BOOL)animation;
/// 是否可以自動隱藏
- (BOOL)canAutoHidden;
/// 禁止播控自動隱藏
- (void)disableAutoHiddenControl:(BOOL)disable forKey:(NSString *)key;
/// 自動隱藏重新計時
- (void)retimeAutoHiddenControl;
@end
手勢管理
- 提供點擊、雙擊、拖動、長按、捏合五種常見手勢
- 每一種手勢都支持多個訂閱者同時訂閱,每一個訂閱者都可以同時訂閱多種手勢手勢識別管理過程
- 手勢識別器接收到 touch 事件,詢問手勢管理器是否接收該 touch 事件 -[UIGestureRecognizerDelegate gestureRecognizer:shouldReceiveTouch:]
- 手勢管理器輪詢訂閱者是否要屏蔽該手勢 -[PlayerGestureHandlerProtocol gestureRecognizerShouldDisable:gestureType:]
- 手勢管理器返回 shouldReceiveTouch 結果:如果沒有響應者,或存在任一響應者禁用該手勢,則返回 NO(即該手勢識別器不進行識別),反之 YES(即該手勢識別器繼續下一步識別)
- 手勢識別器詢問手勢管理器是否應該開始 -[UIGestureRecognizerDelegate gestureRecognizer:gestureRecognizerShouldBegin:]
- 手勢管理器輪詢訂閱者是否有要響應該手勢的, -[PlayerGestureHandlerProtocol gestureRecognizerShouldBegin:gestureType:]
- 當存在多個相應者時,根據相應者的優先級確認一個最終相應者 , -[PlayerGestureHandlerProtocol handlerPriorityForGestureType:]
- 手勢管理器保存本次手勢識別的響應者
- 手勢管理器返回 ShouldBegin 結果:如果沒找到響應者返回 NO(即手勢不進行識別),反之 YES(手勢識別器正常進行識別)
- 手勢識別器識別成功并觸發 Action
- 手勢管理器通知響應者手勢識狀態變化 -[PlayerGestureHandlerProtocol handleGestureRecognizer:gestureType:]
- 手勢識別結束,手勢管理器重置保存的響應者
接口設計
@protocol PlayerGestureServiceInterface <NSObject>
/// 添加handler,響應指定手勢類型
/// @param handler 手勢處理器
/// @param gestureType 手勢類型,可選多個手勢類型
- (void)addGestureHandler:(id<PlayerGestureHandlerProtocol>)handler forType:(GestureType)gestureType;
/// 刪除handler,只針對指定手勢類型
/// @param handler 手勢處理器
/// @param gestureType 手勢類型,可選多個手勢類型
- (void)removeGestureHandler:(id<PlayerGestureHandlerProtocol>)handler forType:(GestureType)gestureType;
/// 便利方法:刪除handler,gestureType = TTVGestureTypeAll
/// @param handler 手勢處理器
- (void)removeGestureHandler:(id<PlayerGestureHandlerProtocol>)handler;
/// 便利方法:屏蔽指定手勢類型
/// @param gestureType 手勢類型,可選多個手勢類型
/// @param scene 場景信息,方便異常調試
- (id<PlayerGestureHandlerProtocol>)disableGestureType:(GestureType)gestureType scene:(NSString *)scene;
@end
/// 手勢Hnadler協議
@protocol PlayerGestureHandlerProtocol <NSObject>
@optional
// 當多個handler都可以相應同一手勢時,需要根據優先級選擇一個,默認為0
- (NSInteger)handlerPriorityForGestureType:(GestureType)gestureType;
/// 是否禁止該手勢,默認為NO
- (BOOL)gestureRecognizerShouldDisable:(UIGestureRecognizer *)gestureRecognizer gestureType:(GestureType)gestureType;
/// 是否響應該手勢,默認為YES
- (BOOL)gestureRecognizerShouldBegin:(UIGestureRecognizer *)gestureRecognizer gestureType:(GestureType)gestureType;
/// 手勢處理回調
- (void)handleGestureRecognizer:(UIGestureRecognizer *)gestureRecognizer gestureType:(GestureType)gestureType;
@end
Module 管理
按照一定規則將播放器拆分成不同的模塊/插件(后續統稱模塊),模塊之間、模塊與底層播放器互相不耦合,支持業務插拔、靈活組裝、定制播放器模塊,并且單個播放器功能模塊功能收斂和隔離。
播放器模塊化架構問題模型
實現播放器模塊化主要是解決下述問題:
- 播放器功能模塊如何劃分
- 播放器功能模塊之間如何交互
- 獲取其它功能模塊狀態 (例如獲取是否全屏、播控是否顯示等)。
- 自己狀態改變,通知訂閱者 (例如顯示播控時隱藏 XX 視圖)。
播放器模塊和底層播放器交互
- 調用底層播放器接口(例如 Seek、Play、Pause、Stop、切換清晰度、倍速等)。
- 獲取播放器信息和狀態(例如播放狀態、Loading 狀態、readyforDisPlay 等)。
- 監聽播放器狀態改變。
業務和播放器模塊交互
- 動態配置播放器模塊(例如打開或關閉重力感應全屏)。
- 傳遞播放器模塊需要的數據(例如播放標題、作者信息等)。
- 監聽播放器模塊狀態變化。
播放器播控
- 播放器模塊在播控上添加視圖、布局視圖 (例如進度條模塊在播控上添加進度條、時長信息 Lable 等)。
- 業務在播控上添加視圖、布局視圖 (例如中視頻在播控上添加點贊、評論、彈幕、作者信息等)。
- 業務獲取、定制、修改播放器模塊的 UI 視圖,控制播控視圖的顯示、隱藏、顯示順序等。
新架構 PlayerModule + PlayerContext
Module 的設計初衷是一個承載播放器功能模塊的容器,播放器的功能邏輯收斂在 Module 容器內部,業務、播放器模塊、底層播放器之間采用低耦合的方式進行交互,支持業務動態組合和插拔 Module。其中主要是為 Module 注入生命周期、狀態查詢和同步機制、綁定和獲取服務(依賴抽象接口)的能力。
PlayerContext
- 通過 Key 發送通知 (類似 NSNotificationCenter)
- 通過 Key 查詢狀態 (存儲播放器所有狀態的一個大字典)
- 通過協議綁定和獲取服務(DI)
新的 Module 架構解決業務、播放器模塊、底層播放器交互解耦合:
- 播放器功能模塊如何劃分 (建議按照功能,業務也可隨意劃分)
- 播放器功能模塊之間如何交互
- 不關心其它模塊而是只關心狀態,通過 PlayerContext 查詢狀態。
- 自己狀態改變,直接 PlayerContext 發送通知。
- 需要調用其它功能模塊接口的,不顯示依賴其它模塊而是依賴抽象協議,通過 PlayerContext 獲取協議類,調用接口。
播放器模塊和底層播放器交互
- 不直接依賴播放器,而是依賴播放器抽象接口服務,通過 PlayerContext 獲取協議類。
- 通過 PlayerContext 獲取播放器信息和狀態(例如播放狀態、loading 狀態、readyforDisPlay 等)。
- 通過 PlayerContext 監聽播放器狀態改變的通知。
業務和播放器模塊交互
- 通過 PlayerContext 發送通知修改狀態來動態配置播放器模塊(例如打開或關閉重力感應全屏)。
- 業務不直接獲取播放器模塊傳遞數據,而是播放器統一封裝數據 Model,業務統一配置,模塊自己獲取需要的數據(例如播放標題、作者信息、水印信息、互動貼紙信息等)。
- 通過 PlayerContext 監聽播放器模塊狀態變化通知。
播放器播控
- 播放器模板更新時通知模塊在播控上添加視圖(例如 Seek 模塊在播控上添加進度條、時長信息 Lable 等)。
- 業務通過新增 Module 在播控上添加視圖、布局視圖 (例如中視頻在播控上添加點贊、評論、彈幕、作者信息等)。
- 業務通過繼承 Player 內部的 Module 重寫生成播控的方法來獲取、定制、修改播放器模塊的 UI 視圖。
- 業務重寫播放器 UI 定制服務來定制、修改播控 UI。
Module 設計類圖:
- PlayerModuleManager
- 提供 Module 增刪查接口,持有所有 Module
- 為 Module 注入 Context(Module 間通信、DI)
- 為 Module 注入生命周期
@interface PlayerModuleManager : NSObject
#pragma mark - add && remove Module
- (void)addModule:(id<PlayerBaseModuleProtocol>)module;
- (void)removeModule:(id<PlayerBaseModuleProtocol>)module;
- (void)addModuleByClzz:(Class)clzz;
- (void)removeModuleByClzz:(Class)clzz;
- (void)removeAllModules;
#pragma mark - Data
// 設置Module數據
- (void)setupData:(id)data;
#pragma mark - Life cycle
// 播放器viewDidLoad
- (void)viewDidLoad;
// 模板更新,可以添加播控的回調
- (void)templateDidUpdate;
@end
- PlayerBaseModule
- 角色:播放器的功能拆分模塊,整體拆分為 MVC,作為 Controller 的角色,作為播放器模塊功能的一個容器
- 提供通信和 DI 工具 PlayerContext
- 提供類似 ViewController 的生命周期方法和常用播放器生命周期方法(適配中發現只需要 ViewDidLoad,刪除 ViewWillApper 等方法),UI 在內部自己創建,并通過播放器 UI 服務綁定一個 key(每個播控對應一個唯一的 Key),然后通過模板服務添加。添加到的位置是業務播控模板配置決定的
@interface PlayerBaseModule : NSObject
//播放器狀態通信、DI
@property (nonatomic, weak) PlayerContext *context;
#pragma mark - Life cycle
//Module加載(通過context注冊服務)和添加播放器ViewDidLoad前的狀態監聽
- (void)moduleDidLoad;
// 播放器viewDidLoad(添加狀態監聽)
- (void)viewDidLoad;
// 模板更新,可以添加播控的回調,在該回調方法中通過UI定制服務獲取或創建播控視圖
- (void)templateDidUpdate;
//Module移除(解除服務、移除監聽、移除視圖等)
- (void)moduleDidUnLoad;
@end
- PlayerUICustomizeInterface
- 定制播放器 UI 的協議
/// 播放器播控視圖自定義服務協議
@protocol PlayerUICustomizeInterface <NSObject>
@required
/// 根據當前播控模版,按需加載視圖
/// @param viewType 視圖類型
- (nullable __kindof UIView *)itemViewWithViewType:(ViewType)viewType;
/// 根據當前播控模版,按需加載、更新試圖
/// @param view 入參view,view為nil時,嘗試構建該類型視圖
/// @param viewType 是圖類型
/// @param loadViewBlock 當入參view為nil,并重新新建視圖時回調該block
- (void)updateItemView:(nullable UIView *)view
viewType:(ViewType)viewType
loadViewBlock:(void(^ __nonnull)(__kindof UIView * __nonnull view))loadViewBlock;
/// 當前播控模版是否支持該viewType
- (BOOL)isSupportedForTemplate:(ViewType)viewType;
@end
PlayerModule 偽代碼
- 新建 PlayerXXModule 繼承自 PlayerBaseModule 或者直接實現 PlayerBaseModuleProtocol 協議
- 在 PlayerXXModule 根據需求定義屬性通過 PlayerContext 的 DI 獲取服務
- 如果 Module 為外部提供接口調用服務,則在 moduleDidLoad 綁定服務
- 在 viewDidLoad 方法中添加狀態監聽,根據狀態變化處理業務邏輯
- 在 templateDidUpdate 通過 UI 定制服務獲取視圖,再通過 actionViewInterface 播控服務添加
- 在 moduleDidUnLoad 方法中解綁服務和移除 UI 視圖
播放器生命周期
生命周期:整個播放器創建、播放、釋放、復用的周期流程,單次播放流程中只進行一次
狀態變化:播放器播放狀態、視圖狀態的變化在單次生命周期中多次(頻繁)發生變化
- 根據播放器的整個生命周期流程,播放器內部功能可以通過簡單的注冊和回調在期望時機進行執行任務。
播放器異步加載
核心思想:優先播放任務,打散、延時非必要的模塊加載
PlayerModuleLoader 介紹
- PlayerModuleLoader 是播放器內置的模塊加載器,支持打散、異步加載模塊,業務選擇使用 CoreModules 核心模塊,播放器初始化后會立即加載所有的 Core Modules。
- AsyncLoadModules 是允許異步加載的模塊,PlayerModuleLoader 在 viewDidLoad 時機,讀取 getAsyncLoadModules,然后開始執行異步、打散加載。
- 異步加載是在 NSDefaultRunLoopMode 模式下執行,并且 App 進入后臺后,會自動暫停加載。
/// 播放器內置基礎ModuleLoader,支持Module異步打散加載
@interface PlayerModuleLoader : PlayerBaseModule
#pragma mark - Override Method
/// 核心模塊,會在moduleDidLoad時機同步加載
- (NSArray<id<PlayerBaseModuleProtocol>> *)getCoreModules;
/// 異步加載模塊,會在viewDidLoad時機開始異步加載
- (NSArray<id<PlayerBaseModuleProtocol>> *)getAsyncLoadModules;
#pragma mark - 添加、移除接口
/// 添加module
- (void)addModule:(id<PlayerBaseModuleProtocol>)module;
// 移除module
- (void)removeModule:(id<PlayerBaseModuleProtocol>)module;
@end
播放器面板管理
播放器作為核心的消費場景,各個業務模塊經常需要以它為基礎展示面板、彈窗,有些不僅影響播放器還會影響其他業務面板,為了方便管理和感知類似的視圖,播放器提供了統一的面板展示接口。
接口定義
/// 展示panelView,已有panelview會被移除掉
/// @param panelView panelView
/// @param animations 自定義展示動畫
/// @param onClickMaskBlock 背景點擊block
- (void)showPanelView:(UIView *)panelView
animations:(PanelViewAnimations)animations
onClickMaskBlock:(PanelViewOnClickMaskBlock)onClickMaskBlock;
/// 展示panelView,已有panelview會被壓棧
- (void)pushPanelView:(UIView *)panelView
animations:(PanelViewAnimations)animations
onClickMaskBlock:(PanelViewOnClickMaskBlock)onClickMaskBlock;
/// 隱藏panelView,支持動畫
/// @param panelView panelView
/// @param animations 自定義消失動畫
- (void)dismissPanelView:(UIView *)panelView animations:(PanelViewAnimations)animations;
/// 移除panelView,無動畫
/// @param panelView panelView
- (void)removePanelView:(UIView *)panelView;
/// 移除所有的panelView
- (void)removeAllPanelViews;
業務適配
視頻業務
之前視頻業務的業務播放器是 PlayerViewController,業務邏輯分散在各個業務 Part 和 PlayerViewController 的 Category 中,而且上層 Feed 業務、詳情頁業務都存在直接獲取底層播放器,直接使用 playerState、playerStore 的現象。
此次適配為新架構,主要需做以下事情:
- 將功能業務進一步拆分、適配到業務 Module 中。
- 將業務播放器 PlayerViewController 的 Category 中的功能邏輯收斂進對應的 Module 中。
- 將接口適配到新老架構中,在業務播放器內做 AB。
- 去除上層業務對底層播放器的的直接依賴,使用業務播放層的接口。
Module 加載
- 創建一個視頻業務的 moduleLoader 來管理中視頻播放器的 Module。
- 將 Seek、controlView 播控視圖、埋點等 9 個 Module 放在 coreModules 中,在播放器創建時就加載這些 Module,其余的功能 Module(20+)均放在 asyncModules 中,在播放器創建完之后再異步加載。
功能模塊適配為 Module
- 業務功能模塊適配為 Module,保證業務邏輯一致,僅對接口形式進行變更。
- 對一些細粒度的功能模塊和邏輯直接合并進相應 Module 中。
總結
本次重構采用自下而上的方式進行,從極簡播放器到播放框架、再到播放架構在副場景驗證,進而延伸到主場景,最終整體完成。
本次播放適配超 5 萬行代碼改動。通過 AB 實驗,本次演進在業務、性能等方面均有不錯的收益,在人效方面也有很大的提升。
- 業務收益:在播放 VV,播放時長,留存等方面均有不錯的表現。
- 性能收益:卡頓、掉幀、首幀等方面均有很大的收益。
- 架構&效率:通過幾輪業務適配,新架構擴展&維護簡便,上手成本也不高。
- 針對不同業務,Module 可以復用。(如不同體裁的適配,業務會有差異,可以通過復用 Module 來提高開發效率)
- 校招同學適配小視頻業務時,只用了 2 人日就完成了適配。