Aspects关联&调用流程浅析

引子: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个区块调整难度……哈哈,扯远了,但愿你可以有收获,结束!

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 206,968评论 6 482
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 88,601评论 2 382
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 153,220评论 0 344
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 55,416评论 1 279
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 64,425评论 5 374
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,144评论 1 285
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,432评论 3 401
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,088评论 0 261
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 43,586评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,028评论 2 325
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,137评论 1 334
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,783评论 4 324
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,343评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,333评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,559评论 1 262
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,595评论 2 355
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,901评论 2 345

推荐阅读更多精彩内容