共计 7233 个字符,预计需要花费 19 分钟才能阅读完成。
导读:为了应答视频编辑类工具利用简单的交互,度咔 iOS 借鉴了 Flux 架构模式的设计思维,参考有向无环图的拓扑概念,将事件进行集中化治理,从开发体验上实现了舒服清新、容易驾驭的“单向流”模式;在这种调度模式下,事件的变动和追踪变得清晰可预测,并且显著的减少了业务的可扩展性。
全文 6882 字,预计浏览工夫 18 分钟。
一、架构背景
视频编辑工具类利用往往交互简单,大部分操作是在同一个主界面上进行,而这个界面同时存在较多的视图区域(预览区、轴区、undo redo、操作面板等等),每个区域既要接管用户手势,又要追随用户操作联动更新状态。同时除反对主场景编辑性能外, 还要同时反对其余特色性能,比方度咔的通用编辑、疾速剪辑、主题模板等,都须要应用预览和编辑性能;于是对架构的可扩大和可复用能力天然有了很高的要求。
通过调研,度咔 iOS 最终借鉴了 Flux 架构模式的设计思维,参考有向无环图的拓扑概念,将事件进行集中化治理,从开发体验上实现了舒服清新、容易驾驭的“单向流”模式;在这种调度模式下,事件的变动和追踪变得清晰可预测,并且显著的减少了业务的可扩展性。
二、播放预览复用
度咔通用编辑以及很多衍生工具、性能都须要依赖于预览、素材编辑这一类根底能力。
比方下列这些性能都依赖于同一套预览播放逻辑,须要将这些根底能力形象为一个 base 控制器。
baseVC 构造为:
三、功能模块复用
预览播放复用的问题解决了,如何在这套逻辑上增加各样的素材编辑性能,比方贴纸、文字、滤镜等性能,并且使这些性能与 VC 解耦,最终达到复用的目标?
最终咱们应用插拔式设计理念,把每一个子性能形象成一个 plugin,采纳间接调用依赖层的形式把 controller、view、timeline、streamingContext、liveWindow 这写 90% 场景下会用到的属性通过 weak 间接赋值给 plugin。
protocol BDTZEditPlugin: NSObjectProtocol {
// 组织控制器
var editViewController: BDTZEditViewController? {get set}
// 所有增加到控制器 View 上的控件 加到这个 View 上,解决层级问题
var mainView: BDTZEditLevelView? {get set}
// 编辑场景的时间轴实体,由轨道组成,能够有多个视频轨道和音频轨道,由视频轨道决定长度
var timeline: Timeline? {get set}
// 流媒体上下文 蕴含工夫线、预览窗口、采集、资源包治理等相干信息汇合的对象
var streamingContext: StreamingContext? {get set}
// 视频预览窗口控件
var liveWindow: LiveWindow? {get set}
/// 插件初始化
func pluginDidLoad()
/// 插件卸载
func pluginDidUnload()}
只有实现这个协定,并且通过调用 baseVC 的 add: 办法增加 plugin 后,那么相应的 plugin 就会拿到对应的属性进行调用,防止应用单例或者通过层层回调到 VC 去解决。
func addPlugin(_ plugin: BDTZEditPlugin) {plugin.pluginWillLoad()
plugin.editViewController = self
plugin.mainView = self.view
plugin.liveWindow = liveWindow
plugin.streamingContext = streamingContext
plugin.timeline = timeline
if plugin.conforms(to: BDTZEditViewControllerDelegate.self) {pluginDispatcher.add(subscriber: plugin as! BDTZEditViewControllerDelegate)
}
plugin.pluginDidLoad()}
func removePugin(_ plugin: BDTZEditPlugin) {plugin.pluginWillUnload()
plugin.editViewController = nil
plugin.mainView = nil
plugin.liveWindow = nil
plugin.streamingContext = nil
plugin.timeline = nil
if plugin.conforms(to: BDTZEditViewControllerDelegate.self) {pluginDispatcher.remove(subscriber: plugin as! BDTZEditViewControllerDelegate)
}
plugin.pluginDidUnload()}
plugin 是具体性能和 VC 之间的一个中间层,能够承受 VC 的生命周期事件、预览播放事件、拿到 VC 中的要害对象、调用 VC 的外部所有 public 接口能力。作为插在 VC 上的一个独立子性能单元,具备编辑能力、素材能力、网络 UI 交互等能力。
plugin 分为 service 层和 UI 层,同时在设计之初,基于该架构的 plugin 不仅仅能在度咔 app 内应用,厂内其余 app 仅须要极少工作量就能立刻接入 plugin。
所有性能能扩散到插件中,按需组装和复用。
同时能够对外输入的不仅仅单个 plugin、还是能够是多个 plugin 的组合。以封面性能为例,封面编辑是一个以 coverVC 为组织的控制器,它蕴含多个 plugin,比方已存在的文字 plugin 和贴纸 plugin;coverVC 除了作为独立性能利用之外,把它包装成一个封面 plugin 只需大量数据对接代码(上图的通用剪辑数据对接 plugin)就能够集成到通用剪辑 VC,像堆乐高积木一样进行拼装组合。
四、事件状态治理
编辑工具 app 因交互的复杂性十分依赖于状态更新,通常来说在 iOS 开发中告诉对象状态变动个别采纳以下几种形式:
- Delegate
- KVO
- NotificationCenter
- Block
这四种形式都能够治理状态的变动,然而都存在一些问题。Delegate 和 Block,往往会在组件之间创立强依赖关系;KVO 和 Notifications,会创立不可见的依赖项,如果某些重要音讯被移除或更改,也很难被发现,从而升高利用稳定性。
即便是苹果的 MVC 模式,也只提倡数据层及其表示层的拆散,没有提供任何工具代码、领导架构。
4.1 为什么抉择 Flux 架构模式
于是咱们借鉴 Flux 架构模式的思维。Flux 是一种十分轻量级的架构模式,Facebook 将其用于客户端 Web 应用程序,用于避开 MVC,反对单向数据流(前面也是列举的前端的 mvc 数据流向图)。核心思想是中心化管制,它让所有的申请与扭转都只能通过 action 收回,对立 由 dispatcher 来调配。益处是 View 能够放弃高度简洁,它不须要关怀太多的逻辑,只须要关怀传入的数据。中心化还管制了所有数据,产生问题时能够不便查问定位。
- Dispatcher:处理事件散发,维持 Store 之间的依赖关系
- Store:负责存储数据和解决数据相干逻辑
- Action:触发 Dispatcher
- View:视图,负责显示用户界
通过上图能够看进去,Flux 的特点就是单向数据流:
- 用户在 View 层发动一个 Action 对象给 D ispatcher
- Dispatcher 接管到 Action 并要求 Store 做相应的更改
- Store 做出绝对应更新,而后收回一个 changeEvent
- View 接管到 changeEvent 事件后,更新页面
- 根本的 MVC 数据流
-
简单的 MVC 数据
- 简略的 Flux 数据流
- 简单 Flux 数据流
相比 MVC 模式,Flux 多出了更多的箭头跟图标,然而有个关键性的差异是:所有的箭头都指向一个方向,在整个零碎中造成一个事件传递链。
4.2 利用 Flux 思维来实现状态治理
状态分为两种:
- 以组织控制器收回的事件产生状态变动,比方:控制器的生命周期 ViewDidLoad()等等、根底编辑预览能力的回调,例如 seek、progress、playState 变动等等
- 各个组件的之间事件传递产生的状态变动,下图中 plugin 协定形象来形容上图中的 Store 作用
控制器持有 EventDispatch 能力的对象 dispatcher,并通过这个 dispatcher 传递事件。
Dispatcher
class WeakProxy: Equatable {
weak var value: AnyObject?
init(value: AnyObject) {self.value = value}
static func == (lhs: WeakProxy, rhs: WeakProxy) -> Bool {return lhs.value === rhs.value}
}
open class BDTZActionDispatcher<T>: NSObject {fileprivate var subscribers = [WeakProxy]()
public func add(subscriber: T) {guard !subscribers.contains(WeakProxy(value: subscriber as AnyObject)) else {return}
subscribers.append(WeakProxy(value: subscriber as AnyObject))
}
public func remove(subscriber: T) {let weak = WeakProxy(value: subscriber as AnyObject)
if let index = subscribers.firstIndex(of: weak) {subscribers.remove(at: index)
}
}
public func contains(subscriber: T) -> Bool {
var res: Bool = false
res = subscribers.contains(WeakProxy(value: subscriber as AnyObject))
return res
}
public func dispatch(_ invocation: @escaping(T) -> ()) {clearNil()
subscribers.forEach {
if let subscriber = $0.value as? T {invocation(subscriber)
}
}
}
private func clearNil() {subscribers = subscribers.filter({ $0.value != nil})
}
}
通过泛型的多重代理形式把事件分发给 subscribers 外部的对象(下面代码块中的 addPlugin: 外部增加 subscribers),当然也能够通过注册 Block 的办法去实现。
Dispatcher 实例
申明一个 protocol 继承要散发的能力
@objc protocol BDTZEditViewControllerDelegate: BDTZEditViewLifeCycleDelegate, StreamingContextDelegate, BDTZEditActionSubscriber {
// BDTZEditViewLifeCycleDelegate 控制器申明周期
// StreamingContextDelegate 预览编辑能力回调
// BDTZEditActionSubscriber plugin 之间的通信协定
}
控制器事件散发
public class BDTZEditViewController: UIViewController {
// 实例化的 BDTZEditViewControllerDelegate
var pluginDispatcher = BDTZEditViewControllerDelegateImp()
public override func viewDidAppear(_ animated: Bool) {super.viewDidAppear(animated)
pluginDispatcher.dispatch { subscriber in
subscriber.editViewControllerViewDidAppear?()}
}
public override func viewDidLoad() {super.viewDidLoad()
/*** 省略局部代码 **/
setupPlugins()
// 放最初调用
pluginDispatcher.dispatch { subscriber in
subscriber.editViewControllerViewDidLoad?()}
}
/***...**/
/// seek 进度回调
func didSeekingTimelinePosition(_ timeline: Timeline!, position: Int64) {
pluginDispatcher.dispatch { subscriber in
subscriber.didSeekingTimelinePosition?(timeline, position: position)
}
}
/***...**/
}
plugin 之间事件传递
plugin 之间的事件传递就要用到下面的 BDTZEditActionSubscriber 协定了。
@objc protocol BDTZEditAction {
}
@objc protocol BDTZEditActionSubscriber {@objc optional func update(action: BDTZEditAction)
}
BDTZEditAction 是一个空协定,能够是任何类继承它来形容想要传递的任何信息。联合编辑工具的特点 (尽管交互简单然而素材类型和操作都是无限的) 只须要大量的 action 就能形容所有状态。目前咱们应用选中 action、各种素材 action、面板起落 action、后退回退 action 等等这些事件来形容素材的增加、删除、挪动、剪裁、保留草稿一些列的操作。咱们以选中 action(选中某个片段的事件)举例:
当 APlugin 收回了一个选中事件,BPlugin、CPlugin 等等都会收到这个事件,从而做出相应的状态扭转。
//APlugin
func sendAction(model: Any?) {let action = BDTZClipSeleteAction.init(event: .selected, type: .sticker, actionTarget: model)
editViewController?.pluginDispatcher.dispatch({ subscriber in
subscriber.update?(action: action)
})
}
//BPlugin
extension BDTZTrackPlugin: BDTZEditActionSubscriber {func update(action: BDTZEditAction) {
if let action = action as? BDTZClipSeleteAction {handleSelectActionDoSomething()
}
}
}
当预览区的贴纸被选中,那么轴区也会随之被选中,底部区域也要切换成三级菜单。** 一个 action 被派发当前,所有 plugin 都会收到它,对此 action 感兴趣的 plugin 会做出相应的状态变动。
**
五、总结
iOS 也有参照 flux 思维设计的 ReSwift 框架,然而如果应用纯 Flux 模式来开发,毛病也非常明显:
- 层级太多,极易产生大量的冗余代码。
- 老代码移植工作量微小。
对咱们来说采纳 Flux 模式设计理念比某个特定的实现框架更重要,咱们依据度咔业务的特点只是取其思维应用单层级构造,用来治理 ViewController 与 Plugin 形象之间的关系和事件传递,而没有把 View 也加到层级中去,plugin 外部能够应用 MVC、MVVM 等任何架构,只须要把通信形式对立。
下面只是应用简略的例子介绍了编辑工具在 Flux 思维上的利用。然而在理论应用中还应该思考:
- UI 层级遮蔽问题:插件中的某个 View 须要加到控制器 View 上,会造成控件层级遮蔽问题。下面代码中的 BDTZEditLevelView 就是为了解决这个问题。
- 多线程问题:在开发中咱们不免大量的线程异步解决工作,咱们必须规定插件通信之间的线程,Dispatcher 外部也应该有线程治理的代码。
- plugin 依赖关系问题:Dispatcher 还要维持 plugin 之间的依赖关系,比方一个 action 要 APlugin 先解决批改某些数据或者状态后,BPlugin 再解决,能够采纳加标等形式解决。
- action 收缩问题:绝对于 API 间接调用的形式,监听 action 尽管写更少的代码,然而容易造成 action 有限增多的状况,所以在定义 action 要思考可扩大和结构化。
参考链接:
[1]http://reswift.github.io/ReSw…
[2]https://facebook.github.io/flux/
[3]https://redux.js.org
[4]http://blog.benjamin-encz.de/…\_source=swifting.io&utm\_medium=web&utm\_campaign=blog%20post
举荐浏览:
|iOS 解体日志在线符号化实际
|百度商业托管页零碎高可用建设办法和实际
|AI 在视频畛域使用—弹幕穿人
———- END ———-
百度 Geek 说
百度官网技术公众号上线啦!
技术干货 · 行业资讯 · 线上沙龙 · 行业大会
招聘信息 · 内推信息 · 技术书籍 · 百度周边
欢送各位同学关注