本文的切入点是2014年的一场线下分享会,也就是sunnyxx分享的objc runtime。很惭愧,这么多年了才完整的看了一下这个分享会视频。当时他出了一份试题,并戏称精神病院objc runtime入院考试。git
咱们今天的这篇文章就是从这个试题中的题目入手,来深刻的学习runtime。github
源码版本objc4-750工具
@implementation Son : Father - (id)init { self = [super init]; if (self) { NSLog(@"%@", NSStringFromClass([self class])); NSLog(@"%@", NSStringFromClass([super class])); } return self; } @end
第一行的[self class]
应该是没有疑问的,确定是Son
,问题就出在这个[super class]
。学习
你们都知道,咱们OC的方法在底层会编译为一个objc_msgSend
的方法(消息发送),[self class]
符合这个状况,由于self是类的一个隐藏参数。可是super
并非一个参数,它是一个关键字,其实是一个“编译器标示符”,因此这就有点不同了,经查阅资料,在调用[super class]
的时候,runtime调用的是objc_msgSendSuper
方法,而不是objc_msgSend
。测试
首先要作的是验证一下是不是调用了objc_msgSendSuper
。这里用到了clang这个工具,咱们能够把OC的代码转成C/C++。atom
@implementation Son - (void)test { [super class]; } @end
在终端运行clang -rewrite-objc Son.m
生成一个Son.cpp文件。spa
在这个.cpp文件的底部咱们能够找到这么一部分代码指针
// @implementation Son static void _I_Son_test(Son * self, SEL _cmd) { ((Class (*)(__rw_objc_super *, SEL))(void *)objc_msgSendSuper)((__rw_objc_super){(id)self, (id)class_getSuperclass(objc_getClass("Son"))}, sel_registerName("class")); } // @end
看起来乱七八糟,有不少强制类型转换的代码,不用理它,咱们只要看到了咱们想要的objc_msgSendSuper
就好。code
去源码中看一下这个方法(具体实现好像是汇编,看不懂)orm
OBJC_EXPORT void objc_msgSendSuper(void /* struct objc_super *super, SEL op, ... */ ) OBJC_AVAILABLE(10.0, 2.0, 9.0, 1.0, 2.0);
能够看出来这个方法第一个参数是一个objc_super
类型的结构体,第二个是一个咱们常见的SEL,后面的...表明还有扩展参数。
再看一下这个objc_super
结构体。
/// Specifies the superclass of an instance. struct objc_super { /// Specifies an instance of a class. __unsafe_unretained _Nonnull id receiver; /// Specifies the particular superclass of the instance to message. #if !defined(__cplusplus) && !__OBJC2__ /* For compatibility with old objc-runtime.h header 为了兼容老的 */ __unsafe_unretained _Nonnull Class class; #else __unsafe_unretained _Nonnull Class super_class; #endif /* super_class is the first class to search */ };
第一个参数是接收消息的receiver,第二个是super_class(见名知意~ 😆)。咱们和上面提到的.cpp中的代码对应一下就会发现重点了,receiver是self。
因此,这个[super class]
的工做原理是,从objc_super
结构体的super_class
指向类的方法列表开始查找class
方法,找到这个方法以后使用receiver
来调用。
因此,调用class
方法的其实仍是self
,结果也就是打印Son
。
下面代码的结果?
BOOL res1 = [(id)[NSObject class] isKindOfClass:[NSObject class]]; BOOL res2 = [(id)[NSObject class] isMemberOfClass:[NSObject class]]; BOOL res3 = [(id)[Sark class] isKindOfClass:[Sark class]]; BOOL res4 = [(id)[Sark class] isMemberOfClass:[Sark class]];
对于这个问题咱们就要从OC类的结构开始提及了。
咱们都应该有所了解,每个Objective-c的对象底层都是一个C语言的结构体,在以前老的源码中体现出,全部对象都包含一个isa
类型的指针,在新的源码中已经不是这样了,用一个结构体isa_t
代替了isa
。这个isa_t
结构体包含了当前对象指向的类的信息。
咱们来看看当前的类的结构,首先从咱们的祖宗类NSObject开始吧。
@interface NSObject <NSObject> { Class isa OBJC_ISA_AVAILABILITY; }
咱们的NSObject类有一个Class类型的变量isa,经过源码咱们能够了解到这个Class究竟是什么
typedef struct objc_class *Class; typedef struct objc_object *id; struct objc_object { private: isa_t isa; } struct objc_class : objc_object { // Class ISA; Class superclass; cache_t cache; // formerly cache pointer and vtable class_data_bits_t bits; // class_rw_t * plus custom rr/alloc flags }
上面的代码是我从源码中复制拼到一块儿来的。能够看出来,Class就是是一个objc_class结构体,objc_class中有四个成员变量Class superclass
,cache_t cache
,class_data_bits_t bits
,和从objc_object
中继承过来的isa_t isa
。
当Objc为一个对象分配内存,初始化实例变量后,在这些实例变量的结构体中第一个就是isa。
并且从上面的objc_class的结构能够看出来,不只仅是实例会包含一个isa结构体,全部的类也会有这个isa。
因此说,咱们能够得出这样一个结论:Objective-c中的类也是一个对象。
那如今就有了一个新的问题,类的isa结构体中储存的是什么?这里就要引入一个元类
的概念。
知识补充:
在Objective-c中,每一个对象能执行的方法并无存在这个对象中,由于若是每个对象都单独储存可执行的方法,那对内存来讲是一个很大的浪费,因此说每一个对象可执行的方法,也就是咱们说的一个类的实例方法,都储存在这个类的objc_class
结构体中的class_data_bits_t
结构体里面。在执行方法是,对象经过本身的isa找到对应的类,而后在class_data_bits_t
中查找方法实现。
关于方法的结构,能够看这篇博客来理解一些。(跳转连接)
引入元类就是来保证了实例方法和类方法查找调用机制的一致性。
因此让一个类的isa指向他的元类,这样的话,对象调用实例方法能够经过isa找到对应的类,而后查找方法的实现并调用,在调用类方法的时候,经过类的isa找到对应的元类,在元类里完成类方法的查找和调用。
下面这种图也是在网上很常见的了,不须要过多解释,你们看一下记住就好了。
看到这里咱们就要回到咱们的题目上了。首先呢,仍是要去看一下这个源码中isKindOfClass:
和isMemberOfClass:
的实现了。
先看isKindOfClass
吧,源码中提供了一个类方法一个实例方法。
+ (BOOL)isKindOfClass:(Class)cls { for (Class tcls = object_getClass((id)self); tcls; tcls = tcls->superclass) { if (tcls == cls) return YES; } return NO; } - (BOOL)isKindOfClass:(Class)cls { for (Class tcls = [self class]; tcls; tcls = tcls->superclass) { if (tcls == cls) return YES; } return NO; }
整体的逻辑都是同样的,都是先声明一个Class类型的tcls,而后把这个tcls跟cls比较,看是否相等,若是不相等则循环tcls的各级superclass来进行比较,直到为tcls为nil中止循环。
不一样的地方就是类方法初始的tcls是object_getClass((id)self)
,实例方法的是[self class]
。
object_getClass((id)self)
实际上是返回了这个self的isa对应的结构,由于这个方法是在类方法中调用的,self则表明这个类,那object_getClass((id)self)
返回的也应该是这个类的元类了。
其实在-isKindOfClass
这个实例方法中,调用方法的是一个对象,tcls初始等于[self class]
,也就是对相对应的类。咱们能够看出来,在实例方法中这个tcls初始的值也是方法调用者的isa对应的结构,跟类方法中逻辑是一致的。
回到咱们的题目中,
BOOL res1 = [(id)[NSObject class] isKindOfClass:[NSObject class]];
[NSObject class]
也就是NSObject类调用这个isKindOfClass:
方法(类方法),方法的参数也是NSObject的类。
在第一次循环中,tcls对应的应该是NSObject的isa指向的,也就是NSObject的元类,它跟NSObject类不相等。第二次循环,tcls取本身的superclass继续比较,咱们上面的那个图,你们能够看一下,NSObject的元类的父类就是NSObject这个类自己,在与NSObject比较结果是相等。因此res1为YES。
BOOL res3 = [(id)[Sark class] isKindOfClass:[Sark class]];
跟上面同样来分析,在第一次循环中,tcls对应的应该是Sark的isa指向的,也就是Sark的元类,跟Sark的类相比,确定是不相等。第二次循环,tcls取superclass,从图中能够看出,Sark元类的父类是NSObject的元类,跟Sark的类相比,确定也是不相等。第三次循环,NSObject元类的父类是NSObject类,也不相等。再取superclass,NSObject的superclass为nil,循环结束,返回NO,因此res3是NO。
+ (BOOL)isMemberOfClass:(Class)cls { return object_getClass((id)self) == cls; } - (BOOL)isMemberOfClass:(Class)cls { return [self class] == cls; }
有了上面isKindOfClass逻辑分析的基础,isMemberOfClass的逻辑咱们应该很清楚,就是使用方法调用者的isa对应的结构和传入的cls参数比较。
BOOL res2 = [(id)[NSObject class] isMemberOfClass:[NSObject class]]; BOOL res4 = [(id)[Sark class] isMemberOfClass:[Sark class]];
NSObject类的isa对应的是NSObject的元类,和NSObject类相比不相等,因此res2为NO。
Sark类的isa对应的是Sark的元类,和Sark类相比也是不相等,因此,res4也是NO。
下面的代码会?Compile Error / Runtime Crash / NSLog…?
@interface NSObject (Sark) + (void)foo; @end @implementation NSObject (Sark) - (void)foo { NSLog(@"IMP: -[NSObject (Sark) foo]"); } @end // 测试代码 [NSObject foo]; [[NSObject new] foo];
[[NSObject new] foo];
这一个代码应该是毫无疑问会调用到-foo
方法。问题就在这个[NSObject foo]
,由于在咱们的认识中[NSObject foo]
是调用的类方法,实现的是实例方法,应该不能调用到。
其实这个题的考点跟第二个题差很少,咱们已经知道了,一个类的实例方法储存在类中,类方法储存在这个类的元类。因此NSObject在调用foo这个方法是,会先去NSObject的元类中找这个方法,没有找到,那就要去父类中继续查找。上面图已经给出了,NSObject的元类的父类是NSObject类,因此在NSObject中查找方法,找到方法以后执行打印。
下面的代码会?Compile Error / Runtime Crash / NSLog…?
@interface Sark : NSObject @property (nonatomic, copy) NSString *name; @end @implementation Sark - (void)speak { NSLog(@"my name's %@", self.name); } @end @implementation ViewController - (void)viewDidLoad { [super viewDidLoad]; id cls = [Sark class]; void *obj = &cls; [(__bridge id)obj speak]; } @end
这里咱们先上结果:
my name's <ViewController: 0x7f9454c1c680>
无论地址是多少,打印的老是ViewController。
咱们先想一下为何能够成功的调用speak?id cls = [Sark class];
建立了一个Sark的class。void *obj = &cls;
建立一个obj指针指向了cls的地址。最后使用(__bridge id)obj
把这个obj指针转成一个oc的对象,用对象来调用speak,因此能够调用成功。
咱们在方法中输出的是self.name
,为何会打印出来ViewController?
通过查阅资料得知,在调用self.name的时候,本质上是self指针在内存向高位地址偏移一个指针。(这个还得之后深刻研究)
为了验证一下查到的这个结论,我改写了一下speak
方法中的代码以下。
- (void)speak { unsigned int count = 0; Ivar * ivars = class_copyIvarList([self class], &count); for (int i = 0; i < count; i ++) { Ivar ivar = ivars[i]; ptrdiff_t offSet = ivar_getOffset(ivar); const char * n = ivar_getName(ivar); NSLog(@"%@-----%ld",[NSString stringWithUTF8String:n],offSet); } NSLog(@"my name's %@", self.name); }
取到类的各个变量,而后打印出他的偏移。输出结构以下:
_name-----8
偏移了一个指针。
那为何打印出来了ViewController的地址,咱们就要研究各个变量的内存地址位置关系了。
在iewDidLoad
中变量的压栈顺序以下所示:
第一个参数self和第二个参数_cmd是隐藏参数,第三和第四个参数是执行[super viewDidLoad]
以后进栈的,以前第一题的时候咱们有了解过,super调用的方法在底层编译以后会有一个objc_super
类型的结构体。在结构体中有receiver和super_class两个变量,receiver就是self。
我在网上查过不少的资料,都是super_class比receiver(self)先入栈,不太懂为何是super_class先入。
最后是生成的obj进栈。
因此在打印self.name的时候,是obj的指针向高位偏移了一个指针,也就是self,因此打印出来的是ViewController的指针。
https://github.com/draveness/...
http://blog.sunnyxx.com/2014/...