Runtime 系列 1-- 从一个崩溃谈起

本文从一个崩溃问题谈起,然后逐步深入,探讨下runtime的细节和使用,主要涉及到的知识点如下:

  1. objc_msgSend的实现原理
  1. isa指针
  2. 类和元类
  3. object_getClass(obj)与[obj class]的区别

崩溃代码

我们先来看看两段代码,第一段代码主要是展示[self class ]和[super class]的区别,第二段代码会在第一段代码的基础上进一步探讨他们的区别,然后就为什么会引起崩溃做进一步探讨,这会涉及到上面的四个runtime相关的知识点

第一段代码

#import "father.h"

@interface son : father

@end

#import "son.h"

@implementation son
-(instancetype)initWithCoder:(NSCoder *)aDecoder{
    if (self = [super initWithCoder:aDecoder]) {
        NSLog(@"self class-->%@",[self class]);
        NSLog(@"super class-->%@",[super class]);

    }
    return self;
}
@end

输出:
2016-08-09 09:42:40.152 test1[33870:252634] self class-->son
2016-08-09 09:42:40.153 test1[33870:252634] super class-->son

分析:

根据其他语言的经验,我们想当然会认为self class是son,super class是father。但是输出的却都是一样的,都是son。这是因为oc一切方法的本质都是消息的发送和接受,是动态的,不能按照字面意思理解。具体的我们后面再进一步探讨。

第二段代码:

#import <UIKit/UIKit.h>

@interface father : UIViewController
@property(nonatomic,strong) NSString * name;

@end

=====================================

#import "father.h"

@implementation father

-(instancetype)initWithCoder:(NSCoder *)aDecoder{
    if (self =[super initWithCoder:aDecoder]) {
        self.name = @"";
    }
    return self;
}


-(void)setName:(NSString *)name{
    NSLog(@"%s,%@", __PRETTY_FUNCTION__, @"不会调用这个方法");
    _name = name;
}
@end

#import "father.h"

@interface son : father

@end

===================================
#import "son.h"

@implementation son
-(instancetype)initWithCoder:(NSCoder *)aDecoder{
    if (self = [super initWithCoder:aDecoder]) {
        NSLog(@"self class-->%@",[self class]);
        NSLog(@"super class-->%@",[super class]);

    }
    return self;
}

-(void)setName:(NSString *)name{
    NSLog(@"%s,%@", __PRETTY_FUNCTION__, @"会调用这个方法");
    if ([name isEqualToString:@""]){
        [NSException raise:NSInvalidArgumentException format:@"姓名不能为空"];
    }
    

}

@end

输出:

2016-08-09 10:00:22.203 test1[34027:265079] -[son setName:],会调用这个方法
2016-08-09 10:00:26.316 test1[34027:265079] *** Terminating app due to uncaught exception 'NSInvalidArgumentException', reason: '姓名不能为空'

分析:

我们在父类father的initWitheCoder方法中设置self.name = @"",我们只想初始化一下name的值为空。然后我们在子类son中重写了setName方法,设置不让name变量为空,否则抛出异常。

按道理说,我们在父类使用self.name方法应该调用father的setName方法,在子类son中使用self.name方法也应该调用sone的setName方法。但是实际上我们看到在父类中使用self.name调用的确实子类son的setName方法,从而导致了崩溃,这是为什么呢?

暂且按下不表,我们先来看看runtime相关的一些知识,只有理解了这些知识,我们才能真正理解上面出现的问题。


objc_msgSend的实现原理

我们平时使用方法调用都是如下的模式:

[target MethodName:var1];

但是却很少去深究这句代码为什么能执行,怎么执行。下面我们就来看看,

首先这句代码会被编译为如下样式:

objc_msgSend(target,@selector(MethodName:),var1);

而objc_msgSend函数就是我们runtime里面的一个非常重要的函数,所有的消息转发都和这个函数息息相关。

ObjC 是一种面向runtime(运行时)的语言,也就是说,它会尽可能地把代码执行的决策从编译和链接的时候,推迟到运行时。这给程序员写代码带来很大的灵活性,比如说你可以把消息转发给你想要的对象,或者随意交换一个方法的实现之类的。这就要求 runtime 能检测一个对象是否能对一个方法进行响应,然后再把这个方法分发到对应的对象去。我们拿 C 来跟 ObjC 对比一下。在 C 语言里面,一切从 main 函数开始,程序员写代码的时候是自上而下地,一个 C 的结构体或者说类吧,是不能把方法调用转发给其他对象的。但是在oc中,我们可以在运行时把上面的target换成其他对象,非常灵活。

objc_msgSend函数的原型如下:

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

上面的函数里面有两个参数id和SEL,我们分别看看。

id

objc_msgSend第一个参数类型为id,大家对它都不陌生,它是一个指向类实例的指针:

typedef struct objc_object *id;

那objc_object又是啥呢:

struct objc_object { Class isa; };

objc_object结构体包含一个isa指针,根据isa指针就可以顺藤摸瓜找到对象所属的类。

PS:

