初学 Objective-C(如下简称ObjC) 的人很容易忽略一个 ObjC 特性 —— ObjC Runtime。这是由于这门语言很容易上手,几个小时就能学会怎么使用,因此程序员们每每会把时间都花在了解 Cocoa 框架以及调整本身的程序的表现上。然而 Runtime 应该是每个 ObjC 都应该要了解的东西,至少要理解编译器会把html
[target doMethodWith:var1];
编译成:程序员
objc_msgSend(target,@selector(doMethodWith:),var1);
这样的语句。理解 ObjC Runtime 的工做原理,有助于你更深刻地去理解 ObjC 这门语言,理解你的 App 是怎样跑起来的。我想全部的 Mac/iPhone 开发者,不管水平如何,都会从中获益的。数组
ObjC Runtime 的代码是开源的,能够从这个站点下载: opensource.apple.com。缓存
这个是全部开源代码的连接: http://www.opensource.apple.com/source/数据结构
这个是ObjC rumtime 的源代码: http://www.opensource.apple.com/source/objc4/
4应该表明的是build版本而不是语言版本,如今是ObjC 2.0app
ObjC 是一种面向runtime(运行时)的语言,也就是说,它会尽量地把代码执行的决策从编译和连接的时候,推迟到运行时。这给程序员写代码带来很大的灵活性,好比说你能够把消息转发给你想要的对象,或者随意交换一个方法的实现之类的。这就要求 runtime 能检测一个对象是否能对一个方法进行响应,而后再把这个方法分发到对应的对象去。咱们拿 C 来跟 ObjC 对比一下。在 C 语言里面,一切从 main 函数开始,程序员写代码的时候是自上而下地,一个 C 的结构体或者说类吧,是不能把方法调用转发给其余对象的。举个栗子:框架
#include < stdio.h > int main(int argc, const char **argv[]) { printf("Hello World!"); return 0; }
这段代码被编译器解析,优化后,会变成一堆汇编代码:ide
.text .align 4,0x90 .globl _main _main: Leh_func_begin1: pushq %rbp Llabel1: movq %rsp, %rbp Llabel2: subq $16, %rsp Llabel3: movq %rsi, %rax movl %edi, %ecx movl %ecx, -8(%rbp) movq %rax, -16(%rbp) xorb %al, %al leaq LC(%rip), %rcx movq %rcx, %rdi call _printf movl $0, -4(%rbp) movl -4(%rbp), %eax addq $16, %rsp popq %rbp ret Leh_func_end1: .cstring LC: .asciz "Hello World!"
而后,再连接 include 的库,完了生成可执行代码。对比一下 ObjC,当咱们初学这门语言的时候教程是这么说滴:用中括号括起来的语句,函数
[self doSomethingWithVar:var1];
被编译器编译以后会变成:优化
objc_msgSend(self,@selector(doSomethingWithVar:),var1);
一个 C 方法,传入了三个变量,self指针,要执行的方法 @selector(doSomethingWithVar:) 还有一个参数 var1。可是在这以后就不晓得发生什么了。
ObjC Runtime 实际上是一个 Runtime 库,基本上用 C 和汇编写的,这个库使得 C 语言有了面向对象的能力(脑中浮现当你乔帮主参观了施乐帕克的 SmallTalk 以后嘴角一抹浅笑)。这个库作的事前就是加载类的信息,进行方法的分发和转发之类的。
再往下深谈以前咱先介绍几个术语。
目前说来Runtime有两种,一个 Modern Runtime 和一个 Legacy Runtime。Modern Runtime 覆盖了64位的Mac OS X Apps,还有 iOS Apps,Legacy Runtime 是早期用来给32位 Mac OS X Apps 用的,也就是能够不用管就是了。
一种 Instance Method,还有 Class Method。instance method 就是带“-”号的,须要实例化才能用的,如 :
-(void)doFoo; [aObj doFoot];
Class Method 就是带“+”号的,相似于静态方法能够直接调用:
+(id)alloc; [ClassName alloc];
这些方法跟 C 函数同样,就是一组代码,完成一个比较小的任务。
-(NSString *)movieTitle { return @"Futurama: Into the Wild Green Yonder"; }
一个 Selector 事实上是一个 C 的结构体,表示的是一个方法。定义是:
typedef struct objc_selector *SEL;
使用起来就是:
SEL aSel = @selector(movieTitle);
这样能够直接取一个selector,若是是传递消息(相似于C的方法调用)就是:
[target getMovieTitleForObject:obj];
在 ObjC 里面,用’[]‘括起来的表达式就是一个消息。包括了一个 target,就是要接收消息的对象,一个要被调用的方法还有一些你要传递的参数。相似于 C 函数的调用,可是又有所不一样。事实上上面这个语句你仅仅是传递了 ObjC 消息,并不表明它就会必定被执行。target 这个对象会检测是谁发起的这个请求,而后决策是要执行这个方法仍是其余方法,或者转发给其余的对象。
Class 的定义是这样的:
typedef struct objc_class *Class; typedef struct objc_object { Class isa; } *id;
咱们能够看到这里这里有两个结构体,一个类结构体一个对象结构体。全部的 objc_object 对象结构体都有一个 isa 指针,这个 isa 指向它所属的类,在运行时就靠这个指针来检测这个对象是否能够响应一个 selector。完了咱们看到最后有一个 id 指针。这个指针其实就只是用来表明一个 ObjC 对象,有点相似于 C++ 的泛型。当你拿到一个 id 指针以后,就能够获取这个对象的类,而且能够检测其是否响应一个 selector。这就是对一个 delegate 经常使用的调用方式啦。这样说还有点抽象,咱们看看 LLVM/Clang 的文档对 Blocks 的定义:
struct Block_literal_1 { void *isa; // initialized to &_NSConcreteStackBlock or &_NSConcreteGlobalBlock int flags; int reserved; void (*invoke)(void *, ...); struct Block_descriptor_1 { unsigned long int reserved; // NULL unsigned long int size; // sizeof(struct Block_literal_1) // optional helper functions void (*copy_helper)(void *dst, void *src); void (*dispose_helper)(void *src); } *descriptor; // imported variables };
能够看到一个 block 是被设计成一个对象的,拥有一个 isa 指针,因此你能够对一个 block 使用 retain, release, copy 这些方法。
接下来看看啥是IMP。
typedef id (*IMP)(id self,SEL _cmd,...);
一个 IMP 就是一个函数指针,这是由编译器生成的,当你发起一个 ObjC 消息以后,最终它会执行的那个代码,就是由这个函数指针指定的。
OK,回过头来看看一个 ObjC 的类。举一个栗子:
@interface MyClass : NSObject { //vars NSInteger counter; } //methods -(void)doFoo; @end
定义一个类咱们能够写成如上代码,而在运行时,一个类就不只仅是上面看到的这些东西了:
#if !__OBJC2__ Class super_class OBJC2_UNAVAILABLE; const char *name OBJC2_UNAVAILABLE; long version OBJC2_UNAVAILABLE; long info OBJC2_UNAVAILABLE; long instance_size OBJC2_UNAVAILABLE; struct objc_ivar_list *ivars OBJC2_UNAVAILABLE; struct objc_method_list **methodLists OBJC2_UNAVAILABLE; struct objc_cache *cache OBJC2_UNAVAILABLE; struct objc_protocol_list *protocols OBJC2_UNAVAILABLE; #endif
能够看到运行时一个类还关联了它的父类指针,类名,成员变量,方法,cache 还有附属的 protocol。
上面我提到过一个 ObjC 类同时也是一个对象,为了处理类和对象的关系,runtime 库建立了一种叫作 标签类 元类(Meta Class)的东西。当你发出一个消息的时候,比方说
[NSObject alloc];
你事实上是把这个消息发给了一个类对象(Class Object),这个类对象必须是一个 Meta Class 的实例,而这个 Meta Class 同时也是一个根 MetaClass 的实例。当你继承了 NSObject 成为其子类的时候,你的类指针就会指向 NSObject 为其父类。可是 Meta Class 不太同样,全部的 Meta Class 都指向根 Meta Class 为其父类。一个 Meta Class 持有全部能响应的方法。因此当 [NSObject alloc] 这条消息发出的时候,objc_msgSend() 这个方法会去 NSObject 它的 Meta Class 里面去查找是否有响应这个 selector 的方法,而后对 NSObject 这个类对象执行方法调用。
初学 Cocoa 开发的时候,多数教程都要咱们继承一个类比方 NSObject,而后咱们就开始 Coding 了。比方说:
MyObject *object = [[MyObject alloc] init];
这个语句用来初始化一个实例,相似于 C++ 的 new 关键字。这个语句首先会执行 MyObject 这个类的 +alloc 方法,Apple 的官方文档是这样说的:
The isa instance variable of the new instance is initialized to a data structure that describes the class; memory for all other instance variables is set to 0.
新建的实例中,isa 成员变量会变初始化成一个数据结构体,用来描述所指向的类。其余的成员变量的内存会被置为0.
因此继承 Apple 的类咱们不只是得到了不少很好用的属性,并且也继承了这种内存分配的方法。
刚刚咱们看到 runtime 里面有一个指针叫 objc_cache *cache,这是用来缓存方法调用的。如今咱们知道一个实例对象被传递一个消息的时候,它会根据 isa 指针去查找可以响应这个消息的对象。可是实际上咱们在用的时候,只有一部分方法是经常使用的,不少方法其实不多用或者根本用不到。好比一个object你可能历来都不用copy方法,那我要是每次调用的时候还去遍历一遍全部的方法那就太笨了。因而 cache 就应运而生了,每次你调用过一个方法,以后,这个方法就会被存到这个 cache 列表里面去,下次调用的时候 runtime 会优先去 cache 里面查找,提升了调用的效率。举一个栗子:
MyObject *obj = [[MyObject alloc] init]; // MyObject 的父类是 NSObject @implementation MyObject -(id)init { if(self = [super init]){ [self setVarA:@”blah”]; } return self; } @end
这段代码是这样执行的:
OK,这就是一个很简单的初始化过程,在 NSObject 类里面,alloc 和 init 没作什么特别重大的事情,可是,ObjC 特性容许你的 alloc 和 init 返回的值不一样,也就是说,你能够在你的 init 函数里面作一些很复杂的初始化操做,可是返回出去一个简单的对象,这就隐藏了类的复杂性。再举个栗子:
#import < Foundation/Foundation.h> @interface MyObject : NSObject { NSString *aString; } @property(retain) NSString *aString; @end @implementation MyObject -(id)init { if (self = [super init]) { [self setAString:nil]; } return self; } @synthesize aString; @end int main (int argc, const char * argv[]) { NSAutoreleasePool * pool = [[NSAutoreleasePool alloc] init]; id obj1 = [NSMutableArray alloc]; id obj2 = [[NSMutableArray alloc] init]; id obj3 = [NSArray alloc]; id obj4 = [[NSArray alloc] initWithObjects:@"Hello",nil]; NSLog(@"obj1 class is %@",NSStringFromClass([obj1 class])); NSLog(@"obj2 class is %@",NSStringFromClass([obj2 class])); NSLog(@"obj3 class is %@",NSStringFromClass([obj3 class])); NSLog(@"obj4 class is %@",NSStringFromClass([obj4 class])); id obj5 = [MyObject alloc]; id obj6 = [[MyObject alloc] init]; NSLog(@"obj5 class is %@",NSStringFromClass([obj5 class])); NSLog(@"obj6 class is %@",NSStringFromClass([obj6 class])); [pool drain]; return 0; }
若是你是ObjC的初学者,那么你极可能会认为这段代码执的输出会是:
NSMutableArray NSMutableArray NSArray NSArray MyObject MyObject
但事实上是这样的:
obj1 class is __NSPlaceholderArray obj2 class is NSCFArray obj3 class is __NSPlaceholderArray obj4 class is NSCFArray obj5 class is MyObject obj6 class is MyObject
这是由于 ObjC 是容许运行 +alloc 返回一个特定的类,而 init 方法又返回一个不一样的类的。能够看到 NSMutableArray 是对普通数组的封装,内部实现是复杂的,可是对外隐藏了复杂性。
这个方法作的事情很多,举个栗子:
[self printMessageWithString:@"Hello World!"];
这句语句被编译成这样:
objc_msgSend(self,@selector(printMessageWithString:),@"Hello World!");
这个方法先去查找 self 这个对象或者其父类是否响应 @selector(printMessageWithString:),若是从这个类的方法分发表或者 cache 里面找到了,就调用它对应的函数指针。若是找不到,那就会执行一些其余的东西。步骤以下:
在编译的时候,你定义的方法好比:
-(int)doComputeWithNum:(int)aNum
会编译成:
int aClass_doComputeWithNum(aClass *self,SEL _cmd,int aNum)
而后由 runtime 去调用指向你的这个方法的函数指针。那么以前咱们说你发起消息其实不是对方法的直接调用,其实 Cocoa 仍是提供了能够直接调用的方法的:
// 首先定义一个 C 语言的函数指针 int (computeNum *)(id,SEL,int); // 使用 methodForSelector 方法获取对应与该 selector 的杉树指针,跟 objc_msgSend 方法拿到的是同样的 // **methodForSelector 这个方法是 Cocoa 提供的,不是 ObjC runtime 库提供的** computeNum = (int (*)(id,SEL,int))[target methodForSelector:@selector(doComputeWithNum:)]; // 如今能够直接调用该函数了,跟调用 C 函数是同样的 computeNum(obj,@selector(doComputeWithNum:),aNum);
若是你须要的话,你能够经过这种方式你来确保这个方法必定会被调用。
在 ObjC 这门语言中,发送消息给一个并不响应这个方法的对象,是合法的,应该也是故意这么设计的。换句话说,我能够对任意一个对象传递任意一个消息(看起来有点像对任意一个类调用任意一个方法,固然事实上不是),固然若是最后找不到能调用的方法就会 Crash 掉。
Apple 设计这种机制的缘由之一就是——用来模拟多重继承(ObjC 原生是不支持多重继承的)。或者你但愿把你的复杂设计隐藏起来。这种转发机制是 Runtime 很是重要的一个特性,大概的步骤以下:
这就给了程序员一次机会,能够告诉 runtime 在找不到改方法的状况下执行什么方法。举个栗子,先定义一个函数:
void fooMethod(id obj, SEL _cmd) { NSLog(@"Doing Foo"); }
完了重载 resolveInstanceMethod 方法:
+(BOOL)resolveInstanceMethod:(SEL)aSEL { if(aSEL == @selector(doFoo:)){ class_addMethod([self class],aSEL,(IMP)fooMethod,"v@:"); return YES; } return [super resolveInstanceMethod]; }
其中 “v@:” 表示返回值和参数,这个符号涉及 Type Encoding,能够参考Apple的文档 ObjC Runtime Guide。
接下来 Runtime 会调用 – (id)forwardingTargetForSelector:(SEL)aSelector 方法。
这就给了程序员第二次机会,若是你没办法在本身的类里面找到替代方法,你就重载这个方法,而后把消息转给其余的Object。
- (id)forwardingTargetForSelector:(SEL)aSelector { if(aSelector == @selector(mysteriousMethod:)){ return alternateObject; } return [super forwardingTargetForSelector:aSelector]; }
这样你就能够把消息转给别人了。固然这里你不能 return self,否则就死循环了=.=
-(void)forwardInvocation:(NSInvocation *)invocation { SEL invSEL = invocation.selector; if([altObject respondsToSelector:invSEL]) { [invocation invokeWithTarget:altObject]; } else { [self doesNotRecognizeSelector:invSEL]; } }
默认状况下 NSObject 对 forwardInvocation 的实现就是简单地执行 -doesNotRecognizeSelector: 这个方法,因此若是你想真正的在最后关头去转发消息你能够重载这个方法(好折腾-.-)。
原文后面介绍了 Non Fragile ivars (Modern Runtime), Objective-C Associated Objects 和 Hybrid vTable Dispatch。鉴于一是底层的能够不用理会,一是早司空见惯的不用详谈,还有一个是很简单的,就是一个创建在方法分发表里面填入默认经常使用的 method,因此有兴趣的读者能够自行查阅原文,这里就不详谈鸟。