Runtime-消息机制

Objective-C是一门动态语言,类型的判断、类的成员变量、方法的内存地址都是在程序的运行阶段才最终确定,并且还能动态的添加成员变量和方法。这意味着即使调用对象一个没有实现的方法,编译也能通过,甚至一个对象它是什么类型并不是表面我们所看到的那样,只有运行之后才能决定其真正的类型。因此OC具有多态动态类型动态绑定特性。

正是因为其动态特性,在OC中调用方法并不是一个简单的函数调用过程,它并不直接调用方法,而是运用Runtime机制实现函数调用,称之为消息发送

Runtime

Runime运行时,是OC中一套底层的C语言API,底层都是基于它来实现的。在运行时,我们所编写的代码会转换为C语言运行。具体内容可以参考:runtime概述

在OC中发送一条消息:[receiver message]
会转为调用底层函数:id objc_msgSend(id self, SEL op, ...)
让我们先看看这个函数的参数和返回值
id在OC中表示任意类型的对象,其结构如下:

/// Represents an instance of a class.
struct objc_object {
    Class _Nonnull isa  OBJC_ISA_AVAILABILITY;
};
/// A pointer to an instance of a class.
typedef struct objc_object *id;

显然id是一个objc_objec结构体类型指针,其内部有一个Class类型的isa指针,指向对应的类。
SEL是对方法的包装,返回对应的编号

struct objc_class {
    Class _Nonnull isa             指向自身对应的类;
#if !__OBJC2__
    Class _Nullable super_class            指向父类                  
    const char * _Nonnull name                               
    long version                                             
    long info                                                
    long instance_size                                       
    struct objc_ivar_list * _Nullable ivars                  
    struct objc_method_list * _Nullable * _Nullable methodLists    类中的方法信息列表                 
    struct objc_cache * _Nonnull cache         调用过的方法信息列表             
    struct objc_protocol_list * _Nullable protocols          
#endif
}

上面是class的结构

消息发送过程一 --- 函数在类中实现

当发送一条消息时,object_getClass(id obj)通过id的isa指针找到对象的对应的类class,在class中先去cache中 通过SEL查找对应函数method(cache中method列表是以SEL为key通过hash表来存储的,这样能提高函数查找速度),若 cache中未找到,方法列表methodList中查找,若能找到,则将method加 入到cache中,以方便下次查找。否则到对应的super_class中查找,如此循环直到NSObject。

我们具体看OC如何用runtime实现函数调用:

自定义Person对象
@interface Person : NSObject
+ (void)eat;
- (void)eat;
- (void)run:(int)age;
@end
@implementation Person
+ (void)eat
{
    NSLog(@"类方法-吃东西");
}
- (void)eat
{
     NSLog(@"实例方法-吃东西");
}
- (void)run:(int)age
{
    NSLog(@"跑了%d米",age);
}
@end
-------------------------UIViewController-----------------------------
- (void)viewDidLoad {
    [super viewDidLoad];

    Person *person = [[Person alloc] init];
    objc_msgSend(person, @selector(eat));
    objc_msgSend(person, @selector(run:),10);

    Class personClass = [Person class];
    objc_msgSend(personClass, @selector(eat));
}
-------------------------console-----------------------------
[1373:101045] 实例方法-吃东西
[1373:101045] 跑了10米
[1373:101045] 类方法-吃东西

以上就是发送消息调用类中实现的方法的过程。


消息发送过程二 --- 函数在类中未实现,动态解析方法

当向receiver发送message时,若SEL对应的函数未在类或者类的父类中实现,程序并不会立即crash,而是对message进一步转发,调用_objc_msgForward(id receiver, SEL sel, ...),具体表现在类中对应的方法为:

1.+ resolveInstanceMethod:(SEL)sel // 对应实例方法
  + resolveClassMethod:(SEL)sel // 对应类方法
2.- (id)forwardingTargetForSelector:(SEL)aSelector
3.- (void)forwardInvocation:(NSInvocation *)anInvocation

注意:以上方法优先级依次递减,高优先级方法消息转发成功不会再执行低优先级方法

现在为Person实例对象发送一条@selector(wy_sleep:)消息:

[person performSelector:@selector(wy_sleep:) withObject:@100];

因为person未实现@selector(wy_sleep:),消息转发转而调用+ (BOOL)resolveInstanceMethod:(SEL)sel:,判断类是在运行时动态的添加方法实现