isa指针不总是指向实例对象所属的类,不能依靠它来确定类型,而是应该用class方法来确定实例对象的类。因为KVO的实现机理就是将被观察对象的isa指针指向一个中间类而不是真实的类,这是一种叫做 isa-swizzling 的技术。

SEL

objc_msgSend函数第二个参数类型为SEL,它是selector在Objc中的表示类型(Swift中是Selector类)。selector是方法选择器,可以理解为区分方法的 ID,而这个 ID 的数据结构是SEL:

typedef struct objc_selector *SEL;

其实它就是个映射到方法的C字符串,你可以用 Objc 编译器命令@selector()或者 Runtime 系统的sel_registerName函数来获得一个SEL类型的方法选择器。

可以根据SEL(方法编号)去类方法列表找到对应的实例方法的实现,或者去元类方法列表找到对应的类方法的实现.

消息转发步骤

结合上面的知识点,我们现在就可以理解了objc_msgSend的实现原理。

  1. 检测这个 selector 是不是要忽略的。比如 Mac OS X 开发,有了垃圾回收就不理会 retain, release 这些函数了。
  2. 检测这个 target 是不是 nil 对象。ObjC 的特性是允许对一个 nil 对象执行任何一个方法不会 Crash,因为会被忽略掉。
  3. 如果上面两个都过了,那就开始查找这个类的 IMP,先从 cache 里面找,完了找得到就跳到对应的函数去执行。
  4. 如果 cache 找不到就找一下方法分发表。
  5. 如果分发表找不到就到超类的分发表去找,一直找,直到找到NSObject类为止。
  6. 如果还找不到就要开始进入动态方法解析了,这个我在另外一篇文章《runtime消息转发机制的理解和使用》中会详细描述

PS:这里说的分发表其实就是Class中的方法列表,它将方法选择器和方法实现地址联系起来。

一图以蔽之:

image

isa指针

上面的objc_msgSend实现原理里面提到了isa指针、类。也是我们平常经常解除的两个概念,但是他们内部具体如何实现,却很少深究。

我们知道所有的对象都是由其对应的类实例化而来,在Objective-C中,我们用到的几乎所有类都是NSObject类的子类,NSObject类定义格式如下(忽略其方法声明):

@interface NSObject <NSObject> {
Class isa;
}

这个Class为何物?在objc.h中我们发现其仅仅是一个结构(struct)指针的typedef定义:

typedef struct objc_class *Class;

同样的,objc_class又是什么呢?在Objective-C2.0中,objc_class的定义如下:

struct objc_class {
Class isa;
}

写到这里大家可能就晕了,怎么又有一个isa?

我们知道isa指针指向的是该对象所属的类,对于实例对象的isa指针我们知道是指向其所属的类,但是实例对象所属的类的isa指针又指向谁呢?

这里我们先记住一点:类本身也是对象!!

那么既然类本身也是对象,那么他所属的类是谁?

答案就是:元类!!

所以实例对象所属的类的isa指针指向的是元类。

1.类对象的实质

类对象是由编译器创建的,即在编译时所谓的类,就是指类对象(官方文档中是这样说的: The class object is the compiled version of the class)。

任何直接或间接继承了NSObject的类,它的实例对象(instance objec)中都有一个isa指针,指向它的类对象(class object)。这个类对象(class object)中存储了关于这个实例对象(instace object)所属的类的定义的一切:包括变量,方法,遵守的协议等等。

因此,类对象能访问所有关于这个类的信息,利用这些信息可以产生一个新的实例,但是类对象不能访问任何实例对象的内容。当你调用一个 “类方法” 例如 [NSObject alloc],你事实上是发送了一个消息给他的类对象。

2.类对象和实例对象的区别

尽管类对象保留了一个类实例的原型,但它并不是实例本身。它没有自己的实例变量,也不能执行那些类的实例的方法(只有实例对象才可以执行实例方法)。然而,类的定义能包含那些特意为类对象准备的方法–类方法( 而不是的实例方法)。类对象从父类那里继承类方法,就像实例从父类那里继承实例方法一样。

类对象是一个功能完整的对象,所以也能被动态识别(dynamically typed),接收消息,从其他类继承方法。特殊之处在于它们是由编译器创建的,缺少它们自己的数据结构(实例变量),只是在运行时产生实例的代理。

元类

实际上,类对象是元类对象的一个实例!!

元类描述了 一个类对象,就像类对象描述了普通对象一样。不同的是元类的方法列表是类方法的集合,由类对象的选择器来响应。当向一个类发送消息时,objc_msgSend会通过类对象的isa指针定位到元类,并检查元类的方法列表(包括父类)来决定调用哪个方法。元类代替了类对象描述了类方法,就像类对象代替了实例对象描述了实例化方法。

很显然,元类也是对象,也应该是其他类的实例,实际上元类是根元类(root class’s metaclass)的实例,而根元类是其自身的实例,即根元类的isa指针指向自身。

类的super_class指向其父类,而元类的super_class则指向父类的元类。元类的super class链与类的super class链平行,所以类方法的继承与实例方法的继承也是并行的。而根元类(root class’s metaclass)的super_class指向根类(root class),这样,整个指针链就链接起来了!!

