导读:为了应答视频编辑类工具利用简单的交互,度咔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 {// 实例化的 BDTZEditViewControllerDelegatevar 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等等都会收到这个事件,从而做出相应的状态扭转。
//APluginfunc sendAction(model: Any?) { let action = BDTZClipSeleteAction.init(event: .selected, type: .sticker, actionTarget: model) editViewController?.pluginDispatcher.dispatch({ subscriber in subscriber.update?(action: action) })}
//BPluginextension 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 说
百度官网技术公众号上线啦!
技术干货 · 行业资讯 · 线上沙龙 · 行业大会
招聘信息 · 内推信息 · 技术书籍 · 百度周边
欢送各位同学关注