博客 / 詳情

返回

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 {
// 實例化的 BDTZEditViewControllerDelegate
var 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等等都會收到這個事件,從而做出相應的狀態改變。

//APlugin
func sendAction(model: Any?) { 
       let action = BDTZClipSeleteAction.init(event: .selected, type: .sticker, actionTarget: model)
       editViewController?.pluginDispatcher.dispatch({ subscriber in
            subscriber.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/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 説

百度官方技術公眾號上線啦!

技術乾貨 · 行業資訊 · 線上沙龍 · 行業大會

招聘信息 · 內推信息 · 技術書籍 · 百度周邊

歡迎各位同學關注

user avatar fannaodeliushu 頭像 u_16316524 頭像
2 位用戶收藏了這個故事!

發佈 評論

Some HTML is okay.