RxCocoa中對UIKit的Delegate的處理

RxCocoa(scrollView.rx.didScroll)

得益于RxCocoa對UIKit做了extension,我們使用UI組件的Rx封裝時(shí)只需要調(diào)用rx屬性烹看,就可以訪問到Rx框架的內(nèi)容刊头。比如需要訂閱UIScrollView的滾動事件:

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

But, why?眠寿??

在原生的Cocoa框架中躬柬,要監(jiān)聽UIScrollView的滑動事件,需要通過實(shí)現(xiàn)UIScrollViewDelegate協(xié)議的scrollViewDidScroll函數(shù)抽减。但是RxCocoa為何可以讓用戶通過簡單的調(diào)用就得到了回調(diào)允青?UIScrollViewDelegate是如何被隱藏的?

rx屬性是怎么來的?

Reactive&ReactiveCompatible

通過查看卵沉,rx屬性是一個(gè)Reactive的類型颠锉。在Reactive.swift文件法牲,可以看到它是一個(gè)泛型struct 。而rx是在一個(gè)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的定義非常簡單拒垃,只有一個(gè)base屬性,而這個(gè)屬性是一個(gè)泛型瓷蛙。

在文件的最后一行悼瓮,NSObject被聲明了實(shí)現(xiàn)ReactiveCompatible,因此UIScrollView也實(shí)現(xiàn)了該協(xié)議艰猬。

查看ReactiveCompatible的定義以及其下方的extension就可以看出横堡,rx屬性的泛型是該類型本身,也就是說UISCrollViewrx屬性就是一個(gè)UISCrollView泛型的Reactive結(jié)構(gòu)體冠桃,base屬性就是這個(gè)UIScrollView本身的實(shí)例**命贴。

順著往下看,可以在UIScrollView+Rx.swift文件里找到食听,是通過extensionwhere語法對UIScrollView泛型的Reactive添加的一個(gè)計(jì)算型屬性胸蛛。

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是一個(gè)實(shí)現(xiàn)UIScrollViewDelegate的代理類型樱报。

暫時(shí)不探究RxScrollViewDelegateProxyproxy(for:)函數(shù)胚泌,比較好理解,source是通過一個(gè)對應(yīng)UISrollView實(shí)例獲得的RxScrollViewDelegateProxy的一個(gè)UIScrollView的contentOffset事件廣播肃弟,函數(shù)最后再把這個(gè)廣播封裝成一個(gè)ControlEvent類型實(shí)例玷室,也就是被訂閱的那個(gè)事件。

而關(guān)鍵就在于RxScrollViewDelegateProxy這個(gè)類型做了什么事情笤受。

RxScrollViewDelegateProxy

繼續(xù)查看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穷缤,并實(shí)現(xiàn)了兩個(gè)協(xié)議:

  • DelegateProxyType
  • UIScrollViewDelegate

顯然這里就是UIScrollViewDelegate的藏身點(diǎn),下面繼續(xù)看在這里它是如何被黑盒化的箩兽。

在這個(gè)文件里可以找到原始回調(diào)的函數(shù):

/// 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)
    }

在回調(diào)的處理上津肛,做了3件事:

  • 發(fā)送contentOffset的數(shù)值變化廣播
  • 發(fā)送contentOffset變化的事件廣播
  • 調(diào)用了_forwardToDelegate屬性的另一個(gè)scrollViewDidScroll函數(shù)

可以看到兩個(gè)廣播事件應(yīng)該是按需發(fā)送的,如果在沒有訂閱者的情況下汗贫,應(yīng)該不會產(chǎn)生對應(yīng)的屬性身坐。而在上面被訂閱的的contentOffsetPublishSubject就是這個(gè)_contentOffsetPublishSubject

_forwardToDelegate應(yīng)該是開發(fā)者在外部設(shè)置的delegate落包,也就是說:

即使開發(fā)者已經(jīng)為一個(gè)UIScrollView設(shè)置了delegate(或者沒有設(shè)置)部蛇,也不會影響通過RxCocoa框架去訂閱這個(gè)UIScrollView的事件。 而且可以有多個(gè)訂閱者通過RxCocoa去訂閱UIScrollView的回調(diào)事件咐蝇,因?yàn)檫@里的Observable是廣播類型涯鲁。**

可見在RxCocoa中,每一個(gè)有事件訂閱者的UIScrollView都有與之對應(yīng)的RxScrollViewDelegateProxy實(shí)例。所以下一個(gè)問題就是:

