本文为L_Ares个人写作,以任何形式转载请表明原文出处。
接上一节,继续探索。本节将从
AspectsContainer、AspectIdentifier来入手,探索Aspects库到底是如何完成了hook。

一、先记录几个问题
- 首先,已知
Aspects库可以完成在被hook的方法的前、后添加代码,也可以替换被hook的方法的原有代码。- 其次,在上一节的例子中,当调用
Aspects库的公开API中的两个方法时,被hook的方法的_cmd也就是方法的SEL名称发生了改变,出现了aspects__前缀,变成了aspects__被hook的方法的SEL,这是怎么回事?(可见下图1.1.0)。- 再次,用来增加或者替换被hook的方法的
block参数块中的函数,是如何完成了method_swizzling的。

二、初始化容器AspectsContainer
上一节探索到 :
AspectsContainer *aspectContainer = aspect_getContainerForObject(self, selector);
这一行代码的上面,探索完了Aspects库对被hook的类和被hook的方法的合法性校验。
这一节从这一行代码开始,先探索aspect_getContainerForObject和AspectsContainer。
这里主要是对
AspectsContainer做操作。
- 获取被hook的对象的
AspectsContainer容器。- 主要思想就是通过关联对象方式,将被hook的方法
selector的名字重命名加上前缀变成aspects__selector,然后以它为键,在self的关联表中查询对应的容器。AspectsContainer只有3个属性,都是数组,分别存储beforeAspects、insteadAspects、afterAspects。
1. aspect_getContainerForObject
释义 : 该方法是获取
AspectsContainer容器。容器对象存储的内容是所有被hook的对象/类。方法的返回值是一个AspectsContainer对象。
方法的功能和注释在
下图2.1.0中。

2. aspect_aliasForSelector
释义 :
上图2.1.0中,对关联表的键aliasSelector的生成方式。
static SEL aspect_aliasForSelector(SEL selector) {
NSCParameterAssert(selector);
/**
1. 方法中的宏 :
static NSString *const AspectsMessagePrefix = @"aspects_";
2. 返回的值 :
关联表的键的命名方式 = "aspects_" + "被hook的方法的SEL"
*/
return NSSelectorFromString([AspectsMessagePrefix stringByAppendingFormat:@"_%@", NSStringFromSelector(selector)]);
}
三、初始化AspectIdentifier
这里就进入到
aspect_add()函数对被hook的类和被hook的selector的信息保存。它们的原始信息都存储在了AspectIdentifier对象中。
//直接调用的AspectIdentifier的初始化方法,构造一个对象
identifier = [AspectIdentifier identifierWithSelector:selector object:self options:options block:block error:error];
1. AspectIdentifier的属性
SEL selector: 被hook的方法。
id block: hook后要执行的操作。
NSMethodSignature *blockSignature: block的签名信息。
id object: 被hook的类。
AspectOptions options:block的执行位置。
2. AspectIdentifier初始化方法

很常规的构造函数,唯二的特点是对参数中
block块的签名的获取和校验。
3. aspect_blockMethodSignature
该方法是获取block的签名信息。
参数
block: 要获取签名信息的block。error: 获取签名信息出现的错误信息。
实现

上图3.3.0中,AspectBlockRef结构体的结构 :

实现的思路非常的简单,在
block的章节中介绍过,可以进我的主页看。
实现思路大体 :
- 通过位移
block的指针,从block的首地址,位移到block结构体的desc3上面。desc3中的signature元素存储了block块的type encoding字符。- 利用
NSMethodSignature的方法,将type encoding字符转成NSMethodSignature对象。这个对象也就是所谓的block块的签名。
4. aspect_isCompatibleBlockSignature
该方法是对上面
3.aspect_blockMethodSignature获得的block块的签名的兼容性验证。
参数
blockSignature: 要验证兼容性的block的签名信息。object: 被hook的类。selector: 被hook的方法。error: 错误信息。
实现

