写点什么

Flux 架构思想在度咔 App 中的实践

用户头像
百度Geek说
关注
发布于: 2021 年 10 月 28 日
Flux架构思想在度咔App中的实践

导读:为了应对视频编辑类工具应用复杂的交互,度咔 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/ReSwift/master/getting-started-guide.html

[2]https://facebook.github.io/flux/

[3]https://redux.js.org

[4]http://blog.benjamin-encz.de/post/real-world-flux-ios/?utm_source=swifting.io&utm_medium=web&utm_campaign=blog%20post


推荐阅读:


iOS 崩溃日志在线符号化实践


|百度商业托管页系统高可用建设方法和实践


|AI 在视频领域运用—弹幕穿人


---------- END ----------


百度 Geek 说


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


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


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


欢迎各位同学关注

发布于: 2021 年 10 月 28 日阅读数: 18
用户头像

百度Geek说

关注

百度官方技术账号 2021.01.22 加入

关注我们,带你了解更多百度技术干货。

评论

发布
暂无评论
Flux架构思想在度咔App中的实践