代碼RxScrollViewDelegateProxy.proxy(for: base)當(dāng)中抹腿,proxy(for:)函數(shù)是如何把一個(gè)RxScrollViewDelegateProxy綁定到一個(gè)UIScrollView上的岛请?

DelegateProxyType

可以查看到proxy(for:)函數(shù)被定義在DelegateProxyType協(xié)議里,通過extension實(shí)現(xiàn)警绩。

/// 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
    }

這個(gè)函數(shù)的參數(shù)只有1個(gè)ParentObject類型崇败,其實(shí)在協(xié)議中定義了,就是一個(gè)AnyObject類型肩祥。

可以看到僚匆,這個(gè)操作需要確保在主線程上進(jìn)行,換句話說搭幻,開發(fā)者不可以在子線程上訂閱UIScrollView的回調(diào)事件咧擂。然后調(diào)用一個(gè)assignedProxy函數(shù)獲取一個(gè)maybeProxy結(jié)果。而這個(gè)函數(shù)做的事情檀蹋,就是通過runtimeobjc_getAssociatedObject函數(shù)在ParentObject上查詢是否有綁定的對象松申,使用的key是該DelegateProxyType實(shí)例本身的identifier變量。(此處有一個(gè)由Swift編譯器引起的bug俯逾,Rx團(tuán)隊(duì)需要對objc_getAssociatedObject的直接結(jié)果做一個(gè)處理才可以返回贸桶,暫時(shí)不探究。)

看到這里已經(jīng)可以看出:

RxCocoa可以讓開發(fā)者跳過實(shí)現(xiàn)Delegate函數(shù)直接獲取UIKit組件的回調(diào)桌肴,其實(shí)是通過runtime把一個(gè)已經(jīng)實(shí)現(xiàn)了Delegate的Proxy綁定到了這個(gè)組件上皇筛。

繼續(xù)看完這個(gè)函數(shù),如果maybeProxy得出的結(jié)果是空的坠七,就會通過createProxy創(chuàng)建一個(gè)新的Self實(shí)例并綁定到UIScrollView的實(shí)例上水醋。這里的Self對應(yīng)的是那個(gè)實(shí)現(xiàn)DelegateProxyType的類型,對應(yīng)上當(dāng)前場景的就是RxScrollViewDelegateProxy彪置。并且在綁定完成后再做了一次判斷拄踪,確保對應(yīng)identifier綁定的實(shí)例就是剛剛新創(chuàng)建出來的那個(gè)。(猜測:為了避免有別的線程也使用同樣的identifier綁定了其他實(shí)例拳魁?)

然后通過函數(shù)_currentDelegate(for:)獲取當(dāng)前UIScrollView的delegate惶桐,這是一個(gè)抽象函數(shù),具體返回的"delegate"是根據(jù)傳入?yún)?shù)實(shí)現(xiàn)的協(xié)議來定潘懊,詳情可以在同一個(gè)文件下找到姚糊,分別有以下幾種情況:

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

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

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

目前場景下屬于情況1,所以看這個(gè)extension里面的實(shí)現(xiàn)授舟,實(shí)際上是返回了傳入?yún)?shù)的的delegate救恨,而在RxScrollViewDelegateProxy.swift文件里就可以看到有UIScrollViewHasDelegate實(shí)現(xiàn),這個(gè)delegate其實(shí)就是UIScrollViewDelegate岂却。

拿到這個(gè)delegate后忿薇,會和上面的proxy做對比(在對比之前又做了一次對proxy的判斷裙椭,確認(rèn)其類型就是當(dāng)前需要的DelegateProxy)躏哩,然后做托管處理署浩。也就是說,如果在開發(fā)者設(shè)置訂閱UIScrollView之前扫尺,UIScrollView已經(jīng)有一個(gè)delegate筋栋,在這里就會把這個(gè)delegate托管給proxy,讓proxy在收到UIScrollView回調(diào)的時(shí)候轉(zhuǎn)發(fā)給delegate正驻,而實(shí)際上UIScrollView此時(shí)的delegate指向的是proxy弊攘。通過proxy的forwardToDelegate可以找回這個(gè)在外部設(shè)置的delegate。**