Aspects库在公开API的注释中说不允许hook静态方法。因为这里调用的是instanceMethodSignatureForSelector。block块的type encoding格式在上一节说过 :返回值的type encoding+block的type encoding : @?+参数的type encoding。
- 第一个位置 : 返回值的
type encoding。- 第二个位置 : block的type encoding,也就是
@?。- 判断一下
block的参数数量是否大于1。
- 等于1说明 :
block的签名中,参数只有block自己。- 大于1说明 :
block不是一个空的block,也就是说不是^(void){}这种无参数block。而是有除block自身外,其他的参数。比如上一节的例子中的 :id<AspectInfo> aspectInfo(存在的话一定在block的参数的第2个位置上)、name、age、sex。block的参数数量绝对不可以多于被hook的方法的参数数量。
四、AspectIdentifier对象加入AspectsContainer容器
能进入到这里,表明
被hook的类和被hook的方法以及block参数中用来插入或者替换的函数都是符合Aspects库的规定的。
[aspectContainer addAspect:identifier withOptions:options];
参数
aspect: 要加入容器的AspectsIdentifier对象。options:block的执行位置信息。
实现
- (void)addAspect:(AspectIdentifier *)aspect withOptions:(AspectOptions)options {
// 1. 断言区
NSParameterAssert(aspect);
// 2. 利用positionFilter过滤器,获取AspectOptions参数中的,想要进行hook的位置,存入position中
NSUInteger position = options&AspectPositionFilter;
// 3. 利用position判断被hook的类和方法属于AspectsContainer容器中哪个数组,存入相应的数组。
switch (position) {
case AspectPositionBefore: self.beforeAspects = [(self.beforeAspects ?:@[]) arrayByAddingObject:aspect]; break;
case AspectPositionInstead: self.insteadAspects = [(self.insteadAspects?:@[]) arrayByAddingObject:aspect]; break;
case AspectPositionAfter: self.afterAspects = [(self.afterAspects ?:@[]) arrayByAddingObject:aspect]; break;
}
}
五、类的准备工作和方法的Hook
能进入这里,也是表明
被hook的类和被hook的selector以及用来插入或置换的block块函数都是符合Aspects库的规定的。
从这里开始,就是真正的对
被hook的类、被hook的selector、block块内的函数进行操作了。
aspect_prepareClassAndHookSelector(self, selector, error);
1. 方法的整体逻辑
先来看这个函数的整体实现逻辑,然后挑出其中的封装逻辑再详细探索。
参数
self: 被hook的对象。
selector: 被hook的方法。
error: 可能发生的错误信息。
实现

2. aspect_hookClass()
该方法是对
被hook的对象的类做hook后的处理。

