原创:知识点总结性文章
创作不易,请珍惜,之后会持续更新,不断完善
个人比较喜欢做笔记和写总结,毕竟好记性不如烂笔头哈哈,这些文章记录了我的IOS成长历程,希望能与大家一起进步
温馨提示:由于简书不支持目录跳转,大家可通过command + F 输入目录标题后迅速寻找到你所需要的内容
目录
- 一、背景:RxSwift之痛
- 二、Dump Observable Link
- 三、Hook Swift动态和静态方法
- 四、Hook RxSwift的方法
文章概要
本文主要从分析RxSwift操作符的实现原理入手,然后介绍了Swift反射机制、Swift的函数派发机制及命名空间机制,同时我们设计了一套实现Hook Swift
的动态及静态方法的解决方案,希望对广大iOS开发者有所帮助。
一、背景:RxSwift之痛
RxSwift是GitHub的ReactiveX团队研发的一套函数响应式编程框架,其主要思想是把事件封装成信号流并采用观察者模式来实现监听。
当你使用RxSwift来实现一些简单的功能如发送一次网络请求、监听按钮点击事件等会让你的代码看起来非常直观简洁,但是如果你使用RxSwift实现了一个异步热流且在不同的类之间层层传递和加工转换之后代码的可读性就大大降低,甚至因为抓不到异步事件产生的堆栈而出现难以调试的情况。
为解决RxSwift的调试难题,我们通过阅读源码分析RxSwift操作符实现原理,然后利用Swift反射机制来dump “Observable Link”
,最后又根据Swift语言的函数派发机制和命名空间机制设计了一套安全高效的hook Swift
的动态及静态方法的方案,通过这套hook
方案完成了对流事件传递链上的关键函数的拦截处理从而顺利实现了精准定位和调试RxSwift中异步事件的目。
二、Dump Observable Link
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、Swift反射机制
尽管 Swift一直都在强调强类型、编译时安全并推荐使用静态调度,但它的标准库仍然提供了一个基于Mirror
的Struct
来实现的反射机制。简单来说,例如你有一个Class A
并创建了一个A
的实例对象a
,此时你就可以通过Mirror(reflecting: a)
来生成一个Mirror
对象m
,然后遍历m.children
就可以获取到a
对象的所有属性。看到这里你应该知道如何去dump
一个Observable Link
了吧,话不多说,先上代码为敬:
![](https://img.haomeiwen.com/i9570900/be12f9b43fa1b1d4.png)
3、为已有的类动态添加存储型属性
dump
出的Observable Link
上的所有Observable
都是我们需要在运行时重点观察的对象,那么我们该如何对这些Observable
与其它Observable
做出区分呢?我们可以为Observable
添加一个tag
属性,在运行时如果发现某个Observable
的tag
不为空就监控这个Observable
上产生的event
。不过这里有一个关联类型问题,any
类型可以转换为某种协议类型,但无法转换为关联类型协议的类型,因为关联的具体类型是未知的。为解决这个问题,我们设计了一个无关联类型的协议RxEventTrackType
,在这个协议的extension
里面为其添加eventTrackerTag
属性,然后让Obseverble
遵守此协议。为了给一个协议类型在extension
中添加一个存储型属性,这里我选择了一个在OC时代经常使用的实现方案: objc_setAssociatedObject
。
![](https://img.haomeiwen.com/i9570900/3e5669826d05cf6f.png)
![](https://img.haomeiwen.com/i9570900/1957b8ba4fbd47a7.png)
三、Hook Swift动态和静态方法
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
修饰的函数会通过运行时进行消息机制派发
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、移花接木——巧用命名空间
在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
方法的调用。
![](https://img.haomeiwen.com/i9570900/f9153a5d66597f33.png)
稍微有些遗憾的是,利用上面所述的这种方案Hook的方法只在我们自己的module里面有效,不过对于一般的Hook需求来说已经足够使用了。
![](https://img.haomeiwen.com/i9570900/d6a7f5ca6c75dc51.png)
四、Hook RxSwift的方法
上面关于Hook的介绍已经给我们提高了充分的理论基础,下面我们就可以用理论来指导实践了。如果要追踪一个流事件产生的源头,关键要做的就是监听ObserverType
的onNext
、onError
、onComplete
方法和BehaviorRelay
的accept
方法。然后当一个ObserverType
的对象的onNext
等方法被调用的时候如果发现这个对象带有observerTypeTrackerTag
就认为这是一个需要被重点观察和监控的对象并作出相应的处理,我们也可以同时在这里加上一个条件断点方便调试,代码截图如下:
![](https://img.haomeiwen.com/i9570900/6ed39d485594137b.png)
![](https://img.haomeiwen.com/i9570900/a225ec3afdcb9bf8.png)
五、总结
在此次RxSwift
异步事件追踪定位工具的研发过程中,最为关键也是难点之一的就是如何实现hook
Swift的动态及静态方法,我们在尝试了两三种方案之后才最终确定了这种利用Swift语言的函数派发机制和命名空间机制来安全高效的hook
Swift的动态及静态方法的方案,相信我们的这套hook
方案也会给你在以后的开发中在处理类似问题时带来更多的思路和灵感。
网友评论