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

{"type":"doc","content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"blockquote","content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","marks":[{"type":"strong","attrs":{}}],"text":"导读","attrs":{}},{"type":"text","text":":为了应对视频编辑类工具应用复杂的交互,度咔iOS借鉴了Flux架构模式的设计思想,参考有向无环图的拓扑概念,将事件进行集中化管理,从开发体验上实现了舒适清爽、容易驾驭的“单向流”模式;在这种调度模式下,事件的变化和追踪变得清晰可预测,并且显著的增加了业务的可扩展性。","attrs":{}}]}],"attrs":{}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","marks":[{"type":"italic","attrs":{}}],"text":"全文6882字,预计阅读时间18分钟。","attrs":{}}]},{"type":"heading","attrs":{"align":null,"level":2},"content":[{"type":"text","marks":[{"type":"strong","attrs":{}}],"text":"一、架构背景","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"视频编辑工具类应用往往交互复杂,大部分操作是在同一个主界面上进行,而这个界面同时存在较多的视图区域(预览区、轴区、undo redo、操作面板等等),每个区域既要接收用户手势,又要跟随用户操作联动更新状态。同时除支持主场景编辑功能外,还要同时支持其他特色功能,比如度咔的通用编辑、快速剪辑、主题模板等,都需要使用预览和编辑功能;于是对架构的可扩展和可复用能力自然有了很高的要求。","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"经过调研,度咔iOS最终借鉴了Flux架构模式的设计思想,参考有向无环图的拓扑概念,将事件进行集中化管理,从开发体验上实现了舒适清爽、容易驾驭的“单向流”模式;在这种调度模式下,事件的变化和追踪变得清晰可预测,并且显著的增加了业务的可扩展性。","attrs":{}}]},{"type":"heading","attrs":{"align":null,"level":2},"content":[{"type":"text","marks":[{"type":"strong","attrs":{}}],"text":"二、播放预览复用","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"度咔通用编辑以及很多衍生工具、功能都需要依赖于预览、素材编辑这一类基础能力。","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"比如下列这些功能都依赖于同一套预览播放逻辑,需要将这些基础能力抽象为一个base控制器。","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"image","attrs":{"src":"https://static001.geekbang.org/infoq/da/da13d8b8c3653cca22013ed35555cade.png","alt":null,"title":null,"style":[{"key":"width","value":"75%"},{"key":"bordertype","value":"none"}],"href":null,"fromPaste":true,"pastePass":true}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","marks":[{"type":"strong","attrs":{}}],"text":"baseVC结构为:","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"image","attrs":{"src":"https://static001.geekbang.org/infoq/75/75413f07d458fb45fbb604afea828de2.png","alt":null,"title":null,"style":[{"key":"width","value":"75%"},{"key":"bordertype","value":"none"}],"href":null,"fromPaste":true,"pastePass":true}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"horizontalrule","attrs":{}},{"type":"heading","attrs":{"align":null,"level":2},"content":[{"type":"text","marks":[{"type":"strong","attrs":{}}],"text":"三、功能模块复用","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"预览播放复用的问题解决了,如何在这套逻辑上添加各样的素材编辑功能,比如贴纸、文字、滤镜等功能,并且使这些功能与VC解耦,最终达到复用的目的?","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"最终我们使用插拔式设计理念,把每一个子功能抽象成一个plugin,采用直接调用依赖层的方式把controller、view、timeline、streamingContext、liveWindow 这写90%场景下会用到的属性通过weak直接赋值给plugin。","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"codeblock","attrs":{"lang":"text"},"content":[{"type":"text","text":"protocol BDTZEditPlugin: NSObjectProtocol {\n // 组织控制器\n var editViewController: BDTZEditViewController? { get set }\n // 所有添加到控制器View上的控件 加到这个View上,解决层级问题\n var mainView: BDTZEditLevelView? { get set }\n // 编辑场景的时间轴实体,由轨道组成,可以有多个视频轨道和音频轨道,由视频轨道决定长度\n var timeline: Timeline? { get set }\n // 流媒体上下文 包含时间线、预览窗口、采集、资源包管理等相关信息集合的对象\n var streamingContext: StreamingContext? { get set }\n // 视频预览窗口控件\n var liveWindow: LiveWindow? { get set }\n\n /// 插件初始化\n func pluginDidLoad()\n\n /// 插件卸载\n func pluginDidUnload()\n}\n","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"只要实现这个协议,并且通过调用baseVC的add:方法添加plugin后,那么相应的plugin就会拿到对应的属性进行调用,避免使用单例或者通过层层回调到VC去处理。","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"codeblock","attrs":{"lang":"text"},"content":[{"type":"text","text":" func addPlugin(_ plugin: BDTZEditPlugin) {\n\n plugin.pluginWillLoad()\n\n plugin.editViewController = self\n\n plugin.mainView = self.view\n\n plugin.liveWindow = liveWindow\n\n plugin.streamingContext = streamingContext\n\n plugin.timeline = timeline\n\n if plugin.conforms(to: BDTZEditViewControllerDelegate.self) {\n pluginDispatcher.add(subscriber: plugin as! BDTZEditViewControllerDelegate)\n }\n plugin.pluginDidLoad()\n }\n\n func removePugin(_ plugin: BDTZEditPlugin) {\n\n plugin.pluginWillUnload()\n\n plugin.editViewController = nil\n\n plugin.mainView = nil\n\n plugin.liveWindow = nil\n\n plugin.streamingContext = nil\n\n plugin.timeline = nil\n\n if plugin.conforms(to: BDTZEditViewControllerDelegate.self) {\n pluginDispatcher.remove(subscriber: plugin as! BDTZEditViewControllerDelegate)\n }\n plugin.pluginDidUnload()\n }\n","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"plugin是具体功能和VC之间的一个中间层,可以接受VC的生命周期事件、预览播放事件、拿到VC中的关键对象、调用VC的内部所有public接口能力。作为插在VC上的一个独立子功能单元,具有编辑能力、素材能力、网络UI交互等能力。","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"plugin分为service层和UI层,同时在设计之初,基于该架构的plugin不仅仅能在度咔app内使用,厂内其他app仅需要极少工作量就能立即接入plugin。","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"image","attrs":{"src":"https://static001.geekbang.org/infoq/63/63fcb50b672e93b44b7b8ac4578c1d86.png","alt":null,"title":null,"style":[{"key":"width","value":"75%"},{"key":"bordertype","value":"none"}],"href":null,"fromPaste":true,"pastePass":true}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"所有功能能分散到插件中,按需组装和复用。","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"image","attrs":{"src":"https://static001.geekbang.org/infoq/12/12e1b8841ed7f3ff3856c1a2838427bc.png","alt":null,"title":null,"style":[{"key":"width","value":"75%"},{"key":"bordertype","value":"none"}],"href":null,"fromPaste":true,"pastePass":true}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"同时可以对外输出的不仅仅单个plugin、还是可以是多个plugin的组合。以封面功能为例,封面编辑是一个以coverVC为组织的控制器,它包含多个plugin,比如已存在的文字plugin和贴纸plugin;coverVC除了作为独立功能应用之外,把它包装成一个封面plugin只需少量数据对接代码(上图的通用剪辑数据对接plugin)就可以集成到通用剪辑VC,像堆乐高积木一样进行拼装组合。","attrs":{}}]},{"type":"heading","attrs":{"align":null,"level":2},"content":[{"type":"text","marks":[{"type":"strong","attrs":{}}],"text":"四、事件状态管理","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"编辑工具app因交互的复杂性非常依赖于状态更新,通常来说在iOS开发中通知对象状态变化一般采用以下几种方式:","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"bulletedlist","content":[{"type":"listitem","attrs":{"listStyle":null},"content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"Delegate","attrs":{}}]}]},{"type":"listitem","attrs":{"listStyle":null},"content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"KVO","attrs":{}}]}]},{"type":"listitem","attrs":{"listStyle":null},"content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"NotificationCenter","attrs":{}}]}]},{"type":"listitem","attrs":{"listStyle":null},"content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"Block","attrs":{}}]}]}],"attrs":{}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"这四种方式都可以管理状态的变化,但是都存在一些问题。Delegate和Block,往往会在组件之间创建强依赖关系;KVO 和 Notifications,会创建不可见的依赖项,如果某些重要消息被移除或更改,也很难被发现,从而降低应用稳定性。","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"即使是苹果的MVC模式,也只提倡数据层及其表示层的分离,没有提供任何工具代码、指导架构。","attrs":{}}]},{"type":"heading","attrs":{"align":null,"level":3},"content":[{"type":"text","marks":[{"type":"strong","attrs":{}}],"text":"4.1 为什么选择Flux架构模式","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"于是我们借鉴Flux架构模式的思想。Flux 是一种非常轻量级的架构模式,Facebook 将其用于客户端 Web 应用程序,用于避开MVC,支持单向数据流(后面也是列举的前端的mvc数据流向图)。核心思想是中心化控制,它让所有的请求与改变都只能通过 action 发出,统一 由 dispatcher 来分配。好处是 View 可以保持高度简洁,它不需要关心太多的逻辑,只需要关心传入的数据。中心化还控制了所有数据,发生问题时可以方便查询定位。","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"bulletedlist","content":[{"type":"listitem","attrs":{"listStyle":null},"content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"Dispatcher:处理事件分发,维持 Store 之间的依赖关系","attrs":{}}]}]},{"type":"listitem","attrs":{"listStyle":null},"content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"Store:负责存储数据和处理数据相关逻辑","attrs":{}}]}]},{"type":"listitem","attrs":{"listStyle":null},"content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"Action:触发 Dispatcher","attrs":{}}]}]},{"type":"listitem","attrs":{"listStyle":null},"content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"View:视图,负责显示用户界","attrs":{}}]}]}],"attrs":{}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"image","attrs":{"src":"https://static001.geekbang.org/infoq/ec/ec6475ddeaf32f746bed7e558c4de973.png","alt":null,"title":null,"style":[{"key":"width","value":"75%"},{"key":"bordertype","value":"none"}],"href":null,"fromPaste":true,"pastePass":true}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"通过上图可以看出来,Flux 的特点就是单向数据流:","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"numberedlist","attrs":{"start":1,"normalizeStart":1},"content":[{"type":"listitem","attrs":{"listStyle":null},"content":[{"type":"paragraph","attrs":{"indent":0,"number":1,"align":null,"origin":null},"content":[{"type":"text","text":"用户在 View 层发起一个 Action 对象给 D ispatcher","attrs":{}}]}]},{"type":"listitem","attrs":{"listStyle":null},"content":[{"type":"paragraph","attrs":{"indent":0,"number":2,"align":null,"origin":null},"content":[{"type":"text","text":"Dispatcher 接收到 Action 并要求 Store 做相应的更改","attrs":{}}]}]},{"type":"listitem","attrs":{"listStyle":null},"content":[{"type":"paragraph","attrs":{"indent":0,"number":3,"align":null,"origin":null},"content":[{"type":"text","text":"Store 做出相对应更新,然后发出一个 changeEvent","attrs":{}}]}]},{"type":"listitem","attrs":{"listStyle":null},"content":[{"type":"paragraph","attrs":{"indent":0,"number":4,"align":null,"origin":null},"content":[{"type":"text","text":"View 接收到 changeEvent 事件后,更新页面","attrs":{}}]}]}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"bulletedlist","content":[{"type":"listitem","attrs":{"listStyle":null},"content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"基本的MVC数据流","attrs":{}}]}]}],"attrs":{}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"image","attrs":{"src":"https://static001.geekbang.org/infoq/e1/e19cb28e8eeeb96dd03ae4e715735af3.png","alt":null,"title":null,"style":[{"key":"width","value":"75%"},{"key":"bordertype","value":"none"}],"href":null,"fromPaste":true,"pastePass":true}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"bulletedlist","content":[{"type":"listitem","attrs":{"listStyle":null},"content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"复杂的MVC数据","attrs":{}}]}]}],"attrs":{}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"image","attrs":{"src":"https://static001.geekbang.org/infoq/04/04a1119a8802ef2629517a4655c42c2d.png","alt":null,"title":null,"style":[{"key":"width","value":"75%"},{"key":"bordertype","value":"none"}],"href":null,"fromPaste":true,"pastePass":true}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"bulletedlist","content":[{"type":"listitem","attrs":{"listStyle":null},"content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"简单的Flux数据流","attrs":{}}]}]}],"attrs":{}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"image","attrs":{"src":"https://static001.geekbang.org/infoq/ec/ec6475ddeaf32f746bed7e558c4de973.png","alt":null,"title":null,"style":[{"key":"width","value":"75%"},{"key":"bordertype","value":"none"}],"href":null,"fromPaste":true,"pastePass":true}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"bulletedlist","content":[{"type":"listitem","attrs":{"listStyle":null},"content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"复杂Flux数据流","attrs":{}}]}]}],"attrs":{}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"image","attrs":{"src":"https://static001.geekbang.org/infoq/72/721d6738408a4c7065f06a369f2a8eeb.png","alt":null,"title":null,"style":[{"key":"width","value":"75%"},{"key":"bordertype","value":"none"}],"href":null,"fromPaste":true,"pastePass":true}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"相比MVC模式,Flux多出了更多的箭头跟图标,但是有个关键性的差别是:所有的箭头都指向一个方向,在整个系统中形成一个事件传递链。","attrs":{}}]},{"type":"heading","attrs":{"align":null,"level":3},"content":[{"type":"text","marks":[{"type":"strong","attrs":{}}],"text":"4.2","attrs":{}},{"type":"text","text":" ","attrs":{}},{"type":"text","marks":[{"type":"strong","attrs":{}}],"text":"应用Flux思想来实现状态管理","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"状态分为两种:","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"bulletedlist","content":[{"type":"listitem","attrs":{"listStyle":null},"content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"以组织控制器发出的事件产生状态变化,比如:控制器的生命周期ViewDidLoad()等等、基础编辑预览能力的回调,例如seek、progress、playState变化等等","attrs":{}}]}]},{"type":"listitem","attrs":{"listStyle":null},"content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"各个组件的之间事件传递产生的状态变化,下图中plugin协议抽象来描述上图中的Store作用","attrs":{}}]}]}],"attrs":{}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"image","attrs":{"src":"https://static001.geekbang.org/infoq/1d/1db1fe01aa9a8f00991bc6af77c738e1.png","alt":null,"title":null,"style":[{"key":"width","value":"75%"},{"key":"bordertype","value":"none"}],"href":null,"fromPaste":true,"pastePass":true}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"控制器持有EventDispatch能力的对象dispatcher,并通过这个dispatcher传递事件。","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","marks":[{"type":"strong","attrs":{}}],"text":"Dispatcher","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"codeblock","attrs":{"lang":"text"},"content":[{"type":"text","text":"class WeakProxy: Equatable {\n\n weak var value: AnyObject?\n init(value: AnyObject) {\n self.value = value\n }\n\n static func == (lhs: WeakProxy, rhs: WeakProxy) -> Bool {\n return lhs.value === rhs.value\n }\n}\n\nopen class BDTZActionDispatcher: NSObject {\n\n fileprivate var subscribers = [WeakProxy]()\n\n public func add(subscriber: T) {\n guard !subscribers.contains(WeakProxy(value: subscriber as AnyObject)) else {\n return\n }\n subscribers.append(WeakProxy(value: subscriber as AnyObject))\n }\n\n public func remove(subscriber: T) {\n let weak = WeakProxy(value: subscriber as AnyObject)\n if let index = subscribers.firstIndex(of: weak) {\n subscribers.remove(at: index)\n }\n }\n\n public func contains(subscriber: T) -> Bool {\n var res: Bool = false\n res = subscribers.contains(WeakProxy(value: subscriber as AnyObject))\n return res\n }\n\n public func dispatch(_ invocation: @escaping(T) -> ()) {\n clearNil()\n subscribers.forEach {\n if let subscriber = $0.value as? T {\n invocation(subscriber)\n }\n }\n }\n\n\n private func clearNil() {\n subscribers = subscribers.filter({ $0.value != nil})\n }\n}\n","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"通过泛型的多重代理方式把事件分发给subscribers内部的对象(上面代码块中的 addPlugin:内部添加subscribers),当然也可以通过注册Block的方法去实现。","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","marks":[{"type":"strong","attrs":{}}],"text":"Dispatcher实例","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"声明一个protocol 继承要分发的能力","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"codeblock","attrs":{"lang":"text"},"content":[{"type":"text","text":"@objc protocol BDTZEditViewControllerDelegate: BDTZEditViewLifeCycleDelegate, StreamingContextDelegate, BDTZEditActionSubscriber {\n// BDTZEditViewLifeCycleDelegate 控制器声明周期\n// StreamingContextDelegate 预览编辑能力回调\n// BDTZEditActionSubscriber plugin之间的通讯协议\n}\n","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","marks":[{"type":"strong","attrs":{}}],"text":"控制器事件分发","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"codeblock","attrs":{"lang":"text"},"content":[{"type":"text","text":"public class BDTZEditViewController: UIViewController {\n// 实例化的 BDTZEditViewControllerDelegate\nvar pluginDispatcher = BDTZEditViewControllerDelegateImp()\n public override func viewDidAppear(_ animated: Bool) {\n super.viewDidAppear(animated)\n pluginDispatcher.dispatch { subscriber in\n subscriber.editViewControllerViewDidAppear?()\n }\n }\n\n public override func viewDidLoad() {\n super.viewDidLoad()\n /***省略部分代码**/\n setupPlugins()\n //放最后调用\n pluginDispatcher.dispatch { subscriber in\n subscriber.editViewControllerViewDidLoad?()\n }\n }\n /***...**/\n /// seek进度回调\n func didSeekingTimelinePosition(_ timeline: Timeline!, position: Int64) {\n pluginDispatcher.dispatch { subscriber in\n subscriber.didSeekingTimelinePosition?(timeline, position: position)\n }\n }\n /***...**/\n}\n","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","marks":[{"type":"strong","attrs":{}}],"text":"plugin之间事件传递","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"plugin之间的事件传递就要用到上面的BDTZEditActionSubscriber协议了。","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"codeblock","attrs":{"lang":"text"},"content":[{"type":"text","text":"@objc protocol BDTZEditAction {\n}\n@objc protocol BDTZEditActionSubscriber {\n @objc optional func update(action: BDTZEditAction)\n}\n","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"BDTZEditAction 是一个空协议,可以是任何类继承它来描述想要传递的任何信息。结合编辑工具的特点(虽然交互复杂但是素材类型和操作都是有限的)只需要少量的action就能描述所有状态。目前我们使用选中action、各种素材action、面板起落action、前进回退action等等这些事件来描述素材的添加、删除、移动、剪裁、保存草稿一些列的操作。我们以选中action(选中某个片段的事件)举例:","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"当APlugin 发出了一个选中事件,BPlugin、CPlugin等等都会收到这个事件,从而做出相应的状态改变。","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"codeblock","attrs":{"lang":"text"},"content":[{"type":"text","text":"//APlugin\nfunc sendAction(model: Any?) { \n let action = BDTZClipSeleteAction.init(event: .selected, type: .sticker, actionTarget: model)\n editViewController?.pluginDispatcher.dispatch({ subscriber in\n subscriber.update?(action: action)\n })\n}\n","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"codeblock","attrs":{"lang":"text"},"content":[{"type":"text","text":"//BPlugin\nextension BDTZTrackPlugin: BDTZEditActionSubscriber {\n func update(action: BDTZEditAction) {\n if let action = action as? BDTZClipSeleteAction {\n handleSelectActionDoSomething()\n }\n }\n}\n","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"当预览区的贴纸被选中,那么轴区也会随之被选中,底部区域也要切换成三级菜单。**一个action被派发以后,所有plugin都会收到它,对此action感兴趣的plugin会做出相应的状态变化。","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"**","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"image","attrs":{"src":"https://static001.geekbang.org/infoq/e6/e6899df2424a0fbbe8079bf87e9deaba.png","alt":null,"title":null,"style":[{"key":"width","value":"75%"},{"key":"bordertype","value":"none"}],"href":null,"fromPaste":true,"pastePass":true}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"horizontalrule","attrs":{}},{"type":"heading","attrs":{"align":null,"level":2},"content":[{"type":"text","marks":[{"type":"strong","attrs":{}}],"text":"五、总结","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"iOS也有参照flux思想设计的ReSwift框架,但是如果使用纯Flux模式来开发,缺点也非常明显:","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"numberedlist","attrs":{"start":1,"normalizeStart":1},"content":[{"type":"listitem","attrs":{"listStyle":null},"content":[{"type":"paragraph","attrs":{"indent":0,"number":1,"align":null,"origin":null},"content":[{"type":"text","text":"层级太多,极易产生大量的冗余代码。","attrs":{}}]}]},{"type":"listitem","attrs":{"listStyle":null},"content":[{"type":"paragraph","attrs":{"indent":0,"number":2,"align":null,"origin":null},"content":[{"type":"text","text":"老代码移植工作量巨大。","attrs":{}}]}]}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"对我们来说采用Flux 模式设计理念比某个特定的实现框架更重要,我们根据度咔业务的特点只是取其思想使用单层级结构,用来管理ViewController与Plugin抽象之间的关系和事件传递,而没有把View也加到层级中去,plugin内部可以使用MVC、MVVM等任何架构,只需要把通讯方式统一。","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"上面只是使用简单的例子介绍了编辑工具在Flux思想上的应用。但是在实际使用中还应该考虑:","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"numberedlist","attrs":{"start":1,"normalizeStart":1},"content":[{"type":"listitem","attrs":{"listStyle":null},"content":[{"type":"paragraph","attrs":{"indent":0,"number":1,"align":null,"origin":null},"content":[{"type":"text","text":"UI层级遮盖问题:插件中的某个View需要加到控制器View上,会造成控件层级遮盖问题。上面代码中的BDTZEditLevelView就是为了解决这个问题。","attrs":{}}]}]},{"type":"listitem","attrs":{"listStyle":null},"content":[{"type":"paragraph","attrs":{"indent":0,"number":2,"align":null,"origin":null},"content":[{"type":"text","text":"多线程问题:在开发中我们难免大量的线程异步处理任务,我们必须规定插件通讯之间的线程,Dispatcher内部也应该有线程管理的代码。","attrs":{}}]}]},{"type":"listitem","attrs":{"listStyle":null},"content":[{"type":"paragraph","attrs":{"indent":0,"number":3,"align":null,"origin":null},"content":[{"type":"text","text":"plugin依赖关系问题:Dispatcher还要维持plugin之间的依赖关系,比如一个action要APlugin先处理修改某些数据或者状态后,BPlugin再处理,可以采用加标等方式解决。","attrs":{}}]}]},{"type":"listitem","attrs":{"listStyle":null},"content":[{"type":"paragraph","attrs":{"indent":0,"number":4,"align":null,"origin":null},"content":[{"type":"text","text":"action膨胀问题:相对于API直接调用的方式,监听action虽然写更少的代码,但是容易造成action无限增多的情况,所以在定义action要考虑可扩展和结构化。","attrs":{}}]}]}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"blockquote","content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","marks":[{"type":"strong","attrs":{}}],"text":"参考链接:","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"[1]http://reswift.github.io/ReSwift/master/getting-started-guide.html","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"[2]https://facebook.github.io/flux/","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"[3]https://redux.js.org","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"[4]http://blog.benjamin-encz.de/post/real-world-flux-ios/?utm_source=swifting.io&utm_medium=web&utm_campaign=blog%20post","attrs":{}}]}],"attrs":{}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"推荐阅读:","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"link","attrs":{"href":"http://mp.weixin.qq.com/s?__biz=Mzg5MjU0NTI5OQ==&mid=2247503500&idx=1&sn=f84fd6f9e62feeb7b91dfd6f1f0d145d&chksm=c03efef0f74977e66f26f287912dc37c1f3cb0124c57483205345ad7645ea59bc3518e1c82e2&scene=21#wechat_redirect","title":"","type":null},"content":[{"type":"text","text":"|","attrs":{}}]},{"type":"link","attrs":{"href":"http://mp.weixin.qq.com/s?__biz=Mzg5MjU0NTI5OQ==&mid=2247504629&idx=1&sn=fd02d4e11bdaea0c800cd2d2f1ec9c1e&chksm=c03ee289f7496b9f2270a4bde6029d0ddcdfa170feecbe8b87a5acee2d1f82bda9ebca11ac10&scene=21#wechat_redirect","title":"","type":null},"content":[{"type":"text","text":"iOS 崩溃日志在线符号化实践","attrs":{}}]}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"link","attrs":{"href":"http://mp.weixin.qq.com/s?__biz=Mzg5MjU0NTI5OQ==&mid=2247503928&idx=1&sn=a595da431aad386e4210f886cbcddaca&chksm=c03ee044f7496952e70788ab0ba71d6acaaff6de4162d4fa3d3e872cfcbb4b34335b95f597cf&scene=21#wechat_redirect","title":"","type":null},"content":[{"type":"text","text":"|百度商业托管页系统高可用建设方法和实践","attrs":{}}]}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"link","attrs":{"href":"http://mp.weixin.qq.com/s?__biz=Mzg5MjU0NTI5OQ==&mid=2247503597&idx=1&sn=a9b60d17f310dc3b5ccf29d067d66111&chksm=c03efe91f74977872cc380f48edd6c97e7915bc2fd99c49ca9edfdbaf2f4d9484e40d05b5b86&scene=21#wechat_redirect","title":"","type":null},"content":[{"type":"text","text":"|AI 在视频领域运用—弹幕穿人","attrs":{}}]}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","marks":[{"type":"strong","attrs":{}}],"text":"---------- END ----------","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"百度 Geek 说","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"百度官方技术公众号上线啦!","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"技术干货 · 行业资讯 · 线上沙龙 · 行业大会","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"招聘信息 · 内推信息 · 技术书籍 · 百度周边","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"欢迎各位同学关注","attrs":{}}]}]}
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章