RxCocoa 源码解析——代理转发

作者: Sheepy | 来源:发表于2017-03-11 21:26 被阅读1925次

    平常我们使用 RxSwift 的时候,一般不会去直接使用 delegate,譬如要处理 tableView 的点击事件,我们会这样:tableView.rx.itemSelected.subscribe(onNext: handleSelectedIndexPath),这跟先设置一个 delegate,然后在 delegate 的tableView(_:didSelectRowAt:)方法中调用handleSelectedIndexPath的效果是一样的。那这个过程到底是如何进行的呢?我们进入 RxCocoa 的 UITableView+Rx.swift 文件来一探究竟,这个文件中不仅有itemSelected,还有诸如itemDeselecteditemAccessoryButtonTappeditemInserteditemDeleteditemMoved等等一系列对应 tableView delegate 的包装方法,本文就以itemSelected为例,其他的都是相同的原理。为便于理解,我会给源码加一点中文注释,:

    /**
     Reactive wrapper for `delegate` message `tableView:didSelectRowAtIndexPath:`.
     */
    public var itemSelected: ControlEvent<IndexPath> {
        // delegate: PublishSubject<[AnyObject]>,[AnyObject]表示 selector 的参数列表
        let source = self.delegate.observe(#selector(UITableViewDelegate.tableView(_:didSelectRowAt:)))
            .map { a in
                // 转化第二个参数的类型为 IndexPath
                return try castOrThrow(IndexPath.self, a[1])
        }
    
        // 包装成一个 ControlEvent 返回,ControlEvent 其实只是在 source 外套了层壳,保证操作会在主线程进行而已
        return ControlEvent(events: source)
    }
    
    

    这个方法是写在extension Reactive where Base: UITableView里的,想必大家应该清楚这只是为了给扩展加一个命名空间,Reactive<Base>是一个范型 struct,它有一个 base 属性,Reactive 对外暴露的方法实际上都会转发给 base。这块如果大家不清楚的话可以看一下 Reactive.swift 文件,由于不是本文的重点就不细说了,可以理解为extension Reactive where Base: UITableView中的方法其实就是给UITableView加的扩展方法。

    DelegateProxy

    几个关键的地方我都加了中文注释,大家应该能明白。值得注意的是,这个方法里出现的self.delegate属性并不在本文件中,那我们推测应该是在别的 Reactive extension 中,跳到 UIScrollView+Rx.swift 看一下,果不其然:

    extension Reactive where Base: UIScrollView {
        // ...
        
        public var delegate: DelegateProxy {
            return RxScrollViewDelegateProxy.proxyForObject(base)
        }
        // ...
    

    原来 delegate 啊是一个 DelegateProxy 类型(代理的代理^ ^)……这个proxyForObject方法显然是接收一个对象(本文中这个对象是个 tableView 实例),然后返回其代理。那我们看一下 RxScrollViewDelegateProxy 这个类,却并没有发现proxyForObject这个方法,这时我看了眼它的类声明:

    /**
     For more information take a look at `DelegateProxyType`.
    */
    public class RxScrollViewDelegateProxy
        : DelegateProxy
        , UIScrollViewDelegate
        , DelegateProxyType {
    

    注释上写着 “For more information take a look at DelegateProxyType.”,想必我们能从 DelegateProxyType 这个协议里发现点什么,嗯,果然在这个协议的 extension 里发现了proxyForObject

    extension DelegateProxyType {
        /**
         Returns existing proxy for object or installs new instance of delegate proxy.
    
         - parameter object: Target object on which to install delegate proxy.
         - returns: Installed instance of delegate proxy.
        */
        public static func proxyForObject(_ object: AnyObject) -> Self {
            MainScheduler.ensureExecutingOnScheduler()
    
            let maybeProxy = Self.assignedProxyFor(object) as? Self
    
            let proxy: Self
            if let existingProxy = maybeProxy {
                proxy = existingProxy
            }
            else {
                proxy = Self.createProxyForObject(object) as! Self
                Self.assignProxy(proxy, toObject: object)
                assert(Self.assignedProxyFor(object) === proxy)
            }
    
            let currentDelegate: AnyObject? = Self.currentDelegateFor(object)
    
            if currentDelegate !== proxy {
                proxy.setForwardToDelegate(currentDelegate, retainDelegate: false)
                assert(proxy.forwardToDelegate() === currentDelegate)
                Self.setCurrentDelegate(proxy, toObject: object)
                assert(Self.currentDelegateFor(object) === proxy)
                assert(proxy.forwardToDelegate() === currentDelegate)
            }
            
            return proxy
        }
    
    

    这个方法看着很长,其核心是通过assignedProxyFor(object)去拿 tableView 实例的关联代理 proxy,如果没有的话,就先用createProxyForObject(object)创建一个代理,然后用assignProxy(proxy, toObject object)将 proxy 设置为 tableView 实例的关联对象。如果这个 tableView 实例还未设置 delegate,就调用setCurrentDelegate(proxy, toObject: object)将 tableView 的 delegate 设置为 proxy,最后返回 proxy。这里使用的几个方法并没有在协议扩展里实现,而是分别在DelegateProxyRxScrollViewDelegateProxy中实现的,先看DelegateProxy中:

    public class func createProxyForObject(_ object: AnyObject) -> AnyObject {
        return self.init(parentObject: object)
    }
    
    public class func assignedProxyFor(_ object: AnyObject) -> AnyObject? { 
        // 得到关联代理
        let maybeDelegate = objc_getAssociatedObject(object, self.delegateAssociatedObjectTag())
        return castOptionalOrFatalError(maybeDelegate.map { $0 as AnyObject })
    }
    
    public class func assignProxy(_ proxy: AnyObject, toObject object: AnyObject) {
        precondition(proxy.isKind(of: self.classForCoder()))
        // 设置关联代理
        objc_setAssociatedObject(object, self.delegateAssociatedObjectTag(), proxy, .OBJC_ASSOCIATION_RETAIN)
    }
    

    这些都很好理解,然后setCurrentDelegate的实现在RxScrollViewDelegateProxy中,值得一提的是,createProxyForObjectRxScrollViewDelegateProxy中也被重写了,我们来看一下:

    public class RxScrollViewDelegateProxy
        : DelegateProxy
        , UIScrollViewDelegate
        , DelegateProxyType {
    
        // ...
        
        public override class func createProxyForObject(_ object: AnyObject) -> AnyObject {
            let scrollView = (object as! UIScrollView)
    
            // 调用 UITableView 的 createRxDelegateProxy 方法,返回一个 RxTextViewDelegateProxy 实例
            return castOrFatalError(scrollView.createRxDelegateProxy())
        }
    
        public class func setCurrentDelegate(_ delegate: AnyObject?, toObject object: AnyObject) {
            let scrollView: UIScrollView = castOrFatalError(object)
            scrollView.delegate = castOptionalOrFatalError(delegate)
        }
        
        // ...
    }
    
    extension UITableView {
    
        public override func createRxDelegateProxy() -> RxScrollViewDelegateProxy {
            return RxTableViewDelegateProxy(parentObject: self)
        }
        // ...
    

    对于 tableView 来说,createProxyForObject返回的实际上是一个RxTableViewDelegateProxy,我们看一下它的声明:

    public class RxTableViewDelegateProxy
        : RxScrollViewDelegateProxy
        , UITableViewDelegate 
    

    绑定 selector 和 subject

    好的,现在我们已经知道开头itemSelected中出现的self.delegate是什么了,接下来看看self.delegate.observe又做了啥,我们回到DelegateProxy中:

    open class DelegateProxy : _RXDelegateProxy {
        
        private var subjectsForSelector = [Selector: PublishSubject<[AnyObject]>]()
        // ...
        // 将 selector(返回值是 void) 和一个 subject 关联,通过 subject 发送事件给 obsevers
        public func observe(_ selector: Selector) -> Observable<[AnyObject]> {
            if hasWiredImplementation(for: selector) {
                print("Delegate proxy is already implementing `\(selector)`, a more performant way of registering might exist.")
            }
    
            if !self.responds(to: selector) {
                rxFatalError("This class doesn't respond to selector \(selector)")
            }
    
            // 已经存在对应这个 selector 的 subject
            let subject = subjectsForSelector[selector]
            
            if let subject = subject {
                return subject
            }
            else {
                // 尚未创建该 selector 对应的 subject,先创建一个
                let subject = PublishSubject<[AnyObject]>()
                // 缓存到字典中
                subjectsForSelector[selector] = subject
                return subject
            }
        }
        // ...
    

    注释已经写清楚了,这个方法第一次会把 selector 和一个新建的 subject 绑定,缓存到字典中,之后就通过 selector 来取对应的 subject。接着我在这个方法的下面看到了另一个方法:

    // 父类 _RXDelegateProxy 重写了 forwardInvocation 方法,forwardInvocation 中会调用本方法
    open override func interceptedSelector(_ selector: Selector, withArguments arguments: [Any]) {
        // selector 对应的 subject 发送一个包含 selector 参数列表的事件
        subjectsForSelector[selector]?.on(.next(arguments as [AnyObject]))
    }
    
    

    这个方法接收一个 selector 和其参数列表,以 selector 为 key 找到对应的 subject,subject 发射一个包含 selector 参数列表的 next 事件。显然这个方法的调用时机是个关键,这里就用到了 Runtime 的消息转发(Runtime 相关的东西网上有很多资料,也不是本文的重点,我就不细说了),我在注释里也写了,DelegateProxy的父类_RXDelegateProxy重写了forwardInvocation方法,在里面调用了interceptedSelector方法。这样一来,当某个 selector 要被调用时,由于 proxy 对象没有对应实现,最后会走 forwardInvocation 把消息转发给 interceptedSelector,对应的 subject 发送包含参数列表的事件给所有 observer,整个过程就走通了。

    小结

    我在文中讲述了自己阅读源码的心路历程,如何按图索骥,一步步理清整个过程,兴许对那些想要阅读源码却不知如何入手的朋友会有帮助。

    水平有限,如有错漏,欢迎指出~

    相关文章

      网友评论

      • 系统盘:我想问下 界面之间类似于老方法的代理之类的rxswift怎么用
      • 2d4e62c28023:大神 再问一个tableview数据绑定的问题, 貌似rxDataSource里面没有类似原生tableview.reloadData 的方法 ,每次只能设置datasource = nil 然后重新绑定新的数据 这样对下拉刷新这样的功能做起来有点棘手 ,有什么办法可以解决这个问题呢 ?还请不吝赐教:relieved:
        2d4e62c28023:@Sheepy 看到了 解决了我的问题非常感谢!现在碰到一个新的问题,我在一个含有tableview和textFiled的控制器里,每次点了textField弹出键盘后,pop这个控制器,返回上层,就奔溃了,打印如下:assertion failed: Delegate was changed from time it was first set. Current Optional(<_TtGC7RxCocoa49RxTableViewReactiveArrayDataSourceSequenceWrapperGSaGSqC14Diction_IPhone28DXFFilterConditionValueModel___: 0x170451ca0>), and it should have been <RxCocoa.RxTableViewDataSourceProxy: 0x17066b880>: file /Users/quezhen/Desktop/newNewCODE/Diction_iPhone/Diction_IPhone/Pods/RxCocoa/RxCocoa/Common/DelegateProxyType.swift, line 208----------------------------但是我要是不点textFiled 不弹出键盘,pop返回 就不会崩溃,望大神点拨!谢谢
        2d4e62c28023:OK,谢谢了,赶紧去拜读一下
        Sheepy:@Rx头疼 我写过一篇 Rx 冷暖自知的文章,有讲到刷新这个问题
      • 2d4e62c28023:大神你好 ,我是小密圈提问了过来看的。想知道原生的UITableViewDataSource里面返回一个侧边索引表的代理方法 sectionIndexTitles(for tableView: UITableView) -> [String]? 在Rx里面用什么姿势写啊 ?困惑一下午了,因为项目是用Rx写的 今天下午卡这里了 ,不知道怎么实现。想用原生的实现 但是设置tableview.dataSource = self 然后写原生的代理方法就崩了。这里问下 Rx该怎么实现呢?真心求助了。。。。。。。。。。。。。
        2d4e62c28023:@Sheepy 大神能研究下怎么用rx实现右侧的索引
        Sheepy:dataSource 是 RxTableViewSectionedReloadDataSource 的一个实例。
        Sheepy:额……dataSource.sectionIndexTitles = { _ in return ["title1", "title2"] }
      • 1423f9a80aa5:厉害了

      本文标题:RxCocoa 源码解析——代理转发

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