消息转发机制与Aspects源码解析

前言

最近在搞重构相关的事情,遇到了不少这样的场景:

进入一个界面,在viewWillAppear:的时候做相应判断,如果满足条件则执行对应代码。

这类业务有一个特点,业务内容是对应整个App的,与对应的ViewController毛关系都没有,但是却不得不耦合到(即使是调用代码可以精简到一行)ViewController中。

我们都知道,这种类似的业务用AOP(面向切片编程)来做十分适合,所谓面向切片编程就是在不修改原方法的前提下,动态的插入自己的想要的执行代码,由于Objective C是动态语言,可以很容易的利用method swizzling来实现AOP。

在正文之前特别感谢微信阅读团队的这篇博客:

面向切面编程之 Aspects 源码解析及应用

这篇博客原理上讲解的比较清楚,但是细节上并没有讲的很详细,所以也就有了本文。

Objective C方法调用过程

这个其实我之前在这篇博客里讲过:

iOS Runtime详解(消息机制,类元对象,缓存机制,消息转发)

这里,把核心的内容再一次列出来。

如下Objective C代码

- (NSInteger )myTestFunction:(NSInteger)input{

    return input + 1;

}

- (void)mySpecialFunction{

   NSInteger result =  [self myTestFunction:10];

}

用clang来重写为C++,

clang -rewrite-objc  MyClass.m

然后,我们通过搜索mySpecialFunction方法名字,来找到转换后的代码,经过简单整理如下

static NSInteger _I_MyClass_myTestFunction_(MyClass * self, SEL _cmd, NSInteger input) {

    return input + 1;

}

static void _I_MyClass_mySpecialFunction(MyClass * self, SEL _cmd) {

   NSInteger result = objc_msgSend(self, sel_registerName("myTestFunction:"),10);

}

我们看到,方法体进行了如下转换

//OC

- (NSInteger )myTestFunction:(NSInteger)input{

    return input + 1;

}

//C++

static NSInteger _I_MyClass_myTestFunction_(MyClass * self, SEL _cmd, NSInteger input) {

    return input + 1;

}

方法调用进行了如下转换

//OC

NSInteger result =  [self myTestFunction:10];

//C++

NSInteger result = objc_msgSend(self, sel_registerName("myTestFunction:"),10);

不难看出,方法的调用并不是直接转换成了对应的C/C++方法调用,而是调用了objc_msgSend通过SEL(就是一个字符串)在运行时动态找到这个的执行体_I_MyClass_myTestFunction_。

那么,在运行时如何找到这个方法的执行体呢? 这里省略一些细节,对细节感兴趣的同学可以看我上文写的那篇文章。一个实例方法的流程如下:


对象实例收到消息(SEL+参数)

根据存储在对象实例中的ISA到类对象,类对象依次查找Class Cache(方法表缓存)和dispatch table找到对应的Method,如果找到Method,执行对应Method的IMP(方法体),并且返回结果

如果找不到Method,则根据类对象中的super_class指针找到父类的Class对象。一直找到NSObject的类对象

如果NSObject也无法找到这个SEL,则进入消息转发机制

如果消息转发机制无法处理,则抛出异常: doesNotRecognizeSelector

Method Swizzling

通过上文我们知道,一个方法的调用实际上就是SEL(方法名)通过Runtime找到IMP(方法执行体)


既然是通过Runtime动态找到的,那么我们就可以利用Runtime的API,讲SEL_1来指向IMP_2,接着我们再在在IMP_2的方法体中执行IMP_1,就实现了动态插入代码。


消息转发机制

在Objective C的方法调用过程中,我们提到了当无法响应一个selector时,在抛出异常之前会先进入消息转发机制。这里来详细讲解消息转发的过程:

关于消息转发,官方文档在这里:Message Forwarding

在触发消息转发机制即forwardInvocation:之前,Runtime提供了两步来进行轻量级的动态处理这个selector.

resolveInstanceMethod:

Dynamically provides an implementation for a given selector for an instance method.

这个方法提供了一个机会:为当前类无法识别的SEL动态增加IMP

比如:最常见的可以通过class_addMethod

void dynamicMethodIMP(id self, SEL _cmd){/*...implementation...*/}

+ (BOOL) resolveInstanceMethod:(SEL)aSEL

{

    if (aSEL == @selector(resolveThisMethodDynamically))

    {

          class_addMethod([self class], aSEL, (IMP) dynamicMethodIMP, "v@:");

          return YES;

    }

    return [super resolveInstanceMethod:aSel];

}

Tips,这里的"v@:"表示方法参数编码,v表示Void,@表示OC对象,:表示SEL类型。关于方法参数编码,更详细的内容参见文档

如果resolveInstanceMethod返回NO,则表示无法在这一步动态的添加方法,则进入下一步:

forwardingTargetForSelector:

Returns the object to which unrecognized messages should first be directed.

这个方法提供了一个机会:简单的把这个SEL交给另外一个对象来执行。

比如:

