1

西瓜视频 iOS 播放器技术重构

 1 year ago
source link: https://www.51cto.com/article/712122.html
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.

播放器简介

播放器是西瓜视频等视频类 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

手势管理

  • 提供点击、双击、拖动、长按、捏合五种常见手势
  • 每一种手势都支持多个订阅者同时订阅,每一个订阅者都可以同时订阅多种手势手势识别管理过程
图片
  1. 手势识别器接收到 touch 事件,询问手势管理器是否接收该 touch 事件 -[UIGestureRecognizerDelegate gestureRecognizer:shouldReceiveTouch:]
  2. 手势管理器轮询订阅者是否要屏蔽该手势 -[PlayerGestureHandlerProtocol gestureRecognizerShouldDisable:gestureType:]
  3. 手势管理器返回 shouldReceiveTouch 结果:如果没有响应者,或存在任一响应者禁用该手势,则返回 NO(即该手势识别器不进行识别),反之 YES(即该手势识别器继续下一步识别)
  4. 手势识别器询问手势管理器是否应该开始 -[UIGestureRecognizerDelegate gestureRecognizer:gestureRecognizerShouldBegin:]
  5. 手势管理器轮询订阅者是否有要响应该手势的, -[PlayerGestureHandlerProtocol gestureRecognizerShouldBegin:gestureType:]
  6. 当存在多个相应者时,根据相应者的优先级确认一个最终相应者 , -[PlayerGestureHandlerProtocol handlerPriorityForGestureType:]
  7. 手势管理器保存本次手势识别的响应者
  8. 手势管理器返回 ShouldBegin 结果:如果没找到响应者返回 NO(即手势不进行识别),反之 YES(手势识别器正常进行识别)
  9. 手势识别器识别成功并触发 Action
  10. 手势管理器通知响应者手势识状态变化 -[PlayerGestureHandlerProtocol handleGestureRecognizer:gestureType:]
  11. 手势识别结束,手势管理器重置保存的响应者

接口设计

@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 的现象。

此次适配为新架构,主要需做以下事情:

  1. 将功能业务进一步拆分、适配到业务 Module 中。
  2. 将业务播放器 PlayerViewController 的 Category 中的功能逻辑收敛进对应的 Module 中。
  3. 将接口适配到新老架构中,在业务播放器内做 AB。
  4. 去除上层业务对底层播放器的的直接依赖,使用业务播放层的接口。

Module 加载

  1. 创建一个视频业务的 moduleLoader 来管理中视频播放器的 Module。
  2. 将 Seek、controlView 播控视图、埋点等 9 个 Module 放在 coreModules 中,在播放器创建时就加载这些 Module,其余的功能 Module(20+)均放在 asyncModules 中,在播放器创建完之后再异步加载。

功能模块适配为 Module

  1. 业务功能模块适配为 Module,保证业务逻辑一致,仅对接口形式进行变更。
  2. 对一些细粒度的功能模块和逻辑直接合并进相应 Module 中。

本次重构采用自下而上的方式进行,从极简播放器到播放框架、再到播放架构在副场景验证,进而延伸到主场景,最终整体完成。

本次播放适配超 5 万行代码改动。通过 AB 实验,本次演进在业务、性能等方面均有不错的收益,在人效方面也有很大的提升。

  • 业务收益:在播放 VV,播放时长,留存等方面均有不错的表现。
  • 性能收益:卡顿、掉帧、首帧等方面均有很大的收益。
  • 架构&效率:通过几轮业务适配,新架构扩展&维护简便,上手成本也不高。
  • 针对不同业务,Module 可以复用。(如不同体裁的适配,业务会有差异,可以通过复用 Module 来提高开发效率)
  • 校招同学适配小视频业务时,只用了 2 人日就完成了适配。

About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK