美文网首页
第三十八节—AOP之Aspects库(二)

第三十八节—AOP之Aspects库(二)

作者: L_Ares | 来源:发表于2021-01-21 16:33 被阅读0次

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

上一节,继续探索。本节将从AspectsContainerAspectIdentifier来入手,探索Aspects库到底是如何完成了hook。

图1.0.0

一、先记录几个问题

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

二、初始化容器AspectsContainer

上一节探索到 :

AspectsContainer *aspectContainer = aspect_getContainerForObject(self, selector);

这一行代码的上面,探索完了Aspects库对被hook的类和被hook的方法的合法性校验。

这一节从这一行代码开始,先探索aspect_getContainerForObjectAspectsContainer

这里主要是对AspectsContainer做操作。

  • 获取被hook的对象的AspectsContainer容器。
  • 主要思想就是通过关联对象方式,将被hook的方法selector的名字重命名加上前缀变成aspects__selector,然后以它为键,在self的关联表中查询对应的容器。
  • AspectsContainer只有3个属性,都是数组,分别存储beforeAspectsinsteadAspectsafterAspects

1. aspect_getContainerForObject

释义 : 该方法是获取AspectsContainer容器。容器对象存储的内容是所有被hook的对象/类。方法的返回值是一个AspectsContainer对象。

方法的功能和注释在下图2.1.0中。

图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初始化方法

图3.2.0

很常规的构造函数,唯二的特点是对参数中block块的签名的获取和校验。

3. aspect_blockMethodSignature

该方法是获取block的签名信息。

参数

  1. block : 要获取签名信息的block。
  2. error : 获取签名信息出现的错误信息。

实现

图3.3.0

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

图3.3.1

实现的思路非常的简单,在block的章节中介绍过,可以进我的主页看。
实现思路大体 :

  1. 通过位移block的指针,从block的首地址,位移到block结构体的desc3上面。
  2. desc3中的signature元素存储了block块type encoding字符。
  3. 利用NSMethodSignature的方法,将type encoding字符转成NSMethodSignature对象。这个对象也就是所谓的block块的签名

4. aspect_isCompatibleBlockSignature

该方法是对上面3.aspect_blockMethodSignature获得的block块的签名的兼容性验证。

参数

  1. blockSignature : 要验证兼容性的block的签名信息。
  2. object : 被hook的类。
  3. selector : 被hook的方法。
  4. error : 错误信息。

实现

图3.4.0
  1. Aspects库在公开API的注释中说不允许hook静态方法。因为这里调用的是instanceMethodSignatureForSelector
  2. block块type encoding格式在上一节说过 : 返回值的type encoding + block的type encoding : @? + 参数的type encoding
    • 第一个位置 : 返回值的type encoding
    • 第二个位置 : block的type encoding,也就是@?
  3. 判断一下block的参数数量是否大于1。
    • 等于1说明 : block的签名中,参数只有block自己。
    • 大于1说明 : block不是一个空的block,也就是说不是^(void){}这种无参数block。而是有除block自身外,其他的参数。比如上一节的例子中的 : id<AspectInfo> aspectInfo(存在的话一定在block的参数的第2个位置上)、nameagesex
  4. block的参数数量绝对不可以多于被hook的方法的参数数量。

四、AspectIdentifier对象加入AspectsContainer容器

能进入到这里,表明被hook的类被hook的方法以及block参数中用来插入或者替换的函数都是符合Aspects库的规定的。

[aspectContainer addAspect:identifier withOptions:options];

参数

  1. aspect : 要加入容器的AspectsIdentifier对象。
  2. 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的selectorblock块内的函数进行操作了。

aspect_prepareClassAndHookSelector(self, selector, error);

1. 方法的整体逻辑

先来看这个函数的整体实现逻辑,然后挑出其中的封装逻辑再详细探索。

参数

self : 被hook的对象。
selector : 被hook的方法。
error : 可能发生的错误信息。

实现

图5.1.0

2. aspect_hookClass()

该方法是对被hook的对象的类做hook后的处理。

图5.2.0

我给这个方法分成了3个区域,方便理解,最主要的是看图5.2.0中的注释,下面依次说明3个区域的功能。

2.1 方法准备区

代码非常的简单,逻辑也简单 :

  1. 断言判断参数的合法性。
  2. statedClass :
    • 如果self是类对象 : statedClass就是类对象本身。
    • 如果self是实例对象 : statedClass就是实例对象所属的类。
  3. baseClass :
    • 如果self是类对象 : baseClass就是元类。
    • 如果self是实例对象 : baseClass就是实例对象所属的类。
  4. className :
    • 如果self是类对象 : className就是元类名称字符串。
    • 如果self是实例对象 : className就是实例对象所属的类的名称字符串。

