RxCocoa中对UIKit的Delegate的处理

RxCocoa(scrollView.rx.didScroll)

得益于RxCocoa对UIKit做了extension,我们使用UI组件的Rx封装时只需要调用rx属性,就可以访问到Rx框架的内容。比如需要订阅UIScrollView的滚动事件:

scrollView
    .rx.didScroll
    .subscribe(onNext: { () in 
        print("scroll view did scroll")
    })

But, why???

在原生的Cocoa框架中,要监听UIScrollView的滑动事件,需要通过实现UIScrollViewDelegate协议的scrollViewDidScroll函数。但是RxCocoa为何可以让用户通过简单的调用就得到了回调?UIScrollViewDelegate是如何被隐藏的?

rx属性是怎么来的?

Reactive&ReactiveCompatible

通过查看,rx属性是一个Reactive的类型。在Reactive.swift文件,可以看到它是一个泛型struct 。而rx是在一个ReactiveCompatibleprotocol里面定义的。

public struct Reactive<Base> {
    /// Base object to extend.
    public let base: Base

    /// Creates extensions with base object.
    ///
    /// - parameter base: Base object.
    public init(_ base: Base) {
        self.base = base
    }
}

/// A type that has reactive extensions.
public protocol ReactiveCompatible {
    /// Extended type
    associatedtype CompatibleType

    /// Reactive extensions.
    static var rx: Reactive<CompatibleType>.Type { get set }

    /// Reactive extensions.
    var rx: Reactive<CompatibleType> { get set }
}

extension ReactiveCompatible {
    /// Reactive extensions.
    public static var rx: Reactive<Self>.Type {
        get {
            return Reactive<Self>.self
        }
        set {
            // this enables using Reactive to "mutate" base type
        }
    }

    /// Reactive extensions.
    public var rx: Reactive<Self> {
        get {
            return Reactive(self)
        }
        set {
            // this enables using Reactive to "mutate" base object
        }
    }
}


import class Foundation.NSObject

/// Extend NSObject with `rx` proxy.
extension NSObject: ReactiveCompatible { }

可以看到Reactive的定义非常简单,只有一个base属性,而这个属性是一个泛型

在文件的最后一行,NSObject被声明了实现ReactiveCompatible,因此UIScrollView也实现了该协议。

查看ReactiveCompatible的定义以及其下方的extension就可以看出,rx属性的泛型是该类型本身,也就是说UISCrollViewrx属性就是一个UISCrollView泛型的Reactive结构体,base属性就是这个UIScrollView本身的实例**。

顺着往下看,可以在UIScrollView+Rx.swift文件里找到,是通过extensionwhere语法对UIScrollView泛型的Reactive添加的一个计算型属性

extension Reactive where Base: UIScrollView {
    ...
        ...
        ...
        /// Reactive wrapper for delegate method `scrollViewDidScroll`
        public var didScroll: ControlEvent<Void> {
            let source = RxScrollViewDelegateProxy.proxy(for: base).contentOffsetPublishSubject
            return ControlEvent(events: source)
        }
        ...
        ...
        ...
    }

从命名可以看出,RxScrollViewDelegateProxy是一个实现UIScrollViewDelegate的代理类型。

暂时不探究RxScrollViewDelegateProxyproxy(for:)函数,比较好理解,source是通过一个对应UISrollView实例获得的RxScrollViewDelegateProxy的一个UIScrollView的contentOffset事件广播,函数最后再把这个广播封装成一个ControlEvent类型实例,也就是被订阅的那个事件。

而关键就在于RxScrollViewDelegateProxy这个类型做了什么事情。

RxScrollViewDelegateProxy

继续查看RxScrollViewDelegateProxy.swift文件

/// For more information take a look at `DelegateProxyType`.
open class RxScrollViewDelegateProxy
    : DelegateProxy<UIScrollView, UIScrollViewDelegate>
    , DelegateProxyType 
, UIScrollViewDelegate {
    ...
    ...
    ...
    fileprivate var _contentOffsetPublishSubject: PublishSubject<()>?
    ...
    /// Optimized version used for observing content offset changes.
    internal var contentOffsetPublishSubject: PublishSubject<()> {
        if let subject = _contentOffsetPublishSubject {
            return subject
        }

        let subject = PublishSubject<()>()
        _contentOffsetPublishSubject = subject

        return subject
    }
    ...
    ...
    ...
}

RxScrollViewDelegateProxy继承了DelegateProxy,并实现了两个协议:

  • DelegateProxyType
  • UIScrollViewDelegate

显然这里就是UIScrollViewDelegate的藏身点,下面继续看在这里它是如何被黑盒化的。

在这个文件里可以找到原始回调的函数:

/// For more information take a look at `DelegateProxyType`.
    public func scrollViewDidScroll(_ scrollView: UIScrollView) {
        if let subject = _contentOffsetBehaviorSubject {
            subject.on(.next(scrollView.contentOffset))
        }
        if let subject = _contentOffsetPublishSubject {
            subject.on(.next(()))
        }
        self._forwardToDelegate?.scrollViewDidScroll?(scrollView)
    }

在回调的处理上,做了3件事:

  • 发送contentOffset的数值变化广播
  • 发送contentOffset变化的事件广播
  • 调用了_forwardToDelegate属性的另一个scrollViewDidScroll函数

可以看到两个广播事件应该是按需发送的,如果在没有订阅者的情况下,应该不会产生对应的属性。而在上面被订阅的的contentOffsetPublishSubject就是这个_contentOffsetPublishSubject

