RxSwift異步事件追蹤定位工具的研發歷程

文章概要:本文主要從分析RxSwift操做符的實現原理入手,而後介紹了Swift反射機制、Swift的函數派發機制及命名空間機制,同時咱們設計了一套實現Hook Swift的動態及靜態方法的解決方案,但願對廣大iOS開發者有所幫助。


1. 背景:RxSwift之痛

RxSwift是GitHub的ReactiveX團隊研發的一套函數響應式編程框架,其主要思想是把事件封裝成信號流並採用觀察者模式來實現監聽。
當你使用RxSwift來實現一些簡單的功能如發送一次網絡請求、監聽按鈕點擊事件等會讓你的代碼看起來很是直觀簡潔,可是若是你使用RxSwift實現了一個異步熱流且在不一樣的類之間層層傳遞和加工轉換以後代碼的可讀性就大大下降,甚至由於抓不到異步事件產生的堆棧而出現難以調試的狀況。


爲解決RxSwift的調試難題,咱們經過閱讀源碼分析RxSwift操做符實現原理,而後利用Swift反射機制來dump 「Observable Link」,最後又根據Swift語言的函數派發機制和命名空間機制設計了一套安全高效的hook Swift的動態及靜態方法的方案,經過這套hook方案完成了對流事件傳遞鏈上的關鍵函數的攔截處理從而順利實現了精準定位和調試RxSwift中異步事件的目標。


2. Dump Observable Link

2.1 RxSwift操做符實現原理簡析

一個Observable使用操做符能夠轉換成一個新的Observable,而這個源Observable通過一些連續的操做符轉換以後就造成了一條Observable Link,要追蹤一個異步事件的源頭首先須要找到整個Observable Link的Head節點。
閱讀RxSwift的源碼以後發現RxSwift的各類操做符的基本原理就是當你使用某個操做符對一個Observable A進行轉換的時候,這個操做符都會生成一個新的Observable B,而且在這個新的Observable B內部持有原來的那個Observable A,當有其餘人訂閱Observable B的時候,Observable B內部同時也會訂閱Observable A以此來實現整個Observable Link的「聯動」效果。此時你也許會有了一些思路,既然每一個操做符都會在其內部持有上一個Observable,那咱們根據這個規律沿着一個操做符Observable一直往上回溯直到根Observable是否是就能夠dump出整個Observable Link了?這個思路是正確的,然而現實卻很殘酷——全部操做符Observable用於持有其源Observable的屬性都是Private的,這也就意味着你根本沒法直接獲取到這些屬性!然而天無絕人之路,所幸的是咱們還能夠利用Swift的反射機制來到達目的。

2.2 Swift反射機制

儘管 Swift一直都在強調強類型、編譯時安全並推薦使用靜態調度,但它的標準庫仍然提供了一個基於Mirror的Struct來實現的反射機制。簡單來講,例如你有一個Class A並建立了一個A的實例對象a,此時你就能夠經過Mirror(reflecting: a)來生成一個Mirror對象m,而後遍歷m.children就能夠獲取到a對象的全部屬性。
看到這裏你應該知道如何去dump一個Observable Link了吧,話很少說,先上代碼爲敬:

2.3 爲已有的類動態添加存儲型屬性

dump出的Observable Link上的全部Observable都是咱們須要在運行時重點觀察的對象,那麼咱們該如何對這些Observable與其它Observable作出區分呢?咱們能夠爲Observable添加一個tag屬性,在運行時若是發現某個Observable的tag不爲空就監控這個Observable上產生的event。不過這裏有一個關聯類型問題,any類型能夠轉換爲某種協議類型,但沒法轉換爲關聯類型協議的類型,由於關聯的具體類型是未知的。爲解決這個問題,咱們設計了一個無關聯類型的協議RxEventTrackType,在這個協議的extension裏面爲其添加eventTrackerTag屬性,而後讓Obseverble遵照此協議。爲了給一個協議類型在extension中添加一個存儲型屬性,這裏我選擇了一個在OC時代常用的實現方案:objc_setAssociatedObject。


3. Hook Swift動態和靜態方法

3.1 Swift的函數派發機制