我给这个方法分成了3个区域,方便理解,最主要的是看
图5.2.0中的注释,下面依次说明3个区域的功能。
2.1 方法准备区
代码非常的简单,逻辑也简单 :
- 断言判断参数的合法性。
statedClass:
- 如果
self是类对象 :statedClass就是类对象本身。- 如果
self是实例对象 :statedClass就是实例对象所属的类。baseClass:
- 如果
self是类对象 :baseClass就是元类。- 如果
self是实例对象 :baseClass就是实例对象所属的类。className:
- 如果
self是类对象 :className就是元类名称字符串。- 如果
self是实例对象 :className就是实例对象所属的类的名称字符串。
2.2 特殊情况区
if ([className hasSuffix:AspectsSubclassSuffix]):如果
self的isa指向的类已经有_Aspects_后缀。例如上节案例中的ViewController,原本它的isa指向的类是元类ViewController,如果它的isa指向的类变成了ViewController_Aspects_,则表明它被hook过。可以直接返回
baseClass。
else if (class_isMetaClass(baseClass)):进入这里,则代表在调用
Aspects的公开API时,调用的是+方法,也就是类方法,说明要hook的是整个类对象,而不是类的某个实例对象。返回
aspect_swizzleClassInPlace((Class)self)。
else if (statedClass != baseClass):进入这里,表示上一步没有发生,也就是说,实例对象才会进入到这个判断。
一般情况下,
实例对象的类和实例对象的isa指向的类是同一个类。
如果发生不是同一个类的情况,则证明,该对象有可能出现了特殊情况,比如进行着键值观察(KVO)。返回
aspect_swizzleClassInPlace(baseClass)。
在这个区域,处理了公开API中,类方法的调用者。以及正在被KVO观察的对象的isa指向的类。并且,它们调用的方法都是aspect_swizzleClassInPlace,只不过传参不同。
2.2.1 aspect_swizzleClassInPlace
static Class aspect_swizzleClassInPlace(Class klass) {
//断言区
NSCParameterAssert(klass);
//获取传入类的名称字符串
NSString *className = NSStringFromClass(klass);
//单例创建的一个集合,存储已经发生swizzled的类
//函数的参数是一个block,那么参数block的执行,就要看函数的实现中,block在哪里被执行
//所以block里面的代码,需要看函数的实现,才能知道什么时候被执行
_aspect_modifySwizzledClasses(^(NSMutableSet *swizzledClasses) {
//如果当前被hook的类,不在已发生swizzled的类集合中
if (![swizzledClasses containsObject:className]) {
//swizzled被hook的类的forwardInvocation方法
aspect_swizzleForwardInvocation(klass);
//添加这个类到已发生swizzled的类的集合
[swizzledClasses addObject:className];
}
});
return klass;
}
有两点 :
- 是
_aspect_modifySwizzledClasses- 是
aspect_swizzleForwardInvocation
先看_aspect_modifySwizzledClasses。
2.2.2 _aspect_modifySwizzledClasses
static void _aspect_modifySwizzledClasses(void (^block)(NSMutableSet *swizzledClasses)) {
//定义静态可变集合,存储已经发生过混合的类
static NSMutableSet *swizzledClasses;
//下面很明显是单例模式创建可变集合
static dispatch_once_t pred;
dispatch_once(&pred, ^{
swizzledClasses = [NSMutableSet new];
});
//这里就是block被执行的地方,用自旋锁保证线程安全
@synchronized(swizzledClasses) {
block(swizzledClasses);
}
}
明显是一个以
带参数的block做参数的函数。先初始化一个静态的可变集合,用来存储已经发生过
swizzled的类,利用单例初始化,然后把集合当参数,传入参数block,并且调用block。所以,最后的重点还是
block内都对传入的类做了什么。
再看aspect_swizzleForwardInvocation
2.2.3 aspect_swizzleForwardInvocation
static void aspect_swizzleForwardInvocation(Class klass) {
//断言区
NSCParameterAssert(klass);
// If there is no method, replace will act like class_addMethod.
/**
1. class_replaceMethod : 可以看一下苹果的官方文档,如果方法不存在,这个方法会像class_addMethod一样去添加这个方法到klass里面。
2. 替换klass(被hook的)的forwardInvocation方法的IMP实现,并且把原有的IMP返回。
3. 之所以要操作klass的forwardInvocation方法,是因为方法的最后查找步骤是forwardInvocation:消息转发
*/
IMP originalImplementation = class_replaceMethod(klass, @selector(forwardInvocation:), (IMP)__ASPECTS_ARE_BEING_CALLED__, "v@:@");
//如果被hook的对象的isa指向的中间类(也就是添加了_Aspects_后缀的中间类),已经实现了forwardInvocation方法
if (originalImplementation) {
//将原有就存在的forwardInvocation方法的IMP添加给__aspects_forwardInvocation:这个方法
class_addMethod(klass, NSSelectorFromString(AspectsForwardInvocationSelectorName), originalImplementation, "v@:@");
}
//打印日志
AspectLog(@"Aspects: %@ is now aspect aware.", NSStringFromClass(klass));
}
也就是说,这个方法把传入类的
forwardInvocation:方法的IMP替换了。对于参数
klass:
如果
klass是类本身,也就是通过+方法进入到这里,那么整个类的forwardInvocation方法的IMP实现,都将被替换成__ASPECTS_ARE_BEING_CALLED__。(关于其他未被hook的方法如果没有实现,该怎么办,后面在该函数的解释里会说明)如果
klass是另一种情况,即被hook的对象的isa指向的类不是一般情况下的自己的父类,而是出现类似KVO键值观察的情况。那么要替换的就是NSKVONotifying_父类名类的forwardInvocation方法的IMP实现。问题 :
其实这里会出现一种情况 :
当
被hook的对象是一个KVO键值观察对象或者就是普通的实例对象,如果被hook的对象所属的类中有未被实现的方法,并且,没实现的方法还不是被hook的方法。那么首先就会进入
动态决议,如果动态决议依旧没有实现,则会进入_objec_msgForward,进行消息转发。如果我们不实现消息转发流程中的
快速转发,则最后会调用一次NSKVONotifying_父类名类的forwardInvocation方法。但是这个方法的IMP被交换了,那是不是没实现的方法也会进入
__ASPECTS_ARE_BEING_CALLED__这个IMP实现呢?会怎么处理呢?在下面__ASPECTS_ARE_BEING_CALLED__方法解析的时候会有介绍。
2.3 默认情况区
这里就是
aspect_hookClass()函数的默认实现,也就是针对 :
- 从未经过
aspect_hook()处理的实例对象。- 不是类对象。
- 不是
KVO观察对象。这三种情况除外的,普通的,被hook的实例对象的,类的处理。
// Default case. Create dynamic subclass.
//默认的情况下,上面的几个if条件都不满足,那么就要自己动手创建动态的子类
//给isa所指的类的名字前面加上Aspects库的后缀
const char *subclassName = [className stringByAppendingString:AspectsSubclassSuffix].UTF8String;
//看清楚这里,这是objc_getClass而不是object_getClass
//两者有明确的区别,objc_getClass(subclassName)和[self class]有点像,返回的都是类本身
//只不过objec_getClass的参数是const char*类型,传入类的名字就可以拿到一个类
//而object_getClass则是获取参数的isa指向的类
Class subclass = objc_getClass(subclassName);
//如果这个类还不存在
if (subclass == nil) {
//创建subclass的元类和类,并设置baseClass为subclass的父类
subclass = objc_allocateClassPair(baseClass, subclassName, 0);
//这个subclass不能被创建
if (subclass == nil) {
//报错
NSString *errrorDesc = [NSString stringWithFormat:@"objc_allocateClassPair failed to allocate class %s.", subclassName];
AspectError(AspectErrorFailedToAllocateClassPair, errrorDesc);
//返回nil
return nil;
}
//设置这个新类的forwardInvocation方法的IMP
aspect_swizzleForwardInvocation(subclass);
//设置新类的-(void)class方法的IMP,让新类的-(void)class方法返回的是被hook的对象的类
aspect_hookedGetClass(subclass, statedClass);
//设置新类的元类的-(void)class方法(也就是新类的+(void)class方法)的IMP,也是返回被hook的对象的类
aspect_hookedGetClass(object_getClass(subclass), statedClass);
//把新类注册到runtime中,这样这个新类才算realized的。
objc_registerClassPair(subclass);
}
//设置self(被hook的对象)的isa指向为subclass类(新类)
object_setClass(self, subclass);
//返回这个新类
return subclass;
还是重点看一下注释情况,然后从中可以找到一个没有解析过,并且封装起来的方法
aspect_hookedGetClass。看实现。
static void aspect_hookedGetClass(Class class, Class statedClass) {
//class是新类和新类的元类,有Aspects库的后缀。
//statedClass是被hook的对象的类
//断言区
NSCParameterAssert(class);
NSCParameterAssert(statedClass);
//拿到class类的-(void)class方法
Method method = class_getInstanceMethod(class, @selector(class));
//设置一个新的IMP,IMP的实现是返回一个被hook的对象的类
IMP newIMP = imp_implementationWithBlock(^(id self) {
return statedClass;
});
//替换掉class类的-(void)class方法的IMP为newIMP,返回statedClass类
class_replaceMethod(class, @selector(class), newIMP, method_getTypeEncoding(method));
}
这个方法的目的 :
- 把新生成的
中间类和中间类的元类的class方法,全都返回被hook的对象的类。- 这样做了以后,哪怕在下面的
object_setClass中,将被hook的对象的isa指向变成了中间类,也不会影响被hook的对象调用-(void)class方法返回的是其原来的类。
3. 被hook的方法的处理
上面的2.aspect_hookClass()完成了对
被hook的对象的类的处理,这里则开始对被hook的方法进行处理。
这里截取的是上图5.1.0中,Class klass = aspect_hookClass(self,error)之后的代码,也就是对被hook的方法的处理。