+ (BOOL)resolveInstanceMethod:(SEL)sel
{
    if (sel == @selector(wy_sleep:)) {
        
        /*
         cls:给哪个类添加方法
         SEL:添加方法的方法编号是什么
         IMP:方法实现,函数入口,函数名
         types:方法类型
         最后一个参数请查看文档Help - API reference - runtime
         */
        class_addMethod([self class], sel, (IMP)wy_sleep, "v@:@");
        
        return YES;
    }
    return [super resolveInstanceMethod:sel];
}

void wy_sleep(id self, SEL _cmd, id param1){
    
    NSLog(@"调用eat %@ %@ %@",self,NSStringFromSelector(_cmd),param1);
}

-------------------------console-----------------------------
[1553:136714] 调用wy_sleep <Person: 0x60000003ed40> wy_sleep: 100

其中class_addMethod(Class cls, SEL name, IMP imp, const char *types)为类添加新的方法实现。


消息发送过程三 --- 函数在类中未实现且无动态解析,快速转发消息

当调用的方法没有在类中实现,且没有动态添加时,则消息继续转发,判断类中是否预备了备用的消息接受者

- (id)forwardingTargetForSelector:(SEL)aSelector
简单、快速,但只能只能指定一个转发对象,无法对消息进行操作

 定义对象A、B
-------------------------A-----------------------------
@interface A : NSObject
- (void)prinAName;
@end
@implementation A
- (void)prinAName
{
    NSLog(@"Hello World! My name is A");
}
@end

-------------------------B-----------------------------
@interface B : NSObject
- (void)prinBName;
@end
@implementation B
- (void)prinBName
{
    NSLog(@"Hello World! My name is B");
}
@end

实现Person中的的快速消息转发:

- (id)forwardingTargetForSelector:(SEL)aSelector
{
    if (class_respondsToSelector([A class], aSelector)) {
        return [[A alloc] init];
    }
    
    B *b = [[B alloc] init];
    if ([b respondsToSelector:aSelector]) {
        return b;
    }
    return [super forwardingTargetForSelector:aSelector];
}

向person实例发送相应的消息:

[person performSelector:@selector(prinAName)];
[person performSelector:@selector(prinBName)];

输出结果:
[1962:235819] Hello World! My name is A
[1962:235819] Hello World! My name is B


消息发送过程四 --- 函数在类中未实现且无动态解析,完整的消息转发

除了设置备用消息接受者,快速转发消息外,还能将完整的消息转发给实现的对象

-(void)forwardInvocation:(NSInvocation *)anInvocation
- (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector
{
    NSMethodSignature *methodSignature = [A instanceMethodSignatureForSelector:aSelector];
    if (methodSignature == nil) {
        methodSignature = [B instanceMethodSignatureForSelector:aSelector];
    }
    if (methodSignature == nil) {
        methodSignature = [super methodSignatureForSelector:aSelector];
    }
    return methodSignature;
}
- (void)forwardInvocation:(NSInvocation *)anInvocation
{
    SEL selector = [anInvocation selector];
    A *a = [[A alloc] init];
    B *b = [[B alloc] init];
    if ([a respondsToSelector:selector]) {
        [anInvocation invokeWithTarget:a];
    } else if ([b respondsToSelector:selector]) {
        [anInvocation invokeWithTarget:b];
    } else {
        [super forwardInvocation:anInvocation];
    }
}

消息发送过程五 --- Crash

当前面消息发送的4个过程都没有实现时,程序调用- (void)doesNotRecognizeSelector:(SEL)aSelector崩溃

- (void)doesNotRecognizeSelector:(SEL)aSelector
{
    NSLog(@"无法实现方法,crash");
}

总结:
OC中函数调用通过发送消息实现,分为五个步骤,如下:

消息发送流程

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

推荐阅读更多精彩内容

  • 从异常说起 我们都知道,在iOS中存在这么一个通用类类型id,它可以用来表示任何对象的类型 —— 这意味着我们使用...
    sindri的小巢阅读 4,496评论 14 43
  • objc_msgSend OC中的实例对象调用一个方法称作消息传递,例如: 代码中,我们将inkInstance这...
    xl_b973阅读 477评论 1 6
  • 转至元数据结尾创建: 董潇伟,最新修改于: 十二月 23, 2016 转至元数据起始第一章:isa和Class一....
    40c0490e5268阅读 1,715评论 0 9
  • 虽然swift 慢慢会越用越多,但凭借oc 强大的runtime机制,oc依旧是现在的中流砥柱.只做记录探讨 加深...
    zqzal阅读 197评论 0 1
  • 《初夏书感》 春与人俱老,花随梦已空。游蜂黏落蕊,轻燕接飞虫。桑悴知蚕起,牲肥赛麦...
    土豪总部阅读 352评论 0 0