持續(xù)更新...

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末姑曙,一起剝皮案震驚了整個(gè)濱河市襟交,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌伤靠,老刑警劉巖捣域,帶你破解...
    沈念sama閱讀 221,548評論 6 515
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異宴合,居然都是意外死亡焕梅,警方通過查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 94,497評論 3 399
  • 文/潘曉璐 我一進(jìn)店門卦洽,熙熙樓的掌柜王于貴愁眉苦臉地迎上來贞言,“玉大人,你說我怎么就攤上這事阀蒂「么埃” “怎么了?”我有些...
    開封第一講書人閱讀 167,990評論 0 360
  • 文/不壞的土叔 我叫張陵蚤霞,是天一觀的道長挪捕。 經(jīng)常有香客問我,道長争便,這世上最難降的妖魔是什么级零? 我笑而不...
    開封第一講書人閱讀 59,618評論 1 296
  • 正文 為了忘掉前任,我火速辦了婚禮滞乙,結(jié)果婚禮上奏纪,老公的妹妹穿的比我還像新娘。我一直安慰自己斩启,他們只是感情好序调,可當(dāng)我...
    茶點(diǎn)故事閱讀 68,618評論 6 397
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著兔簇,像睡著了一般发绢。 火紅的嫁衣襯著肌膚如雪硬耍。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 52,246評論 1 308
  • 那天边酒,我揣著相機(jī)與錄音经柴,去河邊找鬼。 笑死墩朦,一個(gè)胖子當(dāng)著我的面吹牛坯认,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播氓涣,決...
    沈念sama閱讀 40,819評論 3 421
  • 文/蒼蘭香墨 我猛地睜開眼牛哺,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了劳吠?” 一聲冷哼從身側(cè)響起引润,我...
    開封第一講書人閱讀 39,725評論 0 276
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎痒玩,沒想到半個(gè)月后淳附,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 46,268評論 1 320
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡凰荚,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 38,356評論 3 340
  • 正文 我和宋清朗相戀三年燃观,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片便瑟。...
    茶點(diǎn)故事閱讀 40,488評論 1 352
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡缆毁,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出到涂,到底是詐尸還是另有隱情脊框,我是刑警寧澤,帶...
    沈念sama閱讀 36,181評論 5 350
  • 正文 年R本政府宣布践啄,位于F島的核電站浇雹,受9級特大地震影響,放射性物質(zhì)發(fā)生泄漏屿讽。R本人自食惡果不足惜昭灵,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,862評論 3 333
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望伐谈。 院中可真熱鬧烂完,春花似錦、人聲如沸诵棵。這莊子的主人今日做“春日...
    開封第一講書人閱讀 32,331評論 0 24
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽履澳。三九已至嘶窄,卻和暖如春怀跛,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背柄冲。 一陣腳步聲響...
    開封第一講書人閱讀 33,445評論 1 272
  • 我被黑心中介騙來泰國打工吻谋, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人羊初。 一個(gè)月前我還...
    沈念sama閱讀 48,897評論 3 376
  • 正文 我出身青樓滨溉,卻偏偏與公主長得像什湘,于是被迫代替她去往敵國和親长赞。 傳聞我的和親對象是個(gè)殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 45,500評論 2 359

推薦閱讀更多精彩內(nèi)容

  • RxSwift_v1.0筆記——13 Intermediate RxCocoa 這章將學(xué)習(xí)一些高級的RxCocoa...
    大灰很閱讀 662評論 1 1
  • Swift1> Swift和OC的區(qū)別1.1> Swift沒有地址/指針的概念1.2> 泛型1.3> 類型嚴(yán)謹(jǐn) 對...
    cosWriter閱讀 11,111評論 1 32
  • 本文內(nèi)容架構(gòu):itemSelected的底層實(shí)現(xiàn)實(shí)戰(zhàn) 我們在使用RxSwift的時(shí)候經(jīng)常會遇到這樣的代碼闽撤,類似的還...
    JABread閱讀 3,970評論 1 7
  • 從未下定過決心哟旗,要每一天給自己做個(gè)總結(jié)贩据。總是不斷安慰自己闸餐,雖然沒有寫出來饱亮,但心裡是有感悟有收穫的,有數(shù)舍沙!不需要給別...
    笨蝸牛閱讀 291評論 2 3
  • 概括一下近上,由教師轉(zhuǎn)到小老板,后來倒閉拂铡,又再誤入傳銷壹无,結(jié)交好友離開傳銷,后在保險(xiǎn)行業(yè)歷練感帅,做到最高級別斗锭,同時(shí)學(xué)業(yè)獲得...
    Rao輝哲閱讀 172評論 0 0