记住,当一个消息发送给任何一个对象, 方法的检查 从对象的 isa 指针开始,然后是父类。实例方法在类中定义, 类方法在元类和根类中定义。(根类的元类就是根类自己)。

总结

综上所述,类对象(class object)中包含了类的实例变量,实例方法的定义,而元类对象(metaclass object)中包括了类的类方法(也就是C++中的静态方法)的定义。

类对象和元类对象中当然还会包含一些其它的东西,苹果以后也可能添加其它的内容,但对于我们只需要记住:类对象存的是关于实例对象的信息(变量,实例方法等),而元类对象(metaclass object)中存储的是关于类的信息(类的版本,名字,类方法等)。

要注意的是,类对象(class object)和元类对象(metaclass object)的定义都是objc_class结构,其不同仅仅是在用途上,比如其中的方法列表在类对象(instance object)中保存的是实例方法(instance method),而在元类对象(metaclass object)中则保存的是类方法(class method)

一图以蔽之

image

object_getClass(obj)与[obj class]的区别

  1. object_getClass(obj)返回的是obj中的isa指针;

  2. 而[obj class]则分两种情况:

    • 当obj为实例对象时,[obj class]调用的是实例方法:-(Class)class,返回的obj对象中的isa指针;

    • 当obj为类对象(包括元类和根类以及根元类)时,调用的是类方法:+ (Class)class,返回的结果为其本身。

  3. -(Class)class的实现如下:

    - (Class)class {
     return object_getClass(self);
     }
    

第一段代码解析

回头我们再看看第一段代码为什么[self class]和[super class]都输出的是son。

[self class]

根据上面的知识,我们知道[self class]最终会转换为如下形式:

id objc_msgSend(son的实例对象self, @selector(class), ...)

消息的接受者是son的实例对象self,然后调用他的class方法,它自己没有实现该方法,最终在NSObject中找到该方法的实现,然后返self的isa指针,此时self是son类的实例对象,那么isa指针也就是指向son类,所以[self class]返回的son。

[super class]

而当使用 [super setName] 调用时,会使用 objc_msgSendSuper 函数.
看下 objc_msgSendSuper 的函数定义:

id objc_msgSendSuper(struct objc_super *super,  @selector(class), ...)

第一个参数是个objc_super的结构体,第二个参数还是类似上面的类方法的selector,先看下objc_super这个结构体是什么东西:

struct objc_super {
   id receiver;
   Class superClass;
};

在此处上面的结构体转换为如下样式:

struct objc_super {
   son的实例对象self;
   father;
};

那么调用[super class]后的内部流程如下:

  1. 当使用 [super class] 时,这时要转换成 objc_msgSendSuper 的方法。
  2. 先构造 objc_super 的结构体,第一个成员变量就是 self,第二个成员变量是 father,然后要找 class 这个 selector,先去 superClass 也就是father中去找,没有,然后去father的父类中去找,结果还是在 NSObject 中找到了。
  3. 然后内部使用函数 objc_msgSend(objc_super->receiver, @selector(class)) 去调用,此时已经和我们用 [self class] 调用时相同了,因为此时的 receiver 还是 son的实例对象self,所以这里返回的也是 son。

总结

很多人会想当然的认为“ super 和 self 类似,应该是指向父类的指针吧!”。这是很普遍的一个误区。

其实 super 是一个 Magic Keyword, 它本质是一个编译器标示符,和 self 是指向的同一个消息接受者!他们两个的不同点在于:super 会告诉编译器,调用 class 这个方法时,要去父类的方法,而不是本类里的。

上面的例子不管调用[self class]还是[super class],接受消息的对象都是当前 Son *xxx 这个对象。

当使用 self 调用方法时,会从当前类的方法列表中开始找,如果没有,就从父类中再找;而当使用 super 时,则从父类的方法列表中开始找。然后调用父类的这个方法。


第二段代码解析

第二段代码崩溃的原因是因为在father里面使用self.name = @""调用的是子类的setName方法,从而导致了崩溃。

我们来看看为什么没有调用自己的setName方法,反而是调用了子类son的setName方法。

其实结合第一段代码解析就知道,在父类father里面调用[self setName]方法,消息的接受者依然是son的实例对象,然后去son的类方法列表去找setName方法,找到了,就执行。

所以你会看到明明在父类里面调用的自己的setName方法,但是真正被执行的确实子类son的setName方法。

所以我们要注意,如果子类重写了父类的方法,那么不管在子类还是父类调用该方法,最终被执行的方法是子类的方法。


总结

本文从一个崩溃问题谈起,然后开始逐步深入,探讨了一些runtime的特性和机制,由此可见runtime的一些本质,但也只是管中窥豹,做抛砖引玉之用,大家有更好的想法,欢迎探讨。

后续我会继续对runtime其他特性进行介绍,欢迎一起探讨。

这是runtime的源码,有兴趣的同学可以自行阅读,可以加深理解

http://opensource.apple.com//source/objc4/objc4-208/runtime/objc-runtime.m

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

推荐阅读更多精彩内容