Runtime 系列 1-- 从一个崩溃谈起
本文从一个崩溃问题谈起,然后逐步深入,探讨下runtime的细节和使用,主要涉及到的知识点如下:
- objc_msgSend的实现原理
- isa指针
- 类和元类
- 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的实现原理。
- 检测这个 selector 是不是要忽略的。比如 Mac OS X 开发,有了垃圾回收就不理会 retain, release 这些函数了。
- 检测这个 target 是不是 nil 对象。ObjC 的特性是允许对一个 nil 对象执行任何一个方法不会 Crash,因为会被忽略掉。
- 如果上面两个都过了,那就开始查找这个类的 IMP,先从 cache 里面找,完了找得到就跳到对应的函数去执行。
- 如果 cache 找不到就找一下方法分发表。
- 如果分发表找不到就到超类的分发表去找,一直找,直到找到NSObject类为止。
- 如果还找不到就要开始进入动态方法解析了,这个我在另外一篇文章《runtime消息转发机制的理解和使用》中会详细描述
PS:这里说的分发表其实就是Class中的方法列表,它将方法选择器和方法实现地址联系起来。
一图以蔽之:
imageisa指针
上面的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)
一图以蔽之
imageobject_getClass(obj)与[obj class]的区别
-
object_getClass(obj)返回的是obj中的isa指针;
-
而[obj class]则分两种情况:
-
当obj为实例对象时,[obj class]调用的是实例方法:-(Class)class,返回的obj对象中的isa指针;
-
当obj为类对象(包括元类和根类以及根元类)时,调用的是类方法:+ (Class)class,返回的结果为其本身。
-
-
-(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]后的内部流程如下:
- 当使用 [super class] 时,这时要转换成 objc_msgSendSuper 的方法。
- 先构造 objc_super 的结构体,第一个成员变量就是 self,第二个成员变量是 father,然后要找 class 这个 selector,先去 superClass 也就是father中去找,没有,然后去father的父类中去找,结果还是在 NSObject 中找到了。
- 然后内部使用函数 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