看图中画了红框的部分,从上到下,依此说一下设计实现的思路。
1. 首先,Aspects库的作者是利用class_getInstanceMethod来获取klass中的SEL为selector的方法的。
Method targetMethod = class_getInstanceMethod(klass, selector);
使用
class_getInstanceMethod是因为klass已经被处理过,被处理的klass无非就3种情况,在上面的aspect_hookClass中已经介绍过,被hook的对象可能是 :
是普通的实例对象 :
则klass就是中间类。
中间类名称是组成是 :被hook的对象的类的名字 +_Aspects_后缀。是类对象 :
则klass的类不发生改变,依然是被hook的类对象。是被
KVO键值观察的实例对象 :
则klass是KVO的中间类。
KVO的中间类名称组成是 :NSKVONotifying_+实例对象的类名这3种
klass都有着一个绝对的共同点 : 全部继承于被hook的对象的类。所以,
class_getInstanceMethod一定可以在klass的继承链上,找到selector的方法。然后得到targetMethod。
2. 获得targetMethod的IMP
没什么可说的,就是获得
被hook的方法的原始IMP,直接用objc的API,获得Method的IMP。
IMP targetMethodIMP = method_getImplementation(targetMethod);
3. 判断被hook的方法的原始IMP如果不是直接调用_objc_msgForward
if (!aspect_isMsgForwardIMP(targetMethodIMP))
aspect_isMsgForwardIMP的实现 :
static BOOL aspect_isMsgForwardIMP(IMP impl) {
//这个是arm64架构,也就是iOS系统,手机真机的情况下,会直接调用_objc_msgForward
return impl == _objc_msgForward
//不要看这里了,这是非arm64架构下的,消息转发是调用的_objc_msgForward_stret
#if !defined(__arm64__)
|| impl == (IMP)_objc_msgForward_stret
#endif
;
}
一般正常的情况下,我们是不会直接给一个方法的实现写成
_objc_msgForward的。所以大多数的情况,这里的BOOL值都是NO。一般都会进入if判断中的代码。
4. 给中间类添加一个方法
// Make a method alias for the existing method implementation, it not already copied.
//获取被hook的方法的typeEncoding
const char *typeEncoding = method_getTypeEncoding(targetMethod);
//获取被hook的方法的别名SEL
SEL aliasSelector = aspect_aliasForSelector(selector);
//如果被hook的类不响应这个aliasSelector
if (![klass instancesRespondToSelector:aliasSelector]) {
//把这个aliasSelector关联上targetMethod的实现,然后添加到klass上
__unused BOOL addedAlias = class_addMethod(klass, aliasSelector, method_getImplementation(targetMethod), typeEncoding);
NSCAssert(addedAlias, @"Original implementation for %@ is already copied to %@ on %@", NSStringFromSelector(selector), NSStringFromSelector(aliasSelector), klass);
}
也很好理解,只是给klass添加了一个新的方法,方法的SEL名称格式是 :
aspects_ + 被hook的方法的SEL。很熟悉,在给容器添加关联对象的时候出现过,这个SEL名称是容器在关联表中的键。
5. 替换被hook的方法的IMP
//我们利用forwardInvocation方法hook进去
//用_objc_msgForward(消息转发)替换被hook的方法的IMP
class_replaceMethod(klass, selector, aspect_getMsgForwardIMP(self, selector), typeEncoding);
aspect_getMsgForwardIMP的实现,我只截取arm64架构下的实现 :
static IMP aspect_getMsgForwardIMP(NSObject *self, SEL selector) {
//只看这里,这里是arm64架构
IMP msgForwardIMP = _objc_msgForward;
//不用看了
#if !defined(__arm64__)
... ...
#endif
return msgForwardIMP;
}
所以,现在,
被hook的方法的SEL还是原来的SEL,但是IMP已经换成了objc_msgForward了。
也就是说,现在如果再调用
被hook的方法,就相当于直接调用objc_msgForward,进入消息转发。而上面我们刚说过,
klass类的forwardInvocation全部都被替换成了__ASPECTS_ARE_BEING_CALLED__,也就是说 :当完成
aspect_prepareClassAndHookSelector后,再调用被hook的方法,相当于直接调用到了__ASPECTS_ARE_BEING_CALLED__。但是这里还有一个问题存在,如果我实现了
forwardingTargetForSelector怎么办?这个问题放到最后一起解决。
注释
如何执行被替换的block和如何执行原有方法,将会放入下一节,AOP之Aspects库(三)进行探索。