-(id)forwardingTargetForSelector:(SEL)aSelector{

    if (aSelector == @selector(dynamicSelector) && [self.myObj respondsToSelector:@selector(dynamicSelector)]) {

        return self.myObj;

    }else{

        return [super forwardingTargetForSelector:aSelector];

    }

}

如果上述两步都无法完成这个SEL的处理,则进入消息转发机制,消息转发机制有两个比较重要的方法:

forwardInvocation: 具体的NSInvocaion

methodSignatureForSelector: 返回SEL的方法签名

这里不得不提一下两个类:

NSMethodSignature 用来表示方法的参数签名信息:返回值,参数数量和类型

NSInvocaion SEL + 执行SEL的Target + 参数值

通常,拿到NSInvocaion对象后,我们可选择的进行如下操作

修改执行的SEL

修改执行的Target

修改传入的参数

然后调用:[invocation invoke],来执行这个消息。

_objc_msgForward

我们知道,正常情况下SEL背后会对一个IMP,在OC中有一个特殊的IMP就是:_objc_msgForward。当执行_objc_msgForward时,会直接触发消息转发机制,即forwardInvocation:。

Aspect的基本原理

使用Aspect,可以在一个OC方法执行前/后插入代码,也可以替换这个OC方法的实现。

这里,我们以在ViewControler的viewWillAppear:方法之后插入一段代码为例,来讲解hook前后的变化,

在没有hook之前,ViewController的SEL与IMP关系如下


调用以下aspect来hook viewWillAppear:后:

 [ViewController aspect_hookSelector:@selector(viewWillAppear:)

                            withOptions:AspectPositionAfter

                             usingBlock:^{

                                 NSLog(@"Insert some code after ViewWillAppear");

                             } error:&error];


最初的viewWillAppear: 指向了_objc_msgForward

增加了aspects_viewWillAppear:,指向最初的viewWillAppear:的IMP

最初的forwardInvocation:指向了Aspect提供的一个C方法__ASPECTS_ARE_BEING_CALLED__

动态增加了__aspects_forwardInvocation:,指向最初的forwardInvocation:的IMP

然后,我们再来看看hook后,一个viewWillAppear:的实际调用顺序:

object收到selector(viewWillAppear:)的消息

找到对应的IMP:_objc_msgForward,执行后触发消息转发机制。

object收到forwardInvocation:消息

找到对应的IMP:__ASPECTS_ARE_BEING_CALLED__,执行IMP

向object对象发送aspects_viewWillAppear:,执行最初的viewWillAppear方法的IMP

            执行插入的block代码

            如果ViewController无法响应aspects_viewWillAppear,则向object对象发送__aspects_forwardInvocation:来执行最初的forwardInvocation IMP

            所以,Aspects是采用了集中式的hook方式,所有的调用最后走的都是一个C函数__ASPECTS_ARE_BEING_CALLED__。

核心类/数据结构

AspectIdentifier - 代表一个Aspect的具体信息:包括被Hook的对象,SEL,插入的block等具体信息。

@interface AspectIdentifier : NSObject

@property (nonatomic, assign) SEL selector;

@property (nonatomic, strong) id block;

@property (nonatomic, strong) NSMethodSignature *blockSignature;

@property (nonatomic, weak) id object;

@property (nonatomic, assign) AspectOptions options;

@end

AspectTracker - 跟踪一个类的继承链中的hook状态:包括被hook的类,哪些SEL被hook了。

@interface AspectTracker : NSObject

@property (nonatomic, strong) Class trackedClass;

@property (nonatomic, readonly) NSString *trackedClassName;

@property (nonatomic, strong) NSMutableSet *selectorNames;

@property (nonatomic, strong) NSMutableDictionary *selectorNamesToSubclassTrackers;

@end

AspectContainer - AspectIdentifier的容器:以SEL合成key,然后作为关联对象存储到对应的类/对象里。包括beforeAspects,insteadAspects,afterAspects

@interface AspectsContainer : NSObject

@property (atomic, copy) NSArray *beforeAspects;

@property (atomic, copy) NSArray *insteadAspects;

@property (atomic, copy) NSArray *afterAspects;

@end

AspectInfo - NSInvocation的容器,表示一个执行的Command。

@interface AspectInfo : NSObject

@property (nonatomic, unsafe_unretained, readonly) id instance;

@property (nonatomic, strong, readonly) NSArray *arguments;

@property (nonatomic, strong, readonly) NSInvocation *originalInvocation;

@end

hook过程

同样,我们以一个实例方法为例,讲解在这个方法调用后发生了什么

 [ViewController aspect_hookSelector:@selector(viewWillAppear:)

                            withOptions:AspectPositionAfter

                             usingBlock:^{

                                 NSLog(@"Insert some code after ViewWillAppear");

                             } error:&error];

1.对Class和MetaClass进行进行合法性检查,判断能否hook,规则如下

            retain,release,autorelease,forwoardInvocation:不能被hook

            dealloc只能在方法前hook

            类的继承关系中,同一个方法只能被hook一次

