文章概要:本文主要从分析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函数派发规则总结:
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方案也会给你在之后的开发中在处理相似问题时带来更多的思路和灵感。