Objective-c Runtime 消息发送机制

Objective-c 类的结构

Objective-c 作为一种动态语言,除了需要一个编译器来编译之外,还需要有一套运行时环境来动态的创建类和对象。这就是我们要了解的Runtime。Runtime的核心是消息传递(Messaging),了解这一核心能够更好的利用语言的特点,适当的时候进行扩展。

与静态语言不同的是,Objective-c在调用方法的时候并不是直接跳转到编译时生成的函数地址执行代码,而是会把这个方法以消息的形式发送给object。能否由object执行或者转发给别的对象处理或者不做处理,都是在运行时决定的。

打开objc/runtime.h可以看到,系统声明了一个objc_class的结构体


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;

从结构体中可以看出,一个运行时类关联了它的父类指针,类名,版本号,成员变量,方法列表(实例方法),缓存,协议。

其中成员变量,方法列表都是一个结构体类型


//成员变量和成员变量列表

struct objc_ivar {

    char * _Nullable ivar_name                              OBJC2_UNAVAILABLE;

    char * _Nullable ivar_type                              OBJC2_UNAVAILABLE;

    int ivar_offset                                          OBJC2_UNAVAILABLE;

#ifdef __LP64__

    int space                                                OBJC2_UNAVAILABLE;

#endif

}                                                            OBJC2_UNAVAILABLE;

struct objc_ivar_list {

    int ivar_count                                          OBJC2_UNAVAILABLE;

#ifdef __LP64__

    int space                                                OBJC2_UNAVAILABLE;

#endif

    /* variable length structure */

    struct objc_ivar ivar_list[1]                            OBJC2_UNAVAILABLE;

}                                                            OBJC2_UNAVAILABLE;

//方法和方法列表

struct objc_method {

    SEL _Nonnull method_name                                OBJC2_UNAVAILABLE;

    char * _Nullable method_types                            OBJC2_UNAVAILABLE;

    IMP _Nonnull method_imp                                  OBJC2_UNAVAILABLE;

} 

struct objc_method_list {

    struct objc_method_list * _Nullable obsolete            OBJC2_UNAVAILABLE;

    int method_count                                        OBJC2_UNAVAILABLE;

#ifdef __LP64__

    int space                                                OBJC2_UNAVAILABLE;

#endif

    /* variable length structure */

    struct objc_method method_list[1]                        OBJC2_UNAVAILABLE;

}                                                            OBJC2_UNAVAILABLE;

objc_ivar_list和objc_method_list是分别用来存储成员变量和方法的列表,这两个结构体内部有objc_ivar,和objc_method这两个结构体,这两个结构体存储的是对应的成员变量和方法。其中objc_method这个结构体有SEL method_name 和 IMP method_imp这两个变量,分别对应方法的名称和实现。

SEL是selector在oc中的表示,本质是一个映射到方法的一个C字符串。

IMP是一个函数指针,指向一个函数的实现

这里还要再了解一个c结构体objc_object


struct objc_object { 

    Class isa  OBJC_ISA_AVAILABILITY;

};

这个结构体对应的是我们类的实例,结构体内部只有一个isa属性,指向的正是objc_class这个结构体,而objc_class这个结构体中也有一个isa指针说明,类本身也是一种对象,这个isa指向的是类对象的元类Meta Class,Meta Class 表述了类对象本身所具备的元数据。那既然类本身作为一个对象存在,那类方法是不是就相当于是类对象的实例方法那?我们通过下面的代码来测试一下:


Test *test = [[Test alloc] init];

unsigned int count = 0;

Method *meths = class_copyMethodList(object_getClass([test class]), &count);

for (NSInteger i = 0; i < count; i++) {

    Method meth = meths[i];

    SEL sel = method_getName(meth);

    const char *name = sel_getName(sel);

    NSLog(@"%s", name);

}

free(meths);

