美文网首页程序员技术栈程序员
Aspects关联&调用流程浅析

Aspects关联&调用流程浅析

作者: Chriszzzz | 来源:发表于2018-03-14 11:48 被阅读22次

    引子:Aspects简述

    Aspects是iOS支持AOP(Aspect Oriented Programming,即切面编程)的一个支持包。说的浅显一些:如果你想让任何VC的viewDidAppear方法调用前都打印一句“[Class] been called!” 的话,或许Aspects可以成为你的选择。

    但我们今天不讲太多Aspects的使用(因为它提供的对外接口很简单,有需求一看就很容易明白),而是讲讲Aspects实现中的一些关键流程思路。所以,该篇文章适合:
    1)已经应用过Aspects并对其实现原理颇有兴趣的同学;
    2)对iOS runtime,消息转发机制的应用场景有初步了解的同学;
    3)技术发烧粉当然也欢迎!

    让我们开门见山,直入主题

    目录

    1 Aspects关键流程
    1.1 主流程-方法改写
    1.2 主流程-方法调用
    1.3 应用的底层技术参考

    2 Aspects关键流程实现
    2.1 对某个类的所有实例进行hook
    2.1.1 关联流程说明
    2.1.2 方法调用流程说明
    2.2 对某个类实例进行hook

    3 花絮(讨论)
    3.1 Aspects无法对类方法进行关联?
    3.2 关于消息转发的一点讨论
    3.2.1 入坑
    3.2.2 为什么要消息转发

    1、Aspects的关键流程

    1.1 主流程-方法改写

    Aspects思路简述:针对 目标类/目标实例 对象的目标函数,基于消息转发函数(forwardInvocation:)的重写,在目标方法前/后添加代码段(基于block)或直接替换目标函数实现

    对此,Aspects提供了如下仅有的两个简单到不能再简单的对外支持接口。

    /* Aspects的类对象hook接口 */
    + (id<AspectToken>)aspect_hookSelector:(SEL)selector
                          withOptions:(AspectOptions)options
                           usingBlock:(id)block
                                error:(NSError **)error;
    
    /* Aspects的实例对象hook接口 */
    - (id<AspectToken>)aspect_hookSelector:(SEL)selector
                          withOptions:(AspectOptions)options
                           usingBlock:(id)block
                                error:(NSError **)error;
    

    selector: 要hook的函数。
    注:两个方法都只能hook“-“函数(即实例方法,对”+“方法的hook无效)
    options: 设定你要添加的代码段是期望加到目标函数之前、之后,或直接替换目标函数。
    block: 要添加的或替换原函数的代码段
    error: 接受错误消息的指针

    1.2 主流程-方法调用

    Aspects的思路:通过将目标函数的方法实现改写为转发函数的实现(_objc_msgForward),从而使对目标方法的调用可以走入主流程-方法改写中被改写的转发函数实现中,从而相当于调用了改写后的方法。

    1.3 应用的底层技术参考

    • 消息转发(Message Forward)
    • 运行时(Runtime)

    2 Aspect的关键流程实现

    Aspects的具体实现中,针对类对象关联(即对某个类的所有生成实例进行关联)和实例关联略有差异。

    2.1 对某个类的所有实例进行hook

    举例:对UIViewControllerviewAppear:方法进行关联

    image.png

    2.1.1 关联流程说明

    图1.1)允许关联检查:除了基础的关联允许检查(比如某些特定方法如retain拒绝进行关联)外,也构造了一个防止重复关联的数据结构(一个全局的字典,如下图)。

    image.png

    图1.2)填写hook信息:针对关联的类对象,会动态关联一个AspectContainer结构,来保存进行关联的代码段(block)的信息

    image.png

    图1.3)重写转发方法:将forwardInvocation:的方法实现替换为Aspects的自定义实现。

    image.png

    图1.4)添加别名方法:为目标方法添加别名方法,并将别名方法的实现指向原始方法的实现(比如aspects__viewWillAppear:的实现实际上是viewWillAppear:的实现)

    image.png

    图1.5)原始方法指向转发方法:替换原始关联的方法实现为转发(_objc_msgForward)

    image.png

    步骤1.3~1.5 Aspects关键代码对应关系

    static void aspect_prepareClassAndHookSelector(NSObject *self, SEL selector, NSError **error) {
        NSCParameterAssert(selector);
    
        /* 【Chris】aspect_hookClass方法完成了步骤1.3:改写forwardInvocation:的实现 */
        Class klass = aspect_hookClass(self, error);
    
        Method targetMethod = class_getInstanceMethod(klass, selector);
        IMP targetMethodIMP = method_getImplementation(targetMethod);
        if (!aspect_isMsgForwardIMP(targetMethodIMP)) {
            const char *typeEncoding = method_getTypeEncoding(targetMethod);
    
            /* 【Chris】下面几行代码完成了步骤1.4:添加别名方法,并将别名方法的实现指向原方法的实现 */
            SEL aliasSelector = aspect_aliasForSelector(selector);
            if (![klass instancesRespondToSelector:aliasSelector]) {
                __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);
            }
    
            /* 【Chris】下面一行代码完成了步骤1.5:将被hook方法的实现改为消息转发 */
            class_replaceMethod(klass, selector, aspect_getMsgForwardIMP(self, selector), typeEncoding);
            AspectLog(@"Aspects: Installed hook for -[%@ %@].", klass, NSStringFromSelector(selector));
        }
    }
    
    

    2.1.2 方法调用流程

    图2.1)调用消息转发方法:当被hook的方法(如viewWillAppear:)被某个类实例调用时,实际上会进行消息转发,从而触发对应实例forwardInvocation:方法

    图2.2)执行改造的代码块:进而会调用到Aspects自定义的forwardInvocation:方法实现.(如下面截取的关键代码)

    /* __ASPECTS_ARE_BEING_CALLED__方法部分代码截取 */
    
        /* 【Chris】在原方法实现前添加的代码段(block)调用 */
        aspect_invoke(classContainer.beforeAspects, info);
        aspect_invoke(objectContainer.beforeAspects, info);
    
        /* 【Chris】调用原始方法或者替换的代码段(block)调用 */
        BOOL respondsToAlias = YES;
        if (objectContainer.insteadAspects.count || classContainer.insteadAspects.count) {
            aspect_invoke(classContainer.insteadAspects, info);
            aspect_invoke(objectContainer.insteadAspects, info);
        }else {
            Class klass = object_getClass(invocation.target);
            do {
                if ((respondsToAlias = [klass instancesRespondToSelector:aliasSelector])) {
                    [invocation invoke];
                    break;
                }
            }while (!respondsToAlias && (klass = class_getSuperclass(klass)));
        }
    
        /* 【Chris】在原方法实现后添加的代码段(block)调用 */
        aspect_invoke(classContainer.afterAspects, info);
        aspect_invoke(objectContainer.afterAspects, info);
    

    2.2 对某个类实例进行hook

    举例:Aspects对UIViewController类的实例tmpObj进行hook

    image.png

    针对实例对象的关联流程、调用流程与类对象的大同小异,所以我们只针对其中有差异的部分进行简单的说明

    图1.1)简化的hook允许判定:对实例的关联影响范围很小,所以是否允许hook只进行了诸如是否特殊方法(如retain)的检查,而不构造额外的数据结构(如swizzedClassesDict字典)进行辅助判定。

    图1.2)构建Aspect子类:针对实例的关联,Aspects会为实例对应的类动态创建一个子类(比如UIViewController的就叫 UIViewController_Aspects_)

    图1.3)重置实例类类型: 将实例的类型设定为动态添加的子类类型(即:UIViewController_Aspects_)

    步骤1.2 ~1.3 Aspects关键代码的对应关系

        /* 【Chris】完成步骤1.2:动态为hook的实例的类添加子类 */
        const char *subclassName = [className stringByAppendingString:AspectsSubclassSuffix].UTF8String;
        Class subclass = objc_getClass(subclassName);
        if (subclass == nil) {
            subclass = objc_allocateClassPair(baseClass, subclassName, 0);
            if (subclass == nil) {
                NSString *errrorDesc = [NSString stringWithFormat:@"objc_allocateClassPair failed to allocate class %s.", subclassName];
                AspectError(AspectErrorFailedToAllocateClassPair, errrorDesc);
                return nil;
            }
    
            aspect_swizzleForwardInvocation(subclass);
            aspect_hookedGetClass(subclass, statedClass);
            aspect_hookedGetClass(object_getClass(subclass), statedClass);
            objc_registerClassPair(subclass);
        }
    
        /* 【Chris】完成步骤1.3:将关联的实例的类型设定为新添加的子类 */
        object_setClass(self, subclass);
    

    图2.1)调用消息转发方法: 这边调用消息转发的类是Aspects动态创建的子类(即UIViewController_Aspects_类)

    3 花絮(讨论)

    3.1 Aspect无法对类方法进行关联?

    在文章的1.1节,针对Aspects接口参数selector进行解读的时候我添加了一个注:说Aspects只能hook“-”方法,无法hook“+”方法。类似于下面的一张表。


    image.png

    “罪魁祸首”就是下面的代码了

    static BOOL aspect_isCompatibleBlockSignature(NSMethodSignature *blockSignature, id object, SEL selector, NSError **error) {
    
        /* 【Chris】其他代码略 */
        ...... 
    
        BOOL signaturesMatch = YES;
    
        / * 【Chris】hook一个类方法,instanceMethodSignatureForSelector:定会返回nil */
        NSMethodSignature *methodSignature = [[object class] instanceMethodSignatureForSelector:selector];
    
        /* 【Chris】blockSignature.numberOfArguments = 2 > 0,触发了match=NO,在上层方法中结束hook流程 */
        if (blockSignature.numberOfArguments > methodSignature.numberOfArguments) {
            signaturesMatch = NO;
        }
    
        /* 【Chris】其他代码略 */
        ...... 
    }
    

    那么我们怎么对类方法(“+”方法)进行hook呢?
    很简单,不用Aspects直接操作运行时呗!
    (提供一个交换类方法的源代码参考,使用上不再赘述,可自行搜索methodSwizzling)

    /* 交换某个类的类方法 */
    + (void)swizzlingClassMethodWithOriginalSel:(SEL)originalSel swizzledSel:(SEL)swizzledSel {
        
        Class class = [self class];
        
        SEL originalSelector = originalSel;
        SEL swizzledSelector = swizzledSel;
        
        Method originalMethod = class_getClassMethod(class, originalSelector);
        Method swizzledMethod = class_getClassMethod(class, swizzledSelector);
        
        method_exchangeImplementations(originalMethod, swizzledMethod);
    }
    

    3.2 关于消息转发的一点讨论

    3.2.1 入坑

    Step1:
    如果开始学习iOS的消息转发,很可能的一个出发点就是forwardInvocation:函数。

    Step2:
    接下来你会尝试重写forwardInvocation:函数,然后发现重写的函数根本不会被调用

    Step3:
    百度后的你愕然得到提示!必须同时重写methodSignatureForSelector:函数,并反回一个signature才可以使forwardInvocation:函数被调用!你兴奋的贴上了还看不太懂的网上代码demo,哇!forwardInvocation:果然被调用了!

    Step4:
    兴致勃勃的带着睥睨天下的姿态研究下Aspects源码,却发现它只重写了forwardInvocation:函数但根本没有重写methodSignatureForSelector:函数!狗日的!那重写的forwardInvocation:是怎么被调用到的!?

    Step5:
    知道你看到了这篇文章,发现原来forwardInvocation:函数的调用条件本质上与methodSignatureForSelector:无关,只要对应的对象接收到消息转发函数(IMP为_objc_msgForward)的调用就OK了。

    3.2.2 为什么要消息转发

    场景布置
    1)对一个类定义一个方法如:- (void)testAAA; 但不对方法进行实现
    2)提供一个testAAA被调用的操作入口。比如让某个按钮的点击调用该方法。

    实验开始
    1)点击那个可以调用testAAA方法的按钮。
    2)系统会去找testAAA的方法实现,发现——木有!
    3)系统接下来会看看你有没有尝试修复这个方法(在此不赘述),如果你没有修复,那么系统就认为你要进行消息转发了!
    4)系统会调用methodSignatureForSelector:来询问你这个转发的方法的实现描述,你要给它!比如“v@:”代表方法实现的返回值为void(v),第一个参数为方法实例对象(@),第二个参数为方法名(:),看下面的定义可能会稍微清晰一些(细节一样暂不赘述)

    static void __chrisTest(id self, SEL _cmd);
    

    5)拿到方法实现描述后,系统会调用forwardInvocation:来给你操作消息转发的机会。你可以将消息转发给该实例对象的其他函数,也可以将消息转发给其他类实例对象的某个函数。(当然,函数的描述,即返回值,参数等要一致)

    /* 选择目标方法 */
    anInvocation.selector = @selector(anSel);
    /* 选择目标对象并执行方法调用 */
    [anInvocation invokeWithTarget:anObj];
    

    结语

    Aspects能挖掘的点还有很多,比如【根据具体需求局部改写Aspects的实现】【Aspects调用的runtime接口功能模拟】【Aspects数据结构分析】等等等等。或许不仅仅是Aspects,任何一个被大众广泛接受的设计中的每一个确定参数的设定,都值得问一句Why!就如比特币的总数2100万个,每2016个区块调整难度……哈哈,扯远了,但愿你可以有收获,结束!

    相关文章

      网友评论

        本文标题:Aspects关联&调用流程浅析

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