OC运行时机制Runtime(二):探索Runtime的消息转发机制

Runtime最全总结

本系列详细讲解Runtime知识点,由于运行时的内容较多,所以将内容拆分成以下几个方面,可以自行选择想要查看的部分

本文主要分析Runtime的消息机制,探究OC类和对象发送消息的原理。

理解 objc_msgSend 的作用

调用方法是面向对象语言常用的功能,用Objective-C的术语来说这叫“传递消息”,消息有“名称(name)”和“选择子(selector)”,而Runtime赋予了OC动态语言的特性,用一串伪代码简单介绍下静态语言和动态语言调用方法的区别。

if(bool){ function1(); }
else{ function2(); }
//函数地址硬编码在指令中,这是函数调用方式
void (*fnc)();
if(bool){ fnc = function1; }
else{ fnc = function2; }
fnc();
//需要的函数在运行时才能确定,动态绑定

并非编译时就会确定要调用的方法,而是交由运行时决定,这是消息机制的特点,OC基本调用方法为

id returnValue = [someObject messageName:parameter];

在这里someObject叫做接受者(receiver)messageName叫做选择子(selector),选择子和参数一同组成一条消息,编译器将其转为C语言函数调用,函数原型为void objc_msgSend(id self,SEL cmd, ...),这里函数的参数个数可变,那么上面的例子会变成以下这段代码

id returnValue = objc_msgSend(someObject, @selector(messageName:),parameter);

下面还以上一篇的Father和Son为例子,讲解一下消息传递的函数方法,在main.m中输入以下代码。

Son * son = [[Son alloc] init];

接下来用clang将其转为.cpp文件clang -rewrite-objc main.m,代码如下:

Son * son = ((Son *(*)(id, SEL))(void *)objc_msgSend)((id)((Son *(*)(id, SEL))(void *)objc_msgSend)((id)objc_getClass("Son"), sel_registerName("alloc")), sel_registerName("init"));

这里将类型等简化为以下代码

Son * son = (objc_msgSend)((objc_msgSend)(objc_getClass("Son"), sel_registerName("alloc")), sel_registerName("init"));

解释下这段代码,首先使用objc_getClass获取当前类对象,然后通过objc_msgSend,向这个类对象发送消息,消息名为alloc,完成实例对象分配内存,再次调用objc_msgSend向这个实例对象发送消息,消息名为init,表示实例对象的初始化。

根据上一篇文章 OC运行时机制Runtime(一):从isa指针开始初步结识Runtime 我们了解了OC方法的继承体系

struct objc_class {
    Class _Nonnull isa  OBJC_ISA_AVAILABILITY;
#if !__OBJC2__
    Class _Nullable super_class                              OBJC2_UNAVAILABLE;
    const char * _Nonnull name                               OBJC2_UNAVAILABLE;
    long version                                             OBJC2_UNAVAILABLE;
    long info                                                OBJC2_UNAVAILABLE;
    long instance_size                                       OBJC2_UNAVAILABLE;
    struct objc_ivar_list * _Nullable ivars                  OBJC2_UNAVAILABLE;
    struct objc_method_list * _Nullable * _Nullable methodLists                    OBJC2_UNAVAILABLE;
    struct objc_cache * _Nonnull cache                       OBJC2_UNAVAILABLE;
    struct objc_protocol_list * _Nullable protocols          OBJC2_UNAVAILABLE;
#endif
} OBJC2_UNAVAILABLE;
/* Use `Class` instead of `struct objc_class *` */

每个方法从所属类的“方法列表”中查找,找不到则沿继承体系向上查找,如果还是找不到,执行“完整的消息转发机制”,这里有个问题,如果每次都执行同一个方法都要查找一次,那么这个耗时还是比较长的,所以每次找到方法后会将方法存入“快速映射表”中以便下次快速查找。“快速映射表”结构体代码如下

struct objc_cache {
    unsigned int mask /* total = mask + 1 */          OBJC2_UNAVAILABLE;
    unsigned int occupied                             OBJC2_UNAVAILABLE;
    Method buckets[1]                                 OBJC2_UNAVAILABLE;
};

那么这个cache到底有什么作用,OC许多类方法多到几十种,但是只有10%-20%的方法会经常调用,可能产生的调用次数超过80%,所以缓存的作用就体现出来了,首次调用方法从缓存中查找如果找不到再从方法列表中查找,极大的提升了查询效率。