runtime.h中提供了很多方便的方法使我们可以轻易的获取方法列表或者成员变量,其中以class_开头的是跟objc_class相关的方法,以object_开头的是跟objc_object相关的方法,分别对应我们的类对象和实例对象。通过上面的object_getClass方法可以获取需要的类,传入的一个obj对象,如果传入的是test则打印出来的方法名是实例方法,如果传入的是test的类则打印出来的是类方法,通过这个测试也验证了上面的猜测。

了解了类的构成之后,那么在调用方法的时候是怎么处理的那?oc的运行时特性又是怎么体现出来的那?

消息发送

事实上,在编译时,oc的调用方法会被翻译成一个c的函数调用,objc_msgSend(receiver, selector, arg1, arg2, ...).

从上面的分析可以看出,发送一条消息应该执行下列步骤:

1. 通过obj的isa找到实例的class

2. 在class的方法列表中找对应的方法

3. 如果没找到就去superclass里继续找

4. 一旦找到就去执行它的实现IMP

一个正常的方法通常都是这样被调用的,而且由于不同的方法调用频率是不一样的,所以系统提供了一个cache也就是上面看到的objc_cache来做缓存,提高查询效率。

动态方法解析,消息转发和方法签名

上面的例子只是针对正常的情况,那如果该方法没找到怎么办,这时就会抛出unrecognized selector sent to … 的异常,这也是我们在开发中经常遇到的情况。那怎么避免这种情况那,这就用到了oc的动态特性,在一场抛出前,runtime会给三次补救的机会:

1.动态方法解析

查看NSObject的API发现有下面这两个方法+resolveInstanceMethod:和+resolveClassMethod:分别对应实例方法和类方法。在没有找到方法的情况下,如果实现了这两个方法中的一个,那么系统就会重新启动一次消息发送的过程。

以上面的Test类为例,如果一个Test的实例调用一个method1:方法,而Test类没有实现的话使用下面的代码可以避免程序闪退:


void method2(id obj, SEL _cmd)

{

    NSLog(@"do something");

}

+(BOOL)resolveInstanceMethod:(SEL)aSEL

{

    if(aSEL == @selector(method1:))

    {

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

        return YES;

    }

    return [super resloveInstanceMethod];

}

2.如果resolve方法返回NO,就会进行下一步,消息转发(Message Forwarding)

同样runtime提供了一个方法,-forwardingTargetForSelector: ,用这个方法可以获得把消息转发给其他对象的机会,该方法返回一个实现了被调用的方法的实例:


- (id)forwardingTargetForSelector:(SEL)aSelector

{

    if (aSelector == @selector(method1:))

    {

        return xxxObject;

    }

    return [super forwardingTargetForSelector:aSelector];

}

3.如果这个方法也返回nil,则会启动最后一步,方法签名,首先runtime会发送一个-methodSignatureForSelector:消息来获取参数和返回值,如果返回nil则发送doesNotRecognizeSelector:消息,这是程序也就崩溃了。如果返回了一个函数签名,runtime就会创建一个NSInvocation对象,发送-forwardInvocation:消息给当前对象,并传入NSInvocation:


- (NSMethodSignature *)methodSignatureForSelector:(SEL)selector

{

    NSMethodSignature *signature = [super methodSignatureForSelector:selector];

    if(!signature) {

        if([xxxObj respondsToSelector:selector]) {

            return [xxxObj methodSignatureForSelector:selector];

        }

    }

    return signature;

}

- (void)forwardInvocation:(NSInvocation*)invocation

{

    if ([xxxObj respondsToSelector:[invocation selector]]) {

        [invocation invokeWithTarget:xxxObj];

    }else {

        [self doesNotRecognizeSelector:sel];

    }

}

这就是Objective-c关于消息发送的流程,利用这个流程和运行时的特性可以对语音进行扩展和解决一些问题。


Refer:

Objective C Runtime

iOS开发-Runtime详解(简书) - 一个低调的iOS开发 - 博客园

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

推荐阅读更多精彩内容