导读:为了应答视频编辑类工具利用简单的交互,度咔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 的特点就是单向数据流:

  1. 用户在 View 层发动一个 Action 对象给 D ispatcher
  2. Dispatcher 接管到 Action 并要求 Store 做相应的更改
  3. Store 做出绝对应更新,而后收回一个 changeEvent
  4. 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模式来开发,毛病也非常明显:

  1. 层级太多,极易产生大量的冗余代码。
  2. 老代码移植工作量微小。

对咱们来说采纳Flux 模式设计理念比某个特定的实现框架更重要,咱们依据度咔业务的特点只是取其思维应用单层级构造,用来治理ViewController与Plugin形象之间的关系和事件传递,而没有把View也加到层级中去,plugin外部能够应用MVC、MVVM等任何架构,只须要把通信形式对立。

下面只是应用简略的例子介绍了编辑工具在Flux思维上的利用。然而在理论应用中还应该思考:

  1. UI层级遮蔽问题:插件中的某个View须要加到控制器View上,会造成控件层级遮蔽问题。下面代码中的BDTZEditLevelView就是为了解决这个问题。
  2. 多线程问题:在开发中咱们不免大量的线程异步解决工作,咱们必须规定插件通信之间的线程,Dispatcher外部也应该有线程治理的代码。
  3. plugin依赖关系问题:Dispatcher还要维持plugin之间的依赖关系,比方一个action要APlugin先解决批改某些数据或者状态后,BPlugin再解决,能够采纳加标等形式解决。
  4. 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 说

百度官网技术公众号上线啦!

技术干货 · 行业资讯 · 线上沙龙 · 行业大会

招聘信息 · 内推信息 · 技术书籍 · 百度周边

欢送各位同学关注