这里已经看到了在Runtime的作用下,objc_msgSend函数完成了面向对象消息传递向面向过程函数调用的转化,这也是消息机制的基础,那么就存在一个问题了,如果一条消息发送失败,runtime会如何处理呢?

消息转发机制:unrecognized selector出现后的三次机会

首先看一个最典型的案例

id string = @"hello";
[string addObject:@"world"];

这里用id类型声明了一个变量string,这时string默认为NSString类型,那我们调用NSMutableArray的方法,就会报错,[__NSCFConstantString addObject:]: unrecognized selector sent to instance 0x100001040,我们知道oc方法继承体系是通过isa指针向上查找,直到NSObject依然找不到方法,就会调用doesNotRecognizeSelector:方法,报unrecognized selector的错误,当然这不是结束,oc在每个消息发送失败后会给出我们三次机会来解决这条消息,这就是消息转发机制

消息转发分为两大阶段,第一阶段看接受者是否能动态添加方法,这叫做动态方法解析,如果运行期第一阶段已经被系统执行完毕,那么执行第二阶段分为两小步,先看其他有没有对象能处理这条消息,如果有备援的接收者,将消息转发给这个对象,如果没有,则会启动完整的消息转发机制,将所有信息封装到 NSInvocation 对象中,给最后一次解决这个问题的机会。

消息转发流程简图.png

下面举一个例子,首先如果一个属性执行了@dynamic property;,那么如果外界调用这个属性的setter或getter都会crash这里用动态方法解析的方式解决一下这个问题

+ (BOOL)resolveInstanceMethod:(SEL)selector;  //当遇到无法解读的实例方法时调用这个方法
+ (BOOL)resolveClassMethod:(SEL)selector;     //当遇到无法解读的类方法时调用这个方法
Son * son = [Son new];
son.name = @"Tom";
NSLog(@"%@", son.name);
void dynamicNameSetter (id self,SEL _cmd, id value) {
    Son *son = (Son *)self;
    son.resolveName = (NSString *)value;
}

id dynamicNameGetter (id self,SEL _cmd) {
    Son *son = (Son *)self;
    return son.resolveName;
}

@implementation Son

@dynamic name;

+ (BOOL)resolveInstanceMethod:(SEL)sel {
    
    if (sel == @selector(setName:)) {
        class_addMethod(self, sel, (IMP)dynamicNameSetter, "v@:@");
        return true;
    }else if(sel == @selector(name)) {
        class_addMethod(self, sel, (IMP)dynamicNameGetter, "@@:");
        return true;
    }
    return [super resolveClassMethod:sel];
};

这里看到,当我们调用son的setter方法,因为实现了@dynamic;关键字,所以首先消息发送失败,走到消息转发的第一步resolveInstanceMethod,通过添加方法的方式,将值绑定到resolveName上,实现setter和getter方法。

那么如果上一步没有操作,会走到第二步

- (id)forwardingTargetForSelector:(SEL)selector;
//如果找到接收者,返回那个对象,否则返回nil,这一步转发的消息不能进行操作,可以通过完整的消息转发机制操作。
@implementation Son

- (id)forwardingTargetForSelector:(SEL)selector {
    return [[Daughter alloc] init];
}

@end

@implementation Daughter

- (void)substitute {
    NSLog(@"I can realize");
}

@end

这里外部调用

Son * son = [Son new];
[son performSelector:@selector(substitute)];

虽然son找不到substitute方法,但是它的备援接收者daughter可以,所以不会报错,并有成功打印。

如果以上操作都没有做,会执行最后一步完整的消息转发机制

- (void)forwardInvocation:(NSInvocation*)invocation;
//封装了NSInvocation对象,将target,selector,和parameters全部封装其中

总结

ObjC是一门动态语言,以消息机制方式代替函数调用方式,运行时才会决定消息的发送成功与失败,即使发送失败也有一套消息转发机制来避免crash,总共分为动态方法解析备援接收者完整的消息转发三步,如果全部没有操作才会抛出异常。

后续

到这里分析了基础的两个部分,分别是运行时结构和消息机制,感兴趣的朋友们可以移步下一篇文章 OC运行时机制Runtime(三):关联对象Associated Object和分类Category,如果觉得本文对您有些作用,请在下方点个赞再走哈~

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