_forwardToDelegate应该是开发者在外部设置的delegate,也就是说:

即使开发者已经为一个UIScrollView设置了delegate(或者没有设置),也不会影响通过RxCocoa框架去订阅这个UIScrollView的事件。 而且可以有多个订阅者通过RxCocoa去订阅UIScrollView的回调事件,因为这里的Observable是广播类型。**

可见在RxCocoa中,每一个有事件订阅者的UIScrollView都有与之对应的RxScrollViewDelegateProxy实例。所以下一个问题就是:

代码RxScrollViewDelegateProxy.proxy(for: base)当中,proxy(for:)函数是如何把一个RxScrollViewDelegateProxy绑定到一个UIScrollView上的?

DelegateProxyType

可以查看到proxy(for:)函数被定义在DelegateProxyType协议里,通过extension实现。

/// 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.
    ///
    ///
    ///     extension Reactive where Base: UISearchBar {
    ///
    ///         public var delegate: DelegateProxy<UISearchBar, UISearchBarDelegate> {
    ///            return RxSearchBarDelegateProxy.proxy(for: base)
    ///         }
    ///
    ///         public var text: ControlProperty<String> {
    ///             let source: Observable<String> = self.delegate.observe(#selector(UISearchBarDelegate.searchBar(_:textDidChange:)))
    ///             ...
    ///         }
    ///     }
    public static func proxy(for object: ParentObject) -> Self {
        MainScheduler.ensureExecutingOnScheduler()

        let maybeProxy = self.assignedProxy(for: object)

        let proxy: AnyObject
        if let existingProxy = maybeProxy {
            proxy = existingProxy
        }
        else {
            proxy = castOrFatalError(self.createProxy(for: object))
            self.assignProxy(proxy, toObject: object)
            assert(self.assignedProxy(for: object) === proxy)
        }
        let currentDelegate = self._currentDelegate(for: object)
        let delegateProxy: Self = castOrFatalError(proxy)

        if currentDelegate !== delegateProxy {
            delegateProxy._setForwardToDelegate(currentDelegate, retainDelegate: false)
            assert(delegateProxy._forwardToDelegate() === currentDelegate)
            self._setCurrentDelegate(proxy, to: object)
            assert(self._currentDelegate(for: object) === proxy)
            assert(delegateProxy._forwardToDelegate() === currentDelegate)
        }

        return delegateProxy
    }

这个函数的参数只有1个ParentObject类型,其实在协议中定义了,就是一个AnyObject类型。

可以看到,这个操作需要确保在主线程上进行,换句话说,开发者不可以在子线程上订阅UIScrollView的回调事件。然后调用一个assignedProxy函数获取一个maybeProxy结果。而这个函数做的事情,就是通过runtimeobjc_getAssociatedObject函数在ParentObject上查询是否有绑定的对象,使用的key是该DelegateProxyType实例本身的identifier变量。(此处有一个由Swift编译器引起的bug,Rx团队需要对objc_getAssociatedObject的直接结果做一个处理才可以返回,暂时不探究。)

看到这里已经可以看出:

RxCocoa可以让开发者跳过实现Delegate函数直接获取UIKit组件的回调,其实是通过runtime把一个已经实现了Delegate的Proxy绑定到了这个组件上。

继续看完这个函数,如果maybeProxy得出的结果是空的,就会通过createProxy创建一个新的Self实例并绑定到UIScrollView的实例上。这里的Self对应的是那个实现DelegateProxyType的类型,对应上当前场景的就是RxScrollViewDelegateProxy。并且在绑定完成后再做了一次判断,确保对应identifier绑定的实例就是刚刚新创建出来的那个。(猜测:为了避免有别的线程也使用同样的identifier绑定了其他实例?)

然后通过函数_currentDelegate(for:)获取当前UIScrollView的delegate,这是一个抽象函数,具体返回的"delegate"是根据传入参数实现的协议来定,详情可以在同一个文件下找到,分别有以下几种情况:

  1. ParentObject: HasDelegate, Self.Delegate == ParentObject.Delegate

  2. ParentObject: HasDataSource, Self.Delegate == ParentObject.DataSource

  3. ParentObject: HasPrefetchDataSource, Self.Delegate == ParentObject.PrefetchDataSource

目前场景下属于情况1,所以看这个extension里面的实现,实际上是返回了传入参数的的delegate,而在RxScrollViewDelegateProxy.swift文件里就可以看到有UIScrollViewHasDelegate实现,这个delegate其实就是UIScrollViewDelegate

拿到这个delegate后,会和上面的proxy做对比(在对比之前又做了一次对proxy的判断,确认其类型就是当前需要的DelegateProxy),然后做托管处理。也就是说,如果在开发者设置订阅UIScrollView之前,UIScrollView已经有一个delegate,在这里就会把这个delegate托管给proxy,让proxy在收到UIScrollView回调的时候转发给delegate,而实际上UIScrollView此时的delegate指向的是proxy。通过proxy的forwardToDelegate可以找回这个在外部设置的delegate。**


持续更新...

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 203,937评论 6 478
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,503评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,712评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,668评论 1 276
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,677评论 5 366
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,601评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,975评论 3 396
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,637评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,881评论 1 298
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,621评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,710评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,387评论 4 319
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,971评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,947评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,189评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 44,805评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,449评论 2 342

推荐阅读更多精彩内容