本文为L_Ares个人写作,以任何形式转载请表明原文出处。
一、Hook后的调用
经过上一节和上上节步骤,实际上我们已经完成了对类和方法的hook,这里就要知道,在完成了hook的操作后,原方法和block
参数内的函数,到底是如何执行的?
按照上两节获得的信息,我们可以把这个问题转变成这样 :
已知 :
被hook的方法
的IMP
被置换成了objc_msgForward
。而objc_msgForward
最后会引发被hook的方法
所属的被hook的对象
的forwardInvocation
方法的调用。被hook的对象的
forwardInvocation
方法的IMP
被置换成__ASPECTS_ARE_BEING_CALLED__
。所以这个问题就是问
__ASPECTS_ARE_BEING_CALLED__
到底做了什么?
二、ASPECTS_ARE_BEING_CALLED
参数
因为
__ASPECTS_ARE_BEING_CALLED__
是利用forwardInvocation
这个SEL
在dispatch_table
中被查找到的,所以,__ASPECTS_ARE_BEING_CALLED__
的参数和forwardInvocation
的参数是通用的。1.
__unsafe_unretained NSObject *self
:被hook的对象
。首先看
__unsafe_unretained
,这个东西和__weak
其实是相像的,它表示的是对象的弱引用关系,也就是说,在这个函数里面的self
都是弱引用的。__weak
和__unsafe_unretained
的区别是 :当被
__weak
修饰的对象被释放后,系统就要遍历weak表
,将对象的指针指向nil
,而__unsafe_unretained
则不会将对象的指针指向nil
,那么对象就会变成野指针。所以,要使用
__unsafe_unretained
的话,必须清楚它修饰的对象的生命周期。那么这里为什么要弱引用
self
?还有一个问题,就是在之前的AspectIdentifier
中,object
属性也是weak
的,object
为什么也要弱引用?在解决完这个函数的逻辑之后,再详细说。2.
SEL selector
:被hook的方法
的SEL
。3.
NSInvocation *invocation
:被hook的方法
的调用信息。
实现
先放一张整体的代码图,方便使用。后面会根据不同的区域,详细的说明。
二、准备区
准备区的逻辑还是比较简单的,其中比较重点可能就是从关联表中取出AspectsContainer
容器了。
先进行断言的判断,判断参数的合法性。
保存一个
被hook的方法
的原始SEL originalSelector
。
因为我们替换了整个类的forwardInvocation:
方法,但是未必hook
这个类的所有方法。
所以一旦出现某个未被hook方法
没有实现的情况,还是要执行forwardInvocation:
方法进行消息转发的。生成一个新的
SEL aliasSelector
。
命名方式是在originalSelector
基础上添加aspects__
前缀,例如 :aspects__ originalSelector
。将
invocation
中方法原始的SEL originalSelector
用新的SEL aliasSelector
替换掉。这里就解释了上一节中最开始的第2个问题。获取在对方法进行
hook
的时候,存放在关联表中的AspectsContainer
容器对象。这里取了两种容器,分别对应如下情况 :
objc_getAssociatedObject(self, aliasSelector);
:
(1) 是调用Aspects
库的-实例方法
才可以拿到其结果objectContainer
。
(2). 因为是具体的实例对象
和AspectsContainer
进行的关联。
(3). 在关联
AspectsContainer
和实例对象self
的时候,就是以aliasSelector
未键,存放的就是在self
的关联表中。
(4). 虽然在上一节的aspect_hookClass()
方法中,我们替换了self
的isa指针
指向了新生成的中间类,但是self
本身的内存地址并没有发生改变,所以其产生的关联表也未发生改变,一样可以通过被hook的对象self
在关联总表
中找到原有的self的关联表
。
aspect_getContainerForClass(object_getClass(self), aliasSelector);
:
(1). 是调用Aspects
库的+类方法
才可以拿到其结果classContainer
。
(2). 原因先看下面的aspect_getContainerForClass
源码。
static AspectsContainer *aspect_getContainerForClass(Class klass, SEL selector) {
//断言区
NSCParameterAssert(klass);
//定义一个空容器
AspectsContainer *classContainer = nil;
//循环遍历klass的继承链
do {
//从关联对象表中取出容器并赋值
classContainer = objc_getAssociatedObject(klass, selector);
//只要容器中的任一一个数组拥有元素,就会结束do{...}while()循环
if (classContainer.hasAspects) break;
}while ((klass = class_getSuperclass(klass)));
//返回容器
return classContainer;
}
- 接上面的解释。
(3). 在上面的源码中,klass
是object_getClass(self)
,也就是调用者的isa指向的类
。
(4). 而self
一定是调用Aspects
库的+类方法
的类的实例对象
。
(5). 在调用Aspects
库的+类方法
存储AspectsContainer
的时候,AspectsContainer 对象
是以aliasSelector
为键,存放在self的父类
的关联表中。
(6). 所以,要沿着self
的继承链向下查找拥有关联表的父类,再利用其类名,取到父类的关联表,才能以aliasSelector
为键拿到对应的容器。- 初始化一个
AspectInfo
对象。
(1). 其属性instance
是以__unsafe_unretained
修饰的只读属性,存放self
对象。这也解释了上面为什么参数中的self
要用__unsafe_unretained
修饰。
(2). 其属性invoke
存放的就是SEL
被更改后的invocation
参数。也就是当前的一些调用信息。- 定义一个存放
等待注销的hook
的数组。
三、执行区
执行区的内容则是本节的重中之重。这里也解释了上一节最开始的3个问题中的第3个问题。
执行区的代码中,多次调用的一个宏 : aspect_invoke
就是执行block
的代码中的操作。
看这个宏 : aspect_invoke
的实现:
//遍历AspectsContainer容器中的AspectIdentifier对象(被hook的对象和类的信息)
//调用AspectIdentifier的"invokeWithInfo:"方法,参数为上面定义的AspectInfo对象
for (AspectIdentifier *aspect in aspects) {
[aspect invokeWithInfo:info];
if (aspect.options & AspectOptionAutomaticRemoval) {
aspectsToRemove = [aspectsToRemove?:@[] arrayByAddingObject:aspect];
}
}
宏 : aspect_invoke
一共实现了两个功能 :
执行我们调用
Aspects
库公开API的时候,定义在block
中的代码和被hook的方法
的原始代码。判断
被hook的方法
的是不是只被hook一次,如果定义了只被hook一次,那么就在执行完成1步骤后,将存放被hook的方法
和被hook的对象
的AspectIdentifier
对象放入待移除的数组。等待下面的移除操作。
1. invokeWithInfo
在这里,self
就是要调用的AspectIndentifier
对象。也就是上面宏 : aspect_invoke
中的aspect
。
参数
(id
: 所有遵循)info AspectInfo
协议的代理对象都可以做参数。这里我们传入的是上一步中初始化的AspectInfo
对象。
实现
- (BOOL)invokeWithInfo:(id)info {
//获取存储在AspectIdentifier对象属性中的block签名信息
//从block签名信息中获取NSInvocaiotn调用信息对象
NSInvocation *blockInvocation = [NSInvocation invocationWithMethodSignature:self.blockSignature];
//拿到方法原始的Invocation
NSInvocation *originalInvocation = info.originalInvocation;
//获取block中的参数数量
NSUInteger numberOfArguments = self.blockSignature.numberOfArguments;
// Be extra paranoid. We already check that on hook registration.
// 额外的检查,在hook的时候已经检查过block的参数数量是不可以多于原始方法的参数数量的
if (numberOfArguments > originalInvocation.methodSignature.numberOfArguments) {
AspectLogError(@"Block has too many arguments. Not calling %@", info);
return NO;
}
// The `self` of the block will be the AspectInfo. Optional.
// 如果参数的数量多于1,那么就获取AspectInfo对象的指针地址传入,放到block的第二个参数位置上
// 之所以放到第二个参数位置,是因为block的第一个参数位置是自身。
if (numberOfArguments > 1) {
[blockInvocation setArgument:&info atIndex:1];
}
//定义一个空的指针,用来表示参数的buffer
void *argBuf = NULL;
// 从2开始循环是因为,一旦Aspects库的block块带有了参数,那么
// block块的参数前两位是确定的,第一个一定是block自身,第二个一定是AspectInfo
for (NSUInteger idx = 2; idx < numberOfArguments; idx++) {
//通过原始方法的方法签名,获取原始方法的每一个参数的type encoding
const char *type = [originalInvocation.methodSignature getArgumentTypeAtIndex:idx];
//定义一个参数的大小
NSUInteger argSize;
//获取type表示的类型的实际大小和偏移量大小,并切将type类型参数的实际大小放入到argSize的内存上存储
NSGetSizeAndAlignment(type, &argSize, NULL);
//利用reallocf重新分配一块argSize大小的内存,保存在argBuf指向的内存空间中
if (!(argBuf = reallocf(argBuf, argSize))) {
//如果申请内存失败,就记录错误日志,返回NO
AspectLogError(@"Failed to allocate memory for block invocation.");
return NO;
}
//取出原方法对应位置上的参数,存放在argBuf中
[originalInvocation getArgument:argBuf atIndex:idx];
//然后把argBuf中的参数再放入block方法的对应位置上
[blockInvocation setArgument:argBuf atIndex:idx];
}
//设置block块为接收者,接受blockInvocation的消息,并且调用block块内的函数执行
[blockInvocation invokeWithTarget:self.block];
//释放掉定义的buffer
if (argBuf != NULL) {
free(argBuf);
}
//返回成功转发并调用
return YES;
}
内容全部都在上面的注释中,写的也比较详细了,就不再赘述。
2. 三种Options对应的执行方法
2.1 在原有方法前执行block
2.2 替换原有方法执行
if
表示在AspectsContainer
容器的属性insteadAspects
数组拥有对象,也就是在Aspects的公开API
的参数options
中选择了AspectPositionInstead
。insteadAspects
数组没有对象,也就是被hook
的对象没有添加过options
为AspectPositionInstead
的对象。那就执行原方法。- 原方法在上一节讲
aspect_prepareClassAndHookSelector
的时候,已经讲过,原方法的IMP会被存入aliasSelector
,并且一起被添加到被hook的对象
所属的类中。
2.3 在原有方法后执行
四、未被hook的方法执行forwardInvocation:
方法
这里的逻辑其实不难,还是分成3种情况来说明 :
- 被hook的对象是实例对象。
- 被hook的对象是类对象。
- 被hook的对象是实例对象,但是实例对象处于KVO键值观察中。
先看一眼剩下的这部分源码 :
// If no hooks are installed, call original implementation (usually to throw an exception)
//没被hook的方法,又调用了forwardInvocation:方法的,走到这里
if (!respondsToAlias) {
invocation.selector = originalSelector;
SEL originalForwardInvocationSEL = NSSelectorFromString(AspectsForwardInvocationSelectorName);
if ([self respondsToSelector:originalForwardInvocationSEL]) {
((void( *)(id, SEL, NSInvocation *))objc_msgSend)(self, originalForwardInvocationSEL, invocation);
}else {
[self doesNotRecognizeSelector:invocation.selector];
}
}
// Remove any hooks that are queued for deregistration.
//移除所有等待注销的hook
[aspectsToRemove makeObjectsPerformSelector:@selector(remove)];
1. 被hook的对象是普通的实例对象
当
被hook的对象
的原始类
存在其他的实例对象
时 :
其他的实例对象
如果调用一个原始类
并没有实现的方法,你想利用forwardInvocation:
方法进行消息转发,让其他的类来实现的时候,其实根本不会进入到当前这个ASPECTS_ARE_BEING_CALLED
的实现中来。原因很简单,因为如果
被hook的对象
时普通的实例对象,那么上一节的步骤aspect_hookClass
方法的默认情况区
的代码,就已经把被hook的对象
的isa
指向变成了带有_Aspects_
后缀的中间类
了,改变的是中间类
的forwardInvocation:
方法的IMP
,让这个IMP
变成ASPECTS_ARE_BEING_CALLED
。所以根本不会影响
其他的实例对象
,因为其他的实例对象
的isa
指向仍然是默认的原始类
,调用的依然是原始类
的forwardInvocation :
方法。
2. 被hook的对象是类对象
当
被hook的对象
是类对象时,也就是调用的是Aspects
库公开API中的+类方法
时,hook
的是整个类,所以只要是由这个被hook的类对象
实例化出来的对象,如果调用了forwardInvocation :
方法,全部都会进入到ASPECTS_ARE_BEING_CALLED
实现。同时,因为
hook
的是整个类对象,所以类对象在上一节的aspect_swizzleForwardInvocation
方法中,已经做了处理,如果你实现了forwardInvocation :
方法,这个实现会被动态的以AspectsForwardInvocationSelectorName
为SEL,添加到在类中。那么这里的
if ([self respondsToSelector:originalForwardInvocationSEL])
就会为真,可以进入if
的条件语句,手动的调用objc_msgSend
进行转发。如果你没有手动实现
forwardInvocation :
方法,又对未被hook的方法
没有实现,而且还进入到了forwardInvocation :
这一步,那么也会因为无法响应未被hook的方法
进入else
的条件语句,直接调用doesNotRecognizeSelector
进行报错处理。
3. 被hook的对象正在被KVO键值观察
这种情况和第1种情况是一样的。其他的实例对象并没有
被hook
,更换的是NSKVONotifying_ 父类名
这个KVO中间类的forwardInvocation :
方法,所以也不会对其他的父类的实例对象造成影响。
五、解决这两节遗留下的问题
5.1 上一节的问题
这是上一节的,其实已经在上面一步四、未被hook的方法执行forwardInvocation:方法中解决了。
注意
:这里还有另外一种情况 :
如果
被hook的方法
在forwardingTargetForSelector
中被转发走了,那么Aspects
库的功能就失效了。原因很简单吧,_objc_msgForward
不是直接就调用forwardInvocation :
的,在这之前,还会进行一步快速的消息转发,也就是forwardingTargetForSelector
。具体的情况可以看我之前的第十二节—objc_msgSend(四)消息转发。
5.2 本节的问题
这是在上面我留下的问题 :
为什么
ASPECTS_ARE_BEING_CALLED
的参数self
,还有AspectInfo
中的instance
属性要用__unsafe_unretained
修饰?为什么
AspectIdentifier
中的属性object
要用weak
修饰?
原因 :
ASPECTS_ARE_BEING_CALLED
和AspectInfo
这个可以归在一起,因为ASPECTS_ARE_BEING_CALLED
的self
需要用__unsafe_unretained
修饰的原因就是AspectInfo
。
那为什么AspectInfo
的属性instance
要用__unsafe_unretained
修饰?
原因一定是为了避免循环引用,所以我就直接写循环引用链就好了。循环引用链1 :
self
-->AspectsContainer
-->AspectIdentifier
-->block
-->AspectInfo
-->self
所以最后的AspectInfo
对self
的引用要用__unsafe_unretained
进行弱引用。
AspectIdentifier
的属性object
用weak
修饰是为了避免下面的循环引用链形成 :循环引用链2 :
self
-->AspectsContainer
-->AspectIdentifier
-->self
至此,AOP之Aspects库
系列全部结束。