Aspects是一个可以动态hook指定方法的轻量级的库,AOP思想非常棒的体现。
1. 相关结构说明
Aspects.h
AspectOptions
切片位置,用于指定hook后自定义block执行的时机。
AspectPositionAfter | 原始IMP执行之后执行 |
AspectPositionInstead | 替换原始IMP |
AspectPositionBefore | 原始IMP执行之前执行 |
AspectOptionAutomaticRemoval | 执行一次后直接移除 |
AspectToken协议
用于注销hook使用。使用其中的remove方法进行注销。
AspectInfo协议
作为插入的block的第一个参数出现。
对外公开为协议,内部则使用类实现。
方法 | 说明 |
---|---|
instance | 被hook的实例对象 |
originalInvocation | 原始的NSInvocation对象 |
arguments | 原始参数数组 |
Aspects,NSObject的分类
Aspects的公开API核心。
分为两个版本:实例方法版本和类方法版本。
|
|
调用者为类对象,对指定类的selector进行hook,执行时机为options,插入的执行任务为block。可以监控错误,返回值为遵循AspectToken的对象,用它可以实现注销hook操作。
|
|
作用与类方法版本相同,但调用者为类的实例对象,即hook的方法只对本实例有效,其他实例无效。
AspectErrorCode
错误信息说明。
AspectErrorSelectorBlacklisted | 黑名单(不允许hook的情况) |
AspectErrorDoesNotRespondToSelector | 找不到SEL的实现 |
AspectErrorSelectorDeallocPosition | hook到delloc方法的时机错误(只允许before情况) |
AspectErrorSelectorAlreadyHookedInClassHierarchy | 类继承体系中已经hook过该方法 |
AspectErrorFailedToAllocateClassPair | 创建类失败(实例对象hook时可能发生) |
AspectErrorMissingBlockSignature | block的签名错误(编译期签名无效,无法读取使用) |
AspectErrorIncompatibleBlockSignature | block签名与原始方法签名不匹配 |
AspectErrorRemoveObjectAlreadyDeallocated | 重复移除hook对象 |
Aspect.m
AspectBlockFlags
切片block结构体中的位标识
AspectBlockFlagsHasCopyDisposeHelpers | 标识copy函数位(第25位) |
AspectBlockFlagsHasSignature | 标识签名位(第30位) |
AspectBlockRef
_AspectBlock结构体指针,结构与block结构体相似。
AspectInfo
主要用于包装NSInvocation对象。
遵循了AspectInfo协议,作为内部实现(外部只公开为协议,隐藏真正的类)。
将协议方法实现为三个只读属性,使用指定方法进行初始化:
|
|
AspectIdentifier
保存切片的相关信息(如receiver、selector、block和error信息)。作为数据模型类。
|
|
|
|
AspectsContainer
作为AspectIdentifier的容器,负责管理内部的AspectIdentifier对象。提供添加、删除、检查等功能。
根据options将AspectIdentifier对象分别存储在不同的类别数组中。
在Aspects中,根据调用者(实例对象或类对象)实现了两个container。
AspectTracker
切片追踪者,保存着追踪的相关信息,方便查询。
属性 | 说明 |
---|---|
trackedClass | 追踪的类 |
selectorNames | 集合,保存着追踪的选择器名 |
parentEntry | 自身实例的指针,根据类继承体系指向子类对象 |
NSInvocation + Aspects
NSInvocation的分类,提供了方法:直接返回切片的所有参数。
2. 实现源代码学习
Aspects是线程安全的
可以在三个方面验证此结论:
1. 使用自旋锁进行整体hook
我们知道,在公共API中,实现都是通过调用c函数aspect_add来完成的,而其中的执行环境是通过锁机制来保证线程安全的。
|
|
使用自旋锁可以保证同时只执行一个运算任务,且其他运算单元不会因锁被他人保持而进入睡眠状态。自旋锁适合运算量不大的任务。在这种情况下,其效率要明显高于同步锁 @synchronized。
2. AspectContainer中使用原子性数组
在类AspectContainer中,保存AspectIdentifier实例的数组属性(beforeAspects、insteadAspects、afterAspects)的特性为atomic,保证在多线程环境下,访问该容器是安全的。
3. 使用dispatch_once来保证共享数据只有一份
|
|
这里使用dispatch_once保证集合创建为线程安全且只有一份。同时,执行block时也通过同步锁保证线程安全。
Aspects的hook方法不可高频次调用
作者说,hook的方法一般只可以是view或ViewController等方法,调用频次不要超过每秒1000次。这是由于Aspects的hook方法调用实际是在方法转发流程中进行的。
Aspects中所有的真正方法调用都是通过NSInvocation对象进行的:
|
|
我们知道,完整的消息转发流程是在方法调用的最后一步才进行,苹果明确说明这是比较耗费性能的(需要通过获取方法签名NSMethodSignature对象,封装生成NSInvocation对象,然后在forwardInvocation:方法中执行invoke方法)。故作者添加了此说明。
Aspects对实例hook方法和类hook方法使用不同实现方案
对于类的实例来说,使用Aspects对某方法进行hook只是对本实例有效;
而对于类对象来说,对某方法进行hook,即对本类的所有实例都有效。
我们在aspect_hookClass的实现中,可以一探究竟
|
|
首先我们看一下,对于class方法和objc_getClass函数的区别:
class方法: 对于类,返回自身;对于实例对象,返回所属Class
objc_getClass函数: 其实现都是返回调用者的isa。也就是说,对于实例对象,得到的是所属Class;对于类,得到的是metaClass。
1. 类实例hook的实现
|
|
在这里我们可以看到,对于实例hook,Aspects使用动态类的方式进行实现:创建一个继承于原类的子类,对该子类的NSInvocation进行hook,然后通过object_setClass将调用者的类指定为新子类。
下面我们依次查看方法实现:
|
|
通过class_replaceMethod,替换了klass的fowardInvocation: 方法实现(替换IMP为 ASPECTS_ARE_BEING_CALLED),klass没有实现此方法,则直接将此方法及实现添加到类中。
当klass实现了原方法,则Aspects将原IMP保存到klass的AspectsForwardInvocationSelectorName方法中,相当于完成了forwardInvocation: 的方法交换。
|
|
同理,此方法是将Class的class实例方法IMP进行替换,改为了statedClass。在调用时,即将新子类的Class及MetaClass均指向原类。这样原实例则根本不知道自己的实例已经被“偷梁换柱”了。
2. 类对象hook的实现
|
|
class_isMetaClass判断当前类是否为metaClass,通过这种情况,可以判定调用Aspects的hook方法的是类对象。所以调用者的目的是将所有的类实例的指定方法都进行hook。故Aspects直接对该类进行操作。
注意:
由于KVO也是通过创建动态类的方式实现(创建子类后修改isa指向),故hook的应当是KVO之前的原始类。
|
|
这里直接对klass的forwardInvocation: 方法进行了替换,只不过需要在线程安全的前提下进行,且swizzledClasses是全局共享的。
Aspects对指定selector的替换过程
对于selector的处理过程,实例对象和类对象的处理方法是一致的。
|
|
对于原始方法,Aspects将其实现IMP保存至aliasSelector中,而原始方法则直接指向 _objc_msgForward函数。
_objc_msgForward 是方法调用过程中的一步,在objc_msgSend流程中,当方法未找到IMP时,且未能动态添加方法IMP,_objc_msgForward则开始执行。也就是说 _objc_msgForward是消息转发的起点。
交换成功后,调用者执行原方法时,则会直接进入消息转发阶段。
Aspects对待hook的selector进行检测,符合要求才允许进行
|
|
对于实例对象,只要待hook的selector符合上述要求,即可准备进行hook。
而对于类而言,由于类存在继承体系,需要对hook的唯一性进行检测:
|
|
对于类对象来说,当类的指定selector准备hook之前,先要检测其继承体系中是否已经hook过此selector。
其方法是:
通过AspectTracker对象,对其内部的selectorNames数组进行检测,依照parentEntry向下依次查找(parentEntry存储的是子类对象),
如果存在parentEntry的selectorNames已经包含selector,且该parentEntry类不是当前类,则证明selector已经在继承体系中(确切地说是子类中)已经hook过,不可以重复hook。
AspectIdentifier,切面信息的生成
我们回到最初,在aspect_add方法中可以看到,切面信息AspectIdentifier对象,是在开始hook操作之前,便生成添加到AspectsContainer容器中的:
|
|
首先,我们看一下如何得到保存AspectIdentifier对象的容器对象AspectContainer:
|
|
从代码可以看出,AspectsContainer对象是通过关联对象的方式存储到NSObject+Aspects分类中的,其属性名称是使用hook版本的selector名称动态确定的。即每当我们hook一个selector后,NSObject类中即增加了一个名为“aspects_xxx”的属性,其类型为AspectsContainer。 对于加入其内部的AspectIdentifier对象,则根据options保存到不同的内部数组中。
对于AspectIdentifier的生成过程,如下所示:
|
|
即只有当block对象包含完整的签名时,才可以生成AspectIdentifier对象。
结合AspectBlockRef的结构数据,其生成过程如下:
|
|
检查生成的block的签名对象是否正确的方法,就是与原始selector的签名对象进行直接比较(比较每个参数是否相同):
|
|
Aspects中hook完成后,方法的执行过程
方法的执行过程,即hook版本的forwardInvocation: 方法的执行过程,也就是 ASPECTS_ARE_BEING_CALLED 的实现过程:
|
|
整个流程主要是:
- beforeBlock执行(如果有) -> 原始方法执行 -> afterBlock执行(如果有) -> 清除方法的hook状态(如果有)
- 原始方法实现如果不存在:如果原始类实现了forwardInvocation: 方法,则直接执行,走原始的消息转发流程了;如果没有实现,则直接抛异常(因为Aspects的流程已经走完)。其实整体来看,也是遵循原始类的方法调用过程。
这里,需要看一下aspect_invoke函数的执行过程:
|
|
此函数的作用是:
- 依次使用AspectIdentifier对象进行block调用,传入AspectInfo信息。
- 如果执行后需要移除此AspectIdentifier对象(清除hook状态),直接插入到数组中,最后统一清理。
最后我们看一下,AspectIdentifier的invokeWithInfo方法是如何执行的:
|
|
Aspects清除方法的hook状态,恢复原始实例/类
在外部,调用Aspects的API,hook完成相关方法后,得到的返回值为遵循AspectToken协议的对象。在内部,实际上是AspectIdentifier作为此协议的代理进行实现。故清除hook状态的实现即为AspectIdentifier的remove实现,也就是aspect_remove函数:
|
|
其中,从hook状态恢复过程如下:
|
|
首先,统一将selector的IMP替换回来。
对于instance调用者,清除hook状态,只要将其isa指回到原来的类即可。
对于Class调用者来说,则稍微麻烦一些:
首先需要移除全局标记的方法信息:
|
|
对于关联对象,这里正好有个知识点:
|
|
由于关联对象并没有提供单独清除某一个属性的值的方法(只有全部清除),故Aspects使用设置相关对象值为nil的方式进行了清零。
顺便移除了AspectIdentifier的容器对象。
对于复原类hook的forwardInvocation方法,也比较直接:
|
|
参考资料: