探索实现iOS异步回调更加优雅的解决方案

作者: Tangentw | 来源:发表于2016-12-10 09:39 被阅读946次

    前言

    iOS的回调机制

    在iOS开发中,回调机制的实现主要有两种:

    1. 利用代理设计模式,制定好一套回调协议,在需要进行向外回调的实例中添加代理属性,当要进行回调的时候,直接调用自身代理属性的回调方法。在iOS官方API中大多数都是采用这套方案。
    2. 利用闭包(block),采用函数式编程思想,将一段代码作为参数传入方法中,在需要向外回调时,直接调用之前传进来的代码。
      由于使用第一种方法,代码量相对较多,而且代理关系容易变得很复杂,所以一般建议使用第二种方法进行回调,灵活方便。

    MVVM与MVP中的回调

    在使用MVVMMVP架构时,ViewController其实在充当着View的角色,当ViewController中有用户事件触发时,其立即将事件流传递给ViewModel(MVVM)Presenter(MVP),然后对事件进行处理、转换,如将用户按下按键的事件转换成向服务器发送HTTP请求事件,然后当接收到HTTP响应后再把事件转换为改变ViewController界面控件显示状态的事件,形成了一套事件的流式转换,所以,我们能看到,在这一条事件流转换的过程中,ViewController充当着事件的最初发送者以及最后接收者,ViewModelPresenter充当了事件的处理、转换者,在这过程中,必然要使用到回调机制。

    响应式、函数式框架中的回调

    一些第三方的响应式函数式框架,如ReactiveCocoaRxSwift等,能够更加优雅地实现MVVMMVP架构,在对事件回调的解决中,可谓是体现了代码的艺术之美,将事件流高度抽象,让开发者能够方便快捷地实现事件的转换。不仅如此,这些框架还提供了异步的解决方案,使开发者能够设定事件在流中能够跨线程传输。

    设计iOS异步回调更加优雅的轮子

    我受ReactiveCocoa以及RxSwift的启发,然后自己试着去实现一套iOS异步回调的解决方案,使得我在使用MVVMMVP架构小规模项目时没必要再使用ReactiveCocoaRxSwift等较为大型的框架,直接套上这小轮子即可~

    原理

    下面就是这个解决方案的执行逻辑图:


    如图,这个轮子的名字我起作Command,它可以横跨两层,分别是上层以及底层上层即对应了MVVMMVP架构中的View层,底层对应的是MVVM中的ViewModel层,MVP中的Presenter层。
    这两层分工明确,上层接收到用户的反馈事件,并将反馈事件所携带的初始数据传入底层,并且,将底层处理好的结果数据反映到界面上去,如修改某个空间的显示状态等;底层则专注于事件的处理、加工、转换,如用户点击了某个按钮,上层将接受到的点击事件传递给了底层,底层先将按钮的点击事件转换为网络请求事件,当网络请求完毕后,底层再将请求响应事件进行解析,转换成上层的显示事件,最后回调到上层,刷新显示。
    这就是上面所提到的例子的逻辑图:

    Command的作用就是这两层的Binder(粘合剂),专门负责沟通这两层,实现事件、数据的环形流向。

    使用

    简单的例子

    在介绍Command的源码前首先来看下如何使用Command实现MVVM架构模式:

    ViewModel

    class ViewModel {
        //  Commands
        let loginCommand: Command<(String?, String?), Bool>
        
        init() {
            self.loginCommand = Command { nameAndPassword, effect in
                let userName = nameAndPassword.0
                let password = nameAndPassword.1
                effect.onNext(userName == "Tangent" && password == "123")
                effect.onCompleted()
                return NopCancelable()
            }
        }
    }
    

    ViewModel类有一Command属性loginCommand,它的泛型参数为一个数据类型都为String的二元元组,代表用户登录时输入的用户名以及密码,另一个泛型参数为一个布尔类型值,代表用户登录是否成功;在loginCommand构造时,我们利用构造函数中闭包参数中的参数值nameAndPassword来接受用户的事件数据输入,然后进行相应的处理,最后再通过闭包参数的的参数值effect将结果发送出去。

    ViewController

    /**
       当用户按下登录按钮所触发的方法
     */
    func buttonTap(sender: AnyObject) {
        _ = self.viewModel.loginCommand.execute((userNameTextField.text, passwordTextField.text))
    }
    

    当用户按下登录按钮时,触发loginCommandexecute方法,并将用户输入的用户名以及密码传入Command中

    override func viewDidLoad() {
        super.viewDidLoad()
            
        //  装载视图
        self.setupViews()
        //  布局视图
        self.layoutViews()
    
        self.viewModel.loginCommand.handle(next: { result in
            //  登录验证后的回调
            print(result ? "登录成功" : "登录失败")
        })
    }
    

    我们在ViewController中的viewDidLoad方法中进行loginCommand的回调处理,需调用loginCommandhandle方法,传入处理闭包。

    异步回调

    以上,Command的大致使用方法就是这样,但是,上面展示的使用只是针对于同步的单线程(在主线程中),如果我们进行异步的操作,如果说指定处理数据所在的线程,或者指定处理回调所在的线程,我们可以在execute方法以及handle方法的参数中指定所在线程队列:

    //  指定回调处理所在的线程队列
    self.viewModel.loginCommand.handle(on:DispatchQueue.main, next: { result in /* ... */ })
    
    //  指定数据在处理时所在的线程队列
    _ = self.viewModel.loginCommand.execute((userName, password), on: DispatchQueue.global())
    

    取消操作

    在有必要时,你可以取消正在进行的数据处理操作,这时我们需要了解一个协议Cancelable,我们使用它的方法cancel来取消一个Command所正在进行的操作。
    在我们执行了execute方法后,我们会接收到一个返回值,这个返回值是Cancelable协议的实例,我们可以通过它来进行对指定Command操作的取消:

    let cancelable = self.viewModel.loginCommand.execute((userName, password))
            
    //  某个时候
    cancelable.cancel()
    

    在取消操作的时候,我希望能进行一些取消前有必要的动作,比如回收资源、关闭文件流等等,我们可以在构建Command的时候在其构造函数的闭包参数中传入一个返回指定操作的Cancelable:

    self.loginCommand = Command { nameAndPassword, effect in
        //  login ...
        return TodoCancelable {
            //  Todo ...
        }
    }
    

    上面我们传入了一个TodoCancelable类型的Cancelable,我们可以在构造它的时候传入要在取消时执行的动作,也可以通过它的方法addTodo添加动作。
    假如我们不需要在取消时做任何动作,我们可以直接返回一个NopCancelable实例,就像刚刚在上面所展示的简单例子一样。

    源码编写

    Event

    //  MARK: - Event
    enum Event<Element> {
        case next(Element)
        case error(Error)
        case completed
    }
    
    extension Event {
        var element: Element? {
            switch self {
            case let .next(element):
                return element
            default:
                return nil
            }
        }
    }
    

    顾名思义,它代表的是回调处理的事件,分别有nexterrorcompleted

    • next: 表明command已经完成了一项任务,并把完成的结果通过枚举关联值传递出来
    • error: 表明在执行的过程中有异常抛出了,并把异常类通过枚举关联值传递出来
    • completed: 表明command整个任务处理已经完成

    注:当error、completed事件发出时,command会自动调用cancelable进行取消操作

    Cancelable

    //  MARK: - Cancelable
    protocol Cancelable {
        
        var isCancel: Bool { get }
        
        func cancel()
    }
    
    class NopCancelable: Cancelable {
        
        var isCancel: Bool = false
        
        func cancel() {
            objc_sync_enter(self)
            isCancel = true
            objc_sync_exit(self)
        }
    }
    
    class TodoCancelable: Cancelable {
        
        var isCancel: Bool = false
        
        private var todos: [() -> ()] = []
        
        func addTodo(_ todo: @escaping () -> ()) {
            self.todos.append(todo)
        }
        
        func cancel() {
            objc_sync_enter(self)
            isCancel = true
            for todo in self.todos { todo() }
            objc_sync_exit(self)
        }
        
        init() {}
        
        init(_ todo: @escaping () -> ()) {
            self.todos.append(todo)
        }
        
    }
    

    这里有两个类实现了Cancelable协议,他们的作用我在上面已经说明了,在调用cancel方法的时候也进行了同步的操作,做到了线程的安全。

    Effect

    //  MARK: - Effect
    protocol EffectType {
        
        associatedtype E
        
        func on(_ event: Event<E>)
    }
    
    extension EffectType {
        
        final func onNext(_ element: E) {
            self.on(.next(element))
        }
        
        final func onCompleted() {
            self.on(.completed)
        }
        
        final func onError(_ error: Error) {
            self.on(.error(error))
        }
    }
    
    struct Effect<Element>: EffectType {
       
        typealias E = Element
        typealias EventHandler = (Event<Element>) -> ()
    
        let eventHandler: EventHandler
        
        init(eventHandler: @escaping EventHandler) {
            self.eventHandler = eventHandler
        }
        
        func on(_ event: Event<E>) {
            self.eventHandler(event)
        }
    }
    

    Effect可以说是事件的发送者,我们在数据处理得出结果后,就通过它将结果发送出去。这里effect发送的事件与上面提到的Event相对应。

    Command

    //  MARK: - Command
    protocol CommandType {
        
        associatedtype I
        associatedtype O
        
        init(_ todo: @escaping (I, Effect<O>) throws -> Cancelable)
        
        func execute(_ input: I, on: DispatchQueue) -> Cancelable
        
        func handle(on: DispatchQueue, next: ((O) -> ())?, error: ((Error) -> ())?, completed:(() -> ())?)
    }
    
    class Command<In, Out>: CommandType {
        typealias I = In
        typealias O = Out
        
        let todo: (I, Effect<O>) throws -> Cancelable
        
        var eventHandler: ((Event<O>) -> ())?
        
        required init(_ todo: @escaping (I, Effect<O>) throws -> Cancelable) {
            self.todo = todo
        }
        
        func execute(_ input: In, on: DispatchQueue = DispatchQueue.main) -> Cancelable {
            
            let cancelable = TodoCancelable()
            
            let effect = Effect<O> { event in
                if cancelable.isCancel { return }
                self.eventHandler?(event)
                if case .error = event { cancelable.cancel() }
                if case .completed = event { cancelable.cancel() }
            }
            
            on.async {
                do {
                    let mCancelable = try self.todo(input, effect)
                    cancelable.addTodo {
                        mCancelable.cancel()
                    }
                } catch let error {
                    effect.onError(error)
                }
            }
            
            return cancelable
        }
        
        func handle(on: DispatchQueue = DispatchQueue.main, next: ((Out) -> ())? = nil, error: ((Error) -> ())? = nil, completed: (() -> ())? = nil) {
            let eventHandler: (Event<O>) -> () = { event in
                on.async {
                    switch event {
                    case .next(let element):
                        next?(element)
                    case .completed:
                        completed?()
                    case .error(let err):
                        error?(err)
                    }
                }
            }
            self.eventHandler = eventHandler
        }
    }
    

    这个就是重头戏,也就是这篇文章要讲的主角Command,其他它的思想挺简单,就是讲我们制定的处理数据的动作先包装并存储好,当我们要执行操作的时候,就把原本存好的动作拿出来进行调用,并传入执行的数据。
    这里有几点需要拿出来探讨下:

    • 关于Effect以及异步的探讨: command中effect的加入可能会使得它的使用难度有所增加,因为结果并不是在todo闭包中直接返回,而是通过todo闭包中的effect参数将结果发送出去,其实这样设计的思想是为了切合异步多线程,而如果我们在设计command时只针对单线程环境,就大可不必那么麻烦,直接在todo闭包中计算完,返回即可。
    • 关于Cancelable的探讨: 我们在执行完command的execute方法后,其会返回一个Cancelable,而我们在构建Command传入todo闭包时,要求我们要传入一个Cancelable,但是,其两者并不指向同一实例。因为考虑到存在异步调用的情况,所以在todo执行完毕之前,可能execute已经返回了Cancelable了,所以我在execute调用的时候就创建了一个TodoCancelable,然后当todo返回Cancelable时,将其与上面的TodoCancelable进行绑定,使它们能够做到同步取消。
    • 关于同步执行的探讨: 如果我们没有指定command特定的执行或回调线程,其默认是运行在主线程上,但是,这并不代表当我们执行execute方法时,todo的内容就会立即执行并回调出来,而后再继续执行execute方法往下的内容,因为源码中,就算todo执行时在主线程队列中,但是它是被添加到GCD中的,意思是todo的执行会在下一次事件循环到来时才会去执行。

    到此,Command的源代码就展示完毕了。

    拓展

    首先,我们利用函数式编程,将command植入到每个对象中:

    //  MARK: - Commands
    struct Commands<Base> {
        
        let base: Base
        
        init(_ base: Base) {
            self.base = base
        }
    }
    
    protocol CommandCompatible {
        
        associatedtype CompatibleType
        
        var cm: Commands<CompatibleType> { get set }
    }
    
    extension CommandCompatible {
        
        typealias T = Self
        
        var cm: Commands<T> {
            get {
                return Commands(self)
            }
            
            set {
                //  保证能够修改cm中的属性,如object.cm.xxxCommand = xxx
            }
        }
    }
    
    extension NSObject: CommandCompatible {}
    

    由于在扩展(extension)中无法添加存储属性,我们需要利用到关联对象:

    //  MARK: - AssociatedObject
    extension NSObject {
        func addObjectProperty(_ object: AnyObject, key: UnsafeRawPointer) {
            objc_setAssociatedObject(self, key, object, .OBJC_ASSOCIATION_RETAIN)
        }
        
        func getProperty(_ key: UnsafeRawPointer) -> Any {
            return objc_getAssociatedObject(self, key)
        }
    }
    

    对按钮的拓展

    按钮属于UIControl类,我们可以先构建一个针对于UIControlCommand中介类:

    //  MARK: - ControlEventCommand
    class ControlEventCommand {
        
        let command: Command<Void, Any>
        let control: UIControl
        let event: UIControlEvents
        
        init(control: UIControl, command: Command<Void, Any>, event: UIControlEvents) {
            self.control = control
            self.command = command
            self.event = event
            control.addTarget(self, action: #selector(ControlEventCommand.onEvent), for: event)
        }
        
        deinit {
            self.control.removeTarget(self, action: #selector(ControlEventCommand.onEvent), for: self.event)
        }
        
        @objc func onEvent() {
            _ = self.command.execute(())
        }
        
    }
    

    然后我们就可以对某个按钮的点击链接到command中:

    fileprivate var tapCommandKey = 0
    extension Commands where Base: UIButton {
        var tapCommand: Command<Void, Any>? {
            set {
                if let value = newValue {
                    let cec = ControlEventCommand(control: base, command: value, event: .touchUpInside)
                    base.addObjectProperty(cec, key: &tapCommandKey)
                }
            }
            
            get {
                let cec = base.getProperty(&tapCommandKey) as? ControlEventCommand
                return cec?.command
            }
        }
    }
    

    怎么使用呢?

    ViewModel

    class ViewModel {
        
        let buttonTapCommand: Command<Void, Any>
        
        init() {
            self.buttonTapCommand = Command { _, effect in
                //  todo ...
                return NopCancelable()
            }
        }
    }
    

    ViewController

    override func viewDidLoad() {
        super.viewDidLoad()
            
        //  装载视图
        self.setupViews()
        //  布局视图
        self.layoutViews()
    
        self.uploadButton.cm.tapCommand = self.viewModel.buttonTapCommand
            
        self.viewModel.buttonTapCommand.handle(next: { output in
            //  todo...
        })
    }
    

    对UITextField的拓展

    我们想要在textField中的文字改变是就立即触发command的操作,首先我们知道,想要监听textField文字的输入,我们可以通过通知机制,所以我们可以构造一个针对于通知的command中介者:

    //  MARK: - NotificationCommand
    class NotificationCommand<T> {
        
        let command: Command<T, Any>
        let mapper: (NSNotification) -> T
        
        init(object: Any, notificationName: NSNotification.Name?, command: Command<T, Any>, mapper: @escaping (NSNotification) -> T) {
            self.command = command
            self.mapper = mapper
            NotificationCenter.default.addObserver(self, selector: #selector(NotificationCommand.handleNotification(notification:)), name: notificationName, object: object)
        }
        
        deinit {
            NotificationCenter.default.removeObserver(self)
        }
        
        @objc func handleNotification(notification: NSNotification) {
            _ = self.command.execute(self.mapper(notification))
        }
    }
    

    然后我们就可以对textField进行扩展了:

    fileprivate var textCommandKey = 0
    extension Commands where Base: UITextField {
        var textCommand: Command<String, Any>? {
            set {
                if let value = newValue {
                    let notificationCommand = NotificationCommand(object: base, notificationName: NSNotification.Name.UITextFieldTextDidChange, command: value, mapper: { notification in
                        (notification.object as! UITextField).text ?? ""
                    })
                    base.addObjectProperty(notificationCommand, key: &textCommandKey)
                }
            }
            
            get {
                let notificationCommand = base.getProperty(&textCommandKey) as? NotificationCommand<String>
                return notificationCommand?.command
            }
        }
    }
    

    用法也与上面的按钮拓展差不多,就不多说了。

    参考资料

    RxSwift -- Github链接
    ReactiveCocoa -- Github链接

    相关文章

      网友评论

      本文标题:探索实现iOS异步回调更加优雅的解决方案

      本文链接:https://www.haomeiwen.com/subject/xawymttx.html