本文为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库(三)进行探索。
网友评论