导读:为了应对视频编辑类工具应用复杂的交互,度咔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 = selfplugin.mainView = self.viewplugin.liveWindow = liveWindowplugin.streamingContext = streamingContextplugin.timeline = timelineif plugin.conforms(to: BDTZEditViewControllerDelegate.self) {pluginDispatcher.add(subscriber: plugin as! BDTZEditViewControllerDelegate)}plugin.pluginDidLoad()}func removePugin(_ plugin: BDTZEditPlugin) {plugin.pluginWillUnload()plugin.editViewController = nilplugin.mainView = nilplugin.liveWindow = nilplugin.streamingContext = nilplugin.timeline = nilif 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 = falseres = 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 insubscriber.editViewControllerViewDidAppear?()}}public override func viewDidLoad() {super.viewDidLoad()/***省略部分代码**/setupPlugins()//放最后调用pluginDispatcher.dispatch { subscriber insubscriber.editViewControllerViewDidLoad?()}}/***...**//// seek进度回调func didSeekingTimelinePosition(_ timeline: Timeline!, position: Int64) {pluginDispatcher.dispatch { subscriber insubscriber.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 insubscriber.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模式来开发,缺点也非常明显:

  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 说

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

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

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

欢迎各位同学关注

Flux架构思想在度咔App中的实践相关推荐

  1. Weex实战分享|Weex在极客时间APP中的实践

    本文是根据 WeexConf2018 中议题<Weex在极客时间APP中的实践>内容文档整理而成.主要分享极客时间在深度使用Weex过程中的一些经验和体会. 孙涛  极客邦前端负责人 大家 ...

  2. 基于CQRS的架构在答题PK小游戏中的实践案例

    1. 前言 \\ 领域驱动设计(Domain-Driven Design,下文简称 DDD)在微服务时代成为了风口话题,而在 DDD 领域,我们常常看到命令查询与职责分离(Command and Qu ...

  3. 微服务架构在区块链BaaS平台中的实践

    前言 微服务架构是近几年互联网行业比较火的概念,凭借灵活可扩展.独立部署-等优势,逐步成为分布式架构中的主流.那么微服务架构和区块链又能擦出哪些神奇的火花?本期将从微服务架构概述.微服务架构在BaaS ...

  4. 美摄助力百度“度咔剪辑”,让知识创作更容易

    一直以来,短视频内容"泛娱乐化"情况严重,导致泛娱乐产能过剩.各大巨头为了培育新增长点,提升差异化竞争力,满足碎片化时代人们对知识内容的需求,开始纷纷布局"泛知识&quo ...

  5. 标准化思想及组装式架构在后端BFF中的实践

    进入互联网"下半场",靠"堆人力"的研发方式已经不再具备竞争力了,真正可行且有效的方式是让系统能力变得可沉淀.可组合复用.可灵活应对各种变化.在多业态.大规模定 ...

  6. Jetpack Compose 中的架构思想

    Jetpack Compose 中的架构总览 如果应用打算使用 Jetpack Compose 来开发,那么就可以跟以前的MVC.MVP.MVVM等乱七八糟的架构全部说拜拜,这些名词也将在Androi ...

  7. iOS 开发中的 Flux 架构模式

    本文讲的是iOS 开发中的 Flux 架构模式, 在半年前,我开始在 PlanGrid iOS 应用程序中采用 Flux 架构(开发).这篇文章将会讨论我们从传统的 MVC 转换到Flux的动机,同时 ...

  8. 让小程序在自有App中启动的技术来了:mPaaS小程序架构深度解析

    简介:mPaaS 小程序框架作为一款 App 通用框架,帮助开发者面向自身的 App 实现小程序投放.不止如此,小程序代码仅需撰写一次,便可多端投放至自有 App.支付宝.钉钉甚至其他小程序开放平台. ...

  9. Delphi FMX正确设计和加载图片满足分布式跨平台App的性能需求-分布式跨平台App中美工图片的处理、上传下载、并发及客户端显示技术架构

    Delphi FMX正确设计和加载图片满足分布式跨平台App的性能需求 分布式跨平台App中美工图片的处理.上传下载.并发及客户端显示技术架构 [综合:客户端(内存耗用.设备屏幕的自动适配).服务端( ...

最新文章

  1. 河套酒业集团远程应用K/3系统案例解析
  2. No loop matching the specified signature and casting
  3. 使用Azure云原生构建博客是怎样一种体验?(上篇)
  4. tickcount()修改成小时分钟_银行核心系统24小时机制实现总结
  5. java按钮改变窗口大小_java – 当我们调整窗口大小时,Container中的元素如何调整?...
  6. 【BZOJ3681】Arietta,主席树优化网络流
  7. 下拉刷新和上拉加载更多
  8. 关于高效找工作的几条建议
  9. 拓端tecdat|R语言使用bootstrap和增量法计算广义线性模型(GLM)预测置信区间
  10. windows10下载安装jdk1.7教程
  11. 汉字笔画动图怎么做_怎么用flash制作汉字笔顺的动画
  12. 常用DateUtil
  13. 在计算机里看不到硬盘的信息,检测不到硬盘,详细教您系统里找不到硬盘该怎么办...
  14. 如何评价光伏电站的运维能力
  15. 安大计算机学院汤进,“CCF合肥庐州论坛——认知计算研讨会”成功举办
  16. 个人所得税法应充分体现经济法原则
  17. 解决支付宝验证失败sign check fail: check Sign and Data Fail
  18. Vue中如何关闭语法检查
  19. SSL协议密钥交换过程理解
  20. C++风格指南(Google版)

热门文章

  1. NVIDIA TAO 工具包 (TAO Toolkit) 的部署和应用【LDR、LPR】
  2. Halcon显示正常比列长宽比的方法
  3. 传奇高手的一天(笑话)
  4. 如何解决电脑任务栏无故不见了的问题 ?
  5. 37岁转行JAVA真的很难
  6. 作为一个程序员,如何保持优秀
  7. 证件照片是红底,还是蓝底?这其中有什么讲究?
  8. Tomcat IP访问限制
  9. 【Books系列】之第二本书:大冰《我不》读书笔记和读后感
  10. Wangle源码分析:ServerBootstrap