2.2 特殊情况区

  1. if ([className hasSuffix:AspectsSubclassSuffix]) :

    如果selfisa指向的类已经有_Aspects_后缀。例如上节案例中的ViewController,原本它的isa指向的类元类ViewController,如果它的isa指向的类变成了ViewController_Aspects_,则表明它被hook过。

    可以直接返回baseClass

  2. else if (class_isMetaClass(baseClass)) :

    进入这里,则代表在调用Aspects的公开API时,调用的是+方法,也就是类方法,说明要hook的是整个类对象,而不是类的某个实例对象。

    返回aspect_swizzleClassInPlace((Class)self)

  3. 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;
}

有两点 :

  1. _aspect_modifySwizzledClasses
  2. 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 :

  1. 如果klass是类本身,也就是通过+方法进入到这里,那么整个类的forwardInvocation方法的IMP实现,都将被替换成__ASPECTS_ARE_BEING_CALLED__。(关于其他未被hook的方法如果没有实现,该怎么办,后面在该函数的解释里会说明)

  2. 如果klass是另一种情况,即被hook的对象isa指向的类不是一般情况下的自己的父类,而是出现类似KVO键值观察的情况。那么要替换的就是NSKVONotifying_父类名类的forwardInvocation方法的IMP实现。

问题 :

其实这里会出现一种情况 :

  1. 被hook的对象是一个KVO键值观察对象或者就是普通的实例对象,如果被hook的对象所属的类中有未被实现的方法,并且,没实现的方法还不是被hook的方法

  2. 那么首先就会进入动态决议,如果动态决议依旧没有实现,则会进入_objec_msgForward,进行消息转发。

  3. 如果我们不实现消息转发流程中的快速转发,则最后会调用一次NSKVONotifying_父类名类的forwardInvocation方法。

  4. 但是这个方法的IMP被交换了,那是不是没实现的方法也会进入__ASPECTS_ARE_BEING_CALLED__这个IMP实现呢?会怎么处理呢?在下面__ASPECTS_ARE_BEING_CALLED__方法解析的时候会有介绍。

2.3 默认情况区

这里就是aspect_hookClass()函数的默认实现,也就是针对 :

  1. 从未经过aspect_hook()处理的实例对象。
  2. 不是类对象。
  3. 不是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));
}

这个方法的目的 :

  1. 把新生成的中间类中间类的元类class方法,全都返回被hook的对象的类
  2. 这样做了以后,哪怕在下面的object_setClass中,将被hook的对象isa指向变成了中间类,也不会影响被hook的对象调用-(void)class方法返回的是其原来的类。

3. 被hook的方法的处理

上面的2.aspect_hookClass()完成了对被hook的对象的类的处理,这里则开始对被hook的方法进行处理。

这里截取的是上图5.1.0中,Class klass = aspect_hookClass(self,error)之后的代码,也就是对被hook的方法的处理。

图5.3.0

看图中画了红框的部分,从上到下,依此说一下设计实现的思路。

1. 首先,Aspects库的作者是利用class_getInstanceMethod来获取klass中的SELselector的方法的。

Method targetMethod = class_getInstanceMethod(klass, selector);

使用class_getInstanceMethod是因为klass已经被处理过,被处理的klass无非就3种情况,在上面的aspect_hookClass中已经介绍过,被hook的对象可能是 :

  1. 是普通的实例对象 :
    klass就是中间类
    中间类名称是组成是 : 被hook的对象的类的名字 + _Aspects_后缀。

  2. 是类对象 :
    klass的类不发生改变,依然是被hook的类对象

  3. 是被KVO键值观察的实例对象 :
    klassKVO的中间类。
    KVO的中间类名称组成是 : NSKVONotifying_ + 实例对象的类名

这3种klass都有着一个绝对的共同点 : 全部继承于被hook的对象的类。

所以,class_getInstanceMethod一定可以在klass的继承链上,找到selector的方法。然后得到targetMethod

2. 获得targetMethodIMP

没什么可说的,就是获得被hook的方法原始IMP,直接用objcAPI,获得MethodIMP

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库(三)进行探索。

相关文章

网友评论

      本文标题:第三十八节—AOP之Aspects库(二)

      本文链接:https://www.haomeiwen.com/subject/tmpwaktx.html