函數派發就是處理如何去調用一個函數的問題。編譯型語言有三種常見的函數派發方式:直接派發(Direct Dispatch)、函數表派發(Table Dispatch)和消息派發(Message Dispatch)。Swift同時支持這三種函數派發方式。
直接派發(Direct Dispatch)是最快的,不止是由於須要調用的指令集會更少,而且編譯器還可以有很大的優化空間,例如函數內聯等。然而靜態調用對於編程來講也就意味着由於缺少動態性而沒法支持繼承。
函數表派發(Table Dispatch)是編譯型語言實現動態行爲最多見的實現方式。函數表使用了一個數組來存儲類聲明的每個函數的指針。大部分語言把這個稱爲「virtual table」(虛函數表),Swift裏稱爲 「witness table」。每個類都會維護一個函數表,裏面記錄着類全部須要經過函數表派發的函數,若是在本類中override了父類函數的話表裏面只會保存被override以後的函數。一個子類在聲明體內新添加的函數都會被插入到這個函數表的後面,運行時會根據這一個表去決定實際要被調用的函數。
消息機制(Message Dispatch)是調用函數最動態的方式,這樣的機制催生了KVO,UIAppearence和CoreData等功能。這種運做方式的關鍵在於開發者能夠在運行時改變函數的行爲,不止能夠經過swizzling來改變,甚至能夠用isa-swizzling修改對象的繼承關係,能夠在面向對象的基礎上實現自定義派發。
Swift函數派發規則總結:
  • 值類型聲明做用域裏的函數老是會使用直接派發
  • Class聲明做用域裏的函數都會使用函數表進行派發(某些特殊狀況下編譯器會優化爲直接派發)
  • 而協議和類的extension都會使用直接派發
  • 協議裏聲明的,而且帶有默認實現的函數會使用函數表進行派發
  • 用dynamic修飾的函數會經過運行時進行消息機制派發

3.2 靜態語言Swift的Hook難點

相比於動態語言OC,靜態語言Swift的方法Hook變得異常困難。主要緣由以下:
1. 目標函數查找難
在OC中咱們能夠經過一個Selector(你能夠簡單理解爲一個字符串)查找到對應的method,這個method內部的imp字段存儲的便是函數指針。而Swift中的動態方法利用witness table或者protocol witness table經過偏移尋址來查找對應函數指針,Swift中的靜態方法的地址更是在編譯期就已經肯定。
2.強行直接替換函數指針比較危險
若是非要Hook Swift中的動態方法,咱們仍是能夠利用Xcode的lldb調試工具在運行時經過反彙編觀察並記錄某個函數對應的在witness table中的偏移量,而後找到這個類的meta data並根據這些偏移量找到對應的函數指針來進行Hook。然而這是一個很是危險的作法,若是某天Swift調整了其類對象的內存模型,咱們經過固有偏移來實現的Hook將一觸及崩!
3.3 移花接木——巧用命名空間
在Swift中每一個module都表明了一個單獨的命名空間,在不一樣的module裏面能夠定義相同的類型名稱或者方法名稱。例如Swift爲咱們提供的基本數據類型String裏面定義了一個lowercased方法,若是此時咱們在本身的module裏面利用extension給String再增長一個lowercased方法,此時這兩個lowercased方法是能夠共存的,並且當你在本身的module裏面調用String的lowercased方法時候默認優先調用的是你本身module裏面的lowercased方法。
如今,你是否是感受在Swift中Hook方法彷佛有了一些眉目,然而目前還有一個更重要的問題亟待解決:如何在咱們本身的lowercased方法中調用原生的lowercased方法呢?答案一樣是利用命名空間。咱們能夠另外再建一個B module(demo中利用建立一個pod庫的方式實現),在這個B module中給String增長一個originalLowercased方法,這個方法的內部實現很簡單就是直接調用一下String的原生lowercased方法。而後就能夠在咱們本身module的lowercased方法中調用originalLowercased從而間接實現對String的原生lowercased方法的調用。

稍微有些遺憾的是,利用上面所述的這種方案Hook的方法只在咱們本身的module裏面有效,不過對於通常的Hook需求來講已經足夠使用了。編程


4. Hook RxSwift的方法

上面關於Hook的介紹已經給咱們提升了充分的理論基礎,下面咱們就能夠用理論來指導實踐了。
若是要追蹤一個流事件產生的源頭,關鍵要作的就是監聽ObserverType的onNext、onError、onComplete方法和BehaviorRelay的accept方法。而後當一個ObserverType的對象的onNext等方法被調用的時候若是發現這個對象帶有observerTypeTrackerTag就認爲這是一個須要被重點觀察和監控的對象並做出相應的處理,咱們也能夠同時在這裏加上一個條件斷點方便調試,代碼截圖以下:
使用此定位工具來追蹤和定位一步事件源調試效果以下Gif圖所示:


5. 總結

在這次RxSwift異步事件追蹤定位工具的研發過程當中,最爲關鍵也是難點之一的就是如何實現hook Swift的動態及靜態方法,咱們在嘗試了兩三種方案以後才最終肯定了這種利用Swift語言的函數派發機制和命名空間機制來安全高效的hook Swift的動態及靜態方法的方案,相信咱們的這套hook方案也會給你在之後的開發中在處理相似問題時帶來更多的思路和靈感。
相關文章
相關標籤/搜索