第三十九节—AOP之Aspects库(三)

本文为L_Ares个人写作,以任何形式转载请表明原文出处。

一、Hook后的调用

经过上一节和上上节步骤,实际上我们已经完成了对类和方法的hook,这里就要知道,在完成了hook的操作后,原方法和block参数内的函数,到底是如何执行的?

按照上两节获得的信息,我们可以把这个问题转变成这样 :

已知 :

  1. 被hook的方法IMP被置换成了objc_msgForward。而objc_msgForward最后会引发被hook的方法所属的被hook的对象forwardInvocation方法的调用。

  2. 被hook的对象的forwardInvocation方法的IMP被置换成__ASPECTS_ARE_BEING_CALLED__

  3. 所以这个问题就是问__ASPECTS_ARE_BEING_CALLED__到底做了什么?

二、ASPECTS_ARE_BEING_CALLED

参数

因为__ASPECTS_ARE_BEING_CALLED__是利用forwardInvocation这个SELdispatch_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的方法的调用信息。

实现

先放一张整体的代码图,方便使用。后面会根据不同的区域,详细的说明。

图2.0.0

二、准备区

准备区的逻辑还是比较简单的,其中比较重点可能就是从关联表中取出AspectsContainer容器了。

  1. 先进行断言的判断,判断参数的合法性。

  2. 保存一个被hook的方法的原始SEL originalSelector
    因为我们替换了整个类的forwardInvocation:方法,但是未必hook这个类的所有方法。
    所以一旦出现某个未被hook方法没有实现的情况,还是要执行forwardInvocation:方法进行消息转发的。

  3. 生成一个新的SEL aliasSelector
    命名方式是在originalSelector基础上添加aspects__前缀,例如 : aspects__ originalSelector

  4. invocation中方法原始的SEL originalSelector用新的SEL aliasSelector替换掉。这里就解释了上一节中最开始的第2个问题。

  5. 获取在对方法进行hook的时候,存放在关联表中的AspectsContainer容器对象。这里取了两种容器,分别对应如下情况 :

    • objc_getAssociatedObject(self, aliasSelector); :

(1) 是调用Aspects库的-实例方法才可以拿到其结果objectContainer
(2). 因为是具体的实例对象AspectsContainer进行的关联。

(3). 在关联AspectsContainer实例对象self的时候,就是以aliasSelector未键,存放的就是在self的关联表中。
(4). 虽然在上一节的aspect_hookClass()方法中,我们替换了selfisa指针指向了新生成的中间类,但是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;
}
  1. 接上面的解释。
    (3). 在上面的源码中,klassobject_getClass(self),也就是调用者的isa指向的类
    (4). 而self一定是调用Aspects库的+类方法的类的实例对象
    (5). 在调用Aspects库的+类方法存储AspectsContainer的时候,AspectsContainer 对象是以aliasSelector为键,存放在self的父类的关联表中。
    (6). 所以,要沿着self的继承链向下查找拥有关联表的父类,再利用其类名,取到父类的关联表,才能以aliasSelector为键拿到对应的容器。
  2. 初始化一个AspectInfo对象。
    (1). 其属性instance是以__unsafe_unretained修饰的只读属性,存放self对象。这也解释了上面为什么参数中的self要用__unsafe_unretained修饰。
    (2). 其属性invoke存放的就是SEL被更改后的invocation参数。也就是当前的一些调用信息。
  3. 定义一个存放等待注销的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一共实现了两个功能 :

  1. 执行我们调用Aspects库公开API的时候,定义在block中的代码和被hook的方法的原始代码。

  2. 判断被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

图3.2.0

2.2 替换原有方法执行

图3.2.1
  1. if表示在AspectsContainer容器的属性insteadAspects数组拥有对象,也就是在Aspects的公开API的参数options中选择了AspectPositionInstead
  2. insteadAspects数组没有对象,也就是被hook的对象没有添加过optionsAspectPositionInstead的对象。那就执行原方法。
  3. 原方法在上一节讲aspect_prepareClassAndHookSelector的时候,已经讲过,原方法的IMP会被存入aliasSelector,并且一起被添加到被hook的对象所属的类中。

2.3 在原有方法后执行

图3.2.2

四、未被hook的方法执行forwardInvocation:方法

这里的逻辑其实不难,还是分成3种情况来说明 :

  1. 被hook的对象是实例对象。
  2. 被hook的对象是类对象。
  3. 被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:方法中解决了。

图5.0.1

注意 :

这里还有另外一种情况 :

如果被hook的方法forwardingTargetForSelector中被转发走了,那么Aspects库的功能就失效了。原因很简单吧,_objc_msgForward不是直接就调用forwardInvocation :的,在这之前,还会进行一步快速的消息转发,也就是forwardingTargetForSelector。具体的情况可以看我之前的第十二节—objc_msgSend(四)消息转发。

5.2 本节的问题

图5.0.2

这是在上面我留下的问题 :

  1. 为什么ASPECTS_ARE_BEING_CALLED的参数self,还有AspectInfo中的instance属性要用__unsafe_unretained修饰?

  2. 为什么AspectIdentifier中的属性object要用weak修饰?

原因 :

  1. ASPECTS_ARE_BEING_CALLEDAspectInfo这个可以归在一起,因为ASPECTS_ARE_BEING_CALLEDself需要用__unsafe_unretained修饰的原因就是AspectInfo
    那为什么AspectInfo的属性instance要用__unsafe_unretained修饰?
    原因一定是为了避免循环引用,所以我就直接写循环引用链就好了。

循环引用链1 : self-->AspectsContainer-->AspectIdentifier-->block-->AspectInfo-->self
所以最后的AspectInfoself的引用要用__unsafe_unretained进行弱引用。

  1. AspectIdentifier的属性objectweak修饰是为了避免下面的循环引用链形成 :

循环引用链2 : self --> AspectsContainer-->AspectIdentifier-->self

至此,AOP之Aspects库系列全部结束。

你可能感兴趣的:(第三十九节—AOP之Aspects库(三))