iOS 中消息发送与转发

objc_msgSend

调用一个方法的时候,runtime 层会将这个调用翻译成

objc_msgSend(id self, SEL op, ...)

比如,一条语句 [receiver message]; 会由编译器转化为以下的纯 C 调用:

objc_msgSend(receiver, @selector(message));

objc_msgSend 的伪代码为:

id objc_msgSend(id self, SEL _cmd, ...) {
    Class c = object_getClass(self);
    IMP imp = cache_lookup(c, _cmd);
    if(!imp)
        imp = class_getMethodImplementation(c, _cmd);
    return imp(self, _cmd, ...);
}

当向一般对象发送消息时,调用 objc_msgSend;当向 super 发送消息时,调用的是 objc_msgSendSuper; 如果返回值是一个结构体,则会调用 objc_msgSend_stretobjc_msgSendSuper_stret

消息分发

objc_msgSend 的消息分发分为以下几个步骤:

  • 判断 receiver 是否为 nil,也就是 objc_msgSend 的第一个参数 self,也就是要调用的那个方法所属对象
  • 从缓存里寻找,找到了则分发,否则
  • 利用 objc-class.mm_class_lookupMethodAndLoadCache3 方法去寻找 selector
    • 如果支持 GC,忽略掉非 GC 环境的方法(retain 等)
    • 从本 classmethod list 寻找 selector,如果找到,填充到缓存中,并返回 selector,否则
    • 寻找父类的 method list,并依次往上寻找,直到找到 selector,填充到缓存中,并返回 selector,否则
    • 调用 _class_resolveMethod,如果可以动态 resolve 一个 selector,不缓存,方法返回,否则
    • 转发这个 selector,否则
  • 报错,抛出异常

类的定义里就有 cache 字段,类的所有缓存都存在 metaclass 上,所以每个类都只有一份方法缓存,而不是每一个类的 object 都保存一份。
即便是从父类取到的方法,也会存在类本身的方法缓存里。而当用一个父类对象去调用那个方法的时候,也会在父类的 metaclass 里缓存一份。
在调用 _class_lookupMethodAndLoadCache3 之前,已经是从缓存无法找到 selector 了,所以这个方法避免了再去扫描缓存查找方法的过程,而是直接从方法列表找起。

动态添加方法

允许用户在此时为该 Class 动态添加实现。

void _class_resolveMethod(Class cls, SEL sel, id inst)

函数原码为:

void _class_resolveMethod(Class cls, SEL sel, id inst)
{
    if (! cls->isMetaClass()) {
        // try [cls resolveInstanceMethod:sel]
        _class_resolveInstanceMethod(cls, sel, inst);
    } 
    else {
        // try [nonMetaClass resolveClassMethod:sel]
        // and [cls resolveInstanceMethod:sel]
        _class_resolveClassMethod(cls, sel, inst);
        if (!lookUpImpOrNil(cls, sel, inst, 
                            NO/*initialize*/, YES/*cache*/, NO/*resolver*/)) 
        {
            _class_resolveInstanceMethod(cls, sel, inst);
        }
    }
}

此方法是动态方法解析的入口,会间接地发送 +resolveInstanceMethod+resolveClassMethod 消息。通过对 isa 指向的判断,从而分辨出如果是对象方法,则进入 +resolveInstanceMethod 方法,如果是类方法,则进入 +resolveClassMethod 方法。

动态添加实例方法:

void dynamicInstanceMethod(id self, SEL _cmd) {
    NSLog(@" >> dynamic Instance Method");
}
@implementation TestMessage
+ (BOOL)resolveInstanceMethod:(SEL)sel {
    if (sel == @selector(noneMethod))
    {
        class_addMethod([self class], sel, (IMP)dynamicInstanceMethod, "v@:");
        return YES;
    }
    return [super resolveInstanceMethod:sel];
}
@end

动态添加类方法:

void dynamicClassMethod(id self, SEL _cmd){
    NSLog(@" >> dynamic Class Method");
}
@implementation TestMessage
+ (BOOL)resolveClassMethod:(SEL)sel{
    if (sel == @selector(noneMethod)) {
        Class metaClass = objc_getMetaClass([NSStringFromClass(self) UTF8String]);
        class_addMethod(metaClass, @selector(noneMethod), (IMP)dynamicClassMethod, "v@:");
        return YES;
    }
    else{
        return [super resolveClassMethod:sel];
    }
}
@end

注意类方法需要添加到 Meta Class 中。关于 classmeta class 的关系可以参考下图:

消息转发机制(message forwarding)

当对象接受到无法解读的消息后,就会启动消息转发机制。当向一个对象发送一条消息,但它并没有实现的时候,_objc_msgForward 会尝试做消息转发。

消息转发分为两个阶段

  • 先征询接收者所属都类,看其是否能动态添加方法
  • 完整的消息转发机制
    • 查看是否有备援接收者 replacement receiver,否则
    • 把消息的全部细节封装到 NSInvocation 对象中,再给接收者最后一次机会

备援接收者

尝试找到一个能响应该消息的对象。

- (id)forwardingTargetForSelector:(SEL)selector;

完整的消息转发

先调用 methodSignatureForSelector: 方法,尝试获得一个方法签名。如果获取不到,则直接调用 doesNotRecognizeSelector 抛出异常。

- (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector

然后创建 NSInvocation 对象,调用

- (void)forwardInvocation:(NSInvocation *)invocation;

代码:

- (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector {
    if (aSelector == @selector(noneMethod)) {
        return [NSMethodSignature signatureWithObjCTypes:"v@:"];
    }
    return [super methodSignatureForSelector:aSelector];
}

- (void)forwardInvocation:(NSInvocation *)anInvocation {
    SEL selector = [anInvocation selector];
    MyClass *myclass = [MyClass new];
    if ([myclass respondsToSelector:selector]) {
        [anInvocation invokeWithTarget:myclass];
    }
}

消息转发总结

非常值得读的文章

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

推荐阅读更多精彩内容