2.创建AspectsContainer对象,以aspects_ + SEL为key,作为关联对象依附到被hook 的对象上

objc_setAssociatedObject(self, aliasSelector, aspectContainer, OBJC_ASSOCIATION_RETAIN);

3.创建AspectIdentifier对象,并且添加到AspectsContainer对象里存储起来。这个过程分为两步 

            生成block的方法签名NSMethodSignature

            对比block的方法签名和待hook的方法签名是否兼容(参数个数,按照顺序的类型)

4.根据hook实例对象/类对象/类元对象的方法做不同处理。其中,对于上文以类方法来hook的时候,分为两步

hook类对象的forwoardInvocation:方法,指向一个静态的C方法,并且创建一个aspects_ forwoardInvocation:动态添加到之前的类中

IMP originalImplementation = class_replaceMethod(klass, @selector(forwardInvocation:), (IMP)__ASPECTS_ARE_BEING_CALLED__, "v@:@");

if (originalImplementation) {

    class_addMethod(klass, NSSelectorFromString(AspectsForwardInvocationSelectorName), originalImplementation, "v@:@");

}

hook类对象的viewWillAppear:方法让其指向_objc_msgForward,动态添加aspects_viewWillAppear:指向最初的viewWillAppear:实现

Hook实例的方法

Aspects支持只hook一个对象的实例方法

只不过在第4步略有出入,当hook一个对象的实例方法的时候:

新建一个子类,_Aspects_ViewController,并且按照上述的方式hook forwoardInvocation:

hook _Aspects_ViewController的class方法,让其返回ViewController

hook 子类的类元对象,让其返回ViewController

调用objc_setClass来修改ViewController的类为_Aspects_ViewController

这样做,就可以通过object_getClass(self)获得类名,然后看看是否有前缀类名来判断是否被hook过了

其他

object_getClass/self.class的区别

object_getClass获得的是isa的指向

self.class则不一样,当self是实例对象的时候,返回的是类对象,否则则返回自身。

比如:

TestClass * testObj = [[TestClass alloc] init];

//Same

logAddress([testObj class]);

logAddress([TestClass class]);

//Not same

logAddress(object_getClass(testObj));

logAddress(object_getClass([TestClass class]));

Log

2017-05-22 22:41:48.216 OCTest[899:25934] 0x107d10930

2017-05-22 22:41:48.216 OCTest[899:25934] 0x107d10930

2017-05-22 22:41:48.216 OCTest[899:25934] 0x107d10930

2017-05-22 22:41:49.061 OCTest[899:25934] 0x107d10908

Block签名

block因为背后其实是一个C结构体,结构体中存储着着一个函数指针来指向实际的方法体

Block的内存布局如下

typedef NS_OPTIONS(int, AspectBlockFlags) {

    AspectBlockFlagsHasCopyDisposeHelpers = (1 << 25),

    AspectBlockFlagsHasSignature          = (1 << 30)

};

typedef struct _AspectBlock {

    __unused Class isa;

    AspectBlockFlags flags;

    __unused int reserved;

    void (__unused *invoke)(struct _AspectBlock *block, ...);

    struct {

        unsigned long int reserved;

        unsigned long int size;

        // requires AspectBlockFlagsHasCopyDisposeHelpers

        void (*copy)(void *dst, const void *src);

        void (*dispose)(const void *);

        // requires AspectBlockFlagsHasSignature

        const char *signature;

        const char *layout;

    } *descriptor;

    // imported variables

} *AspectBlockRef;

对应生成NSMethodSignature的方法:

static NSMethodSignature *aspect_blockMethodSignature(id block, NSError **error) {

    AspectBlockRef layout = (__bridge void *)block;

    if (!(layout->flags & AspectBlockFlagsHasSignature)) {

        NSString *description = [NSString stringWithFormat:@"The block %@ doesn't contain a type signature.", block];

        AspectError(AspectErrorMissingBlockSignature, description);

        return nil;

    }

    void *desc = layout->descriptor;

    desc += 2 * sizeof(unsigned long int);

    if (layout->flags & AspectBlockFlagsHasCopyDisposeHelpers) {

        desc += 2 * sizeof(void *);

    }

    if (!desc) {

        NSString *description = [NSString stringWithFormat:@"The block %@ doesn't has a type signature.", block];

        AspectError(AspectErrorMissingBlockSignature, description);

        return nil;

    }

    const char *signature = (*(const char **)desc);

    return [NSMethodSignature signatureWithObjCTypes:signature];

}

关于Block的更多讲解,参见我的前一篇博客

Objective C block背后的黑魔法

效率

消息转发机制相对于正常的方法调用来说是比较昂贵的,所以一定不要用消息转发机制来处理那些一秒钟成百上千次的调用。

总结

Objective C的消息转发机制是一个非常灵活的机制,用好它会让你实现很多黑科技,也能够让你的架构更加灵活

原文:

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

推荐阅读更多精彩内容