原文地址:
https://developer.apple.com/library/mac/#documentation/Cocoa/Reference/ObjCRuntimeRef/Reference/reference.html
水平有限,如翻译有误还请指正,谢谢~
刚接触Objective-C(以下简称ObjC)的人很容易忽略它的一个特性——Rumtime(运行时)。原因是由于ObjC只需要几小时就能学会,初学者往往花费更多的时间在调整自己的程序使得其能够适应Cocoa框架的工作方式上。然而,runtime是每个人都应该了解的,至少应该知道它在一些细节上是如何工作的,比如知道
[target doMethodWith:var1];
这种代码是会被编译器翻译成
objc_msgSend(target,@selector(doMethodWith:),var1);
的。理解ObjC runtime的工作原理肯定会让你对ObjeC语言本身以及app是如何运行的有更深的理解。我想所有的Mac/iPhone的开发者,不管你是新手还是经验丰富的老将,都能从中受益。
Objective-C Runtime是开源的
ObjC Runtime一直都是开源的:http://opensource.apple.com
ObjC Runtime源码地址: http://www.opensource.apple.com/source/objc4/
动态语言 vs 静态语言
ObjC是一种由运行时导向的语言,也就是说,代码具体要做的事在运行时才会决定,编译链接阶段决定要做的则不一定真的会执行。这就给予了你很大的灵活度,比如如果你需要,你可以把一条消息重定向给一个你认为合适的对象,甚至你可以直接调换两个方法的实现,等等。这就需要用到runtime的一些功能,检查对象看看它能做什么或者不能做什么,然后将消息分发到恰当的对象上。如果我们拿C语言对比来看,在C语言中,你的代码从一个main()
方法开始,然后就是从上往下执行你所写的逻辑和方法。C语言的方式不能将一个消息转发给另一个对象。假如现在你有下面这段程序:
#include < stdio.h >
int main(int argc, const char **argv[])
{
printf("Hello World!");
return 0;
}
编译器会将你的代码优化,然后转换成汇编语言
.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!"
然后把它和库链接起来,生成可执行文件。然而类似的代码在ObjC中,由于runtime库的存在,编译器所生成的代码可能会不一样。当我们刚学ObjC的时候,可能会被告知类似的代码在ObjC中类似于(简单来说):
[self doSomethingWithVar:var1];
它会被编译器翻译成:
objc_msgSend(self,@selector(doSomethingWithVar:),var1);
但是除此之外,我们对于runtime到底做了什么就不知道了。
Objective-C Runtime到底是什么?
Objective-C Runtime是一个运行时类库,主要是用C语言和汇编写的,它是在C语言的基础上增加了面向对象的能力,从而创造了ObjC。加载类信息,分发和转发所有的方法,等等都是它的工作。本质上来说,就是因为有了runtime,才使得用ObjC实现面向对象编程成为了可能。
Objective-C Runtime 术语
进一步深入了解runtime之前,让我们先看一些术语,这样我才能确定你听得懂我说的是什么。
有2种runtime:现代的runtime(The Modern Runtime)和老的runtime(the Legacy Runtime)。现代的runtime覆盖了所有64位的Mac OS X应用程序和所有iPhone应用程序。而老的runtime只覆盖了所有32位Mac OS X应用程序。
有2种基本类型的方法:实例方法(以-开头的比如
-(void)dofoo;
)和类方法(以+开头的比如+(id)alloc
),方法就类似于C语言中的函数,是一段代码的集合,用来执行一个小任务,像下面这个:
-(NSString *)movieTitle
{
return @"Futurama: Into the Wild Green Yonder";
}
- Selector(方法选择器)
ObjC中的Selector实际上是一个结构体,用来标识你想执行的方法。在runtime中,它被定义为:
typedef struct objc_selector *SEL;
使用方式如下:
SEL aSel = @selector(movieTitle);
- Message(消息)
[target getMovieTitleForObject:obj];
ObjC里的消息就是包括在两个中括号[]之间的东西,它包含了接受这个消息的目标,你想要执行的方法,以及方法中传递的参数。和C语言中的函数相似,但是调用方式不一样。实际上你给一个对象发送的方法不一定会被执行,因为接受到消息的对象可能会检查一下消息的发送来源,然后决定是否执行另一个方法或者把这个接收到的消息转发给另一个对象。
- Class(类)
如果你在runtime中寻找类的定义的话,将会看到下面的代码:
typedef struct objc_class *Class;
typedef struct objc_object {
Class isa;
} *id;
这里有一个ObjC类的结构体,和ObjC对象的结构体。所有的ObjC对象都有一个定义为isa的类指针,这就是我们通常所说的isa指针
。运行时就是用这个isa指针来检查对象属于哪个类,在发送消息时查看对象是否能响应这个消息。
然后,我们再看id指针。id指针用来指向一个ObjC对象,当你有一个id指针,你可以获取它的类,查看其是否能够响应某个方法,如果你已经知道了这是哪个对象,还可以做一些更具体的操作。
你也可以在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
};
Blocks被设计成了能够与ObjC runtime兼容,可以把它们当做对象来看待,它们也能够响应消息,比如-retain
,-release
,-copy
,等等。
- IMP(Method Implementations 方法实现)
typedef id (*IMP)(id self,SEL _cmd,...);
IMP是一个指向方法实现的函数指针,如果你是一个初学者,你不需要直接去处理它,编译器会帮你自动生成。后面我将会说到运行时如何调用方法的,其实执行的都是这些函数指针指向的方法实现。
- ObjC Class
说这么多,到底ObjC类长啥样子呢?一个最基本的ObjC类看起来像这样:
@interface MyClass : NSObject {
//vars
NSInteger counter;
}
//methods
-(void)doFoo;
@end
runtime中能得到更多的信息:
#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),协议列表等信息。当在处理消息的时候,这些信息都是runtime所需要的。
所以类可以定义对象,然后它自己也是个对象?这是怎么回事?
是的,前面我说过在ObjC中,类本身也是一个对象,为了处理这些,runtime创造了元类(Meta Classes)。当你发送一个类似于[NSObject alloc]
的方法时,你就是在给一个类对象发送消息。每个类都是一个元类的对象,元类又是根元类(root meta class)的对象。就像你从NSObject类继承了一个子类,这个类就会指向NSObject把它当做父类一样,所有的元类都指向根元类并把其作为自己的父类。所有的元类只是简单的保存了他的类对象方法列表中的方法,当你发送一个类方法消息时(比如[NSObject alloc]
,它会被编译器翻译成objc_msgSend()
),会在元类保存的方法列表中查找是否有响应该消息的方法,如有找到了,就会在对应的类对象上执行。
为什么我们要继承苹果的类
当你刚接触Cocoa开发的时候,所有的教程都会告诉你需要去继承苹果的类然后再开始写代码,你也应该或多或少的感受到了这样做的好处。但是有一件事你不知道的是,当你继承苹果的类时,你的类会创建成适应ObjC runtime工作方式的类。在给我们自己的类创建实例的时候,我们大概会这么做:
MyObject *object = [[MyObject alloc] init];
第一个被执行的方法是+alloc
。苹果的官方文档里说道
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。
所以继承自苹果的类不仅仅是继承了一些重要属性,还继承了在内存中轻松分配创建我们的对象的能力,并且创建出来的对象的结构符合runtime的期望(有一个isa指针指向我们的类)。
那什么是Class Cache呢?(objc_cache *cache)
当runtime沿着对象的isa指针进行检索时,有时可能会发现一个对象实现了很多个方法。但是你可能只需要调用其中的几个方法,如果每次调用都在所有的方法列表中查找一遍的话,那太费劲了。Class Cache就是为了解决这个问题而诞生的,当你在一个个类的方法列表中找到了你要调用的方法,这个方法就会被放入Class Cache中,以便下次调用。objc_msgSend()
在查找一个类的方法时也会优先从Class Cache中找。这都建立在这个理论上:如果你调用了一个类的方法一次,那么之后你很有可能再调用这个方法。知道了这点,我们来分析一下下面这段代码发生了什么:
MyObject *obj = [[MyObject alloc] init];
@implementation MyObject
-(id)init {
if(self = [super init]){
[self setVarA:@”blah”];
}
return self;
}
@end
这段代码是这样执行的:
-
[MyObject alloc]
方法首先被执行。MyObject类并没有实现+alloc
方法,所以我们在MyObject类中查找这个方法会找不到。 - 然后顺着MyObject类的父类指针,在其父类NSObject中找到了
+alloc
方法。+alloc
方法检查到消息的接受者是MyObject
类,会根据类的大小分配一块内存区域,使其isa指针指向MyObject类,并把+alloc
方法放入NSObject的Class Cache中。于是我们就得到了一个MyObject类的实例对象。 - 然后
-init
方法被执行,MyObejct类实现了这个方法,方法被执行,然后被加入MyObject的Class Cache。 - 然后是
self = [super init]
,super
是一个关键字,指向对象的父类,所以就是调用NSObject的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;
}
如果你是一个初学者,你猜测的输出结果可能会像这样:
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
方法返回另一个类的对象。
那么objc_msgSend中究竟发生了什么?
在objc_msgSend确实发什么了很多事情,假如我们有这样一段代码:
[self printMessageWithString:@"Hello World!"];
它会被编译器翻译成这样:
objc_msgSend(self,@selector(printMessageWithString:),@"Hello World!");
这个方法根据self对象的isa指针去查找这个对象的类或者父类能否响应这个方法@selector(printMessageWithString:)
。假如在类的方法列表或者Class Cache中找到了该方法,就根据方法的函数指针找到函数的实现并执行它。但是objc_msgSend()
并不会返回,它被执行后就会根据指针找到响应方法并执行,方法返回了看起来就像是objc_msgSend()
返回了。关于这个过程, Bill Bumgarner 在Part1,Part2,Part3中进行了详细的讲解。大概意思就是:
- 检查是否有可以忽略的方法,比如在垃圾回收的环境下我们可以忽略
-retain
,-release
等方法。 - 检查目标对象是否为nil。不像其他语言,在ObjectC中给一个nil对象发消息是完全可以的,并有时你会因为某些原因希望这么去做,假设我们有一个非nil的目标对象,然后我们继续...
- 然后我们需要在目标类中查找IMP,首先在类的Class Cache中查找,如果找到了就顺着指针找到要执行的方法。
- 如果在Class Cache中没有找到IMP,那么就到类的方法列表中查找,如果找到了就跳到对应方法执行。
- 如果上面2个地方都没有找到IMP,那么就启动消息转发机制,这就意味着,你的方法会被编译器转换成C函数。比如你的方法是下面这个:
-(int)doComputeWithNum:(int)aNum
将会被转换成这样:
int aClass_doComputeWithNum(aClass *self,SEL _cmd,int aNum)
ObjC runtime通过调用函数指针来调用这些方法,你不能直接调用这些转换后的方法,但是Cocoa框架提供了一个方法去获得方法的指针
//declare C function pointer
int (computeNum *)(id,SEL,int);
//methodForSelector is COCOA & not ObjC Runtime
//gets the same function pointer objc_msgSend gets
computeNum = (int (*)(id,SEL,int))[target methodForSelector:@selector(doComputeWithNum:)];
//execute the C function pointer returned by the runtime
computeNum(obj,@selector(doComputeWithNum:),aNum);
通过这种方式,你可以直接在runtime时调用某个方法。如果你想确保某个方法一定会执行,你甚至可以用这种方式绕过runtime的动态特性。在ObjC中也是这么调用方法的,只不过用的是objc_msgSend()
。
ObjC消息转发
在ObjC中,允许发消息给一个不能响应该消息的对象(有可能是苹果故意这么设计的)。苹果在他的文档中给出的理由是为了模拟实现多重继承机制(ObjC原生是不支持这种机制的),或者你希望把你的设计抽象化,把具体的实现隐藏起来。消息转发是runtime一个非常重要的能力。它的工作方式大概是这样:
- runtime在你的类及其父类的的Class Cache和方法分发表中都没有找到目标方法。
- 然后runtime将会调用你的类的
+ (BOOL) resolveInstanceMethod:(SEL)aSEL
方法。这就给了你一个机会去提供一个方法的实现并告诉runtime这个方法是可用的,如果runtime开始查找方法就可以找到你提供的这个方法。你可以这样做,定义一个方法:
void fooMethod(id obj, SEL _cmd)
{
NSLog(@"Doing Foo");
}
用class_addMethod()
将其添加到类的方法列表中
+(BOOL)resolveInstanceMethod:(SEL)aSEL
{
if(aSEL == @selector(doFoo:)){
class_addMethod([self class],aSEL,(IMP)fooMethod,"v@:");
return YES;
}
return [super resolveInstanceMethod];
}
class_addMethod()
最后一个参数"v@:"是前面指定方法的返回值和参数。你可以在这篇文档中查看这个参数可以放哪些值。
- 如果第2步没能解决问题,那么runtime将会调用
- (id)forwardingTargetForSelector:(SEL)aSelector
方法,这就再次给了我们机会去告诉runtime将消息转发给另一个对象来处理。最好在这一步之前对消息做处理,因为下一步的开销较大,你可以这么做:
- (id)forwardingTargetForSelector:(SEL)aSelector
{
if(aSelector == @selector(mysteriousMethod:)){
return alternateObject;
}
return [super forwardingTargetForSelector:aSelector];
}
当然你不能在这个方法中返回self,因为这样会造成死循环。
- 如果在上一步时没做处理,那么runtime将会调用
- (void)forwardInvocation:(NSInvocation *)anInvocation
最后一次尝试给目标对象发送消息。NSInvocation本质上是一条ObjC消息封装成的对象,只要你拿到了一个NSInvocation,你就可以改变一条消息的任何值,比如目标对象,方法选择器或者参数等等,你可以想这样做:
-(void)forwardInvocation:(NSInvocation *)invocation
{
SEL invSEL = invocation.selector;
if([altObject respondsToSelector:invSEL]) {
[invocation invokeWithTarget:altObject];
} else {
[self doesNotRecognizeSelector:invSEL];
}
}
如果你的类是继承自NSObject,那么它的- (void)forwardInvocation:(NSInvocation *)anInvocation
方法的默认实现只是简单的调用了-doesNotRecognizeSelector:
方法,如果你想最后一次对这条消息做处理,可以重写这个方法。
健壮的实例变量(Modern Runtime)
我们最近在现代runtime中得到的是健壮实例变量(Non Fragile ivars)的概念。编译时,我们定义的变量是以在类中的偏移地址访问的,而且这些工作编译器能自动帮我们完成,这牵扯到底层的细节,大致类似于:先得到一个指针指向创建的对象,然后基于该对象的起始地址,再根据变量的偏移地址我们就可以访问到变量,最后根据变量的类型确定变量所占的内存空间,所以编译后变量的输出形式(ivar layout)类似于下边的表格,左边一列数字代表偏移地址:
我们再NSObject类中有一些变量,然后我们创建一个类继承它,并在这个类中添加一些新的成员变量。这样做是没问题的,直到苹果发布了Mac OS X 10.x,就发生了下面的情况:
我们的子类中的成员变量被抹掉了,因为在父类中的相同位置也存在成员变量。唯一的解决办法就是苹果恢复到以前的布局方式,但是如果这样做的话,苹果的框架就会变得很不先进,因为成员变量的位置布局是固定死的。在不健壮的变量下,你不得不重新编译你的类,使得其恢复兼容性。那么健壮的成员变量下又是怎样呢?
变量的的布局和非健壮变量情况下是一样的,所以父类和子类中也会有重叠覆盖的成员变量,但是当runtime检测到有覆盖的变量时,它会调整子类中新增变量的位置,使得其不被覆盖。
ObjC关联对象
关联引用(Associated References)是最近被引入 Mac OS X 10.6的一项特性。与某些其他语言不同,ObjC不支持动态的给一个类添加变量。之前在ObjC中你想"假装"给一个类动态的添加变量是需要花很多功夫的,关联引用的引入就是为了解决这个问题,如果你想给任何一个已经存在的类添加变量你可以这样做,比如NSView:
#import < Cocoa/Cocoa.h> //Cocoa
#include < objc/runtime.h> //objc runtime api’s
@interface NSView (CustomAdditions)
@property(retain) NSImage *customImage;
@end
@implementation NSView (CustomAdditions)
static char img_key; //has a unique address (identifier)
-(NSImage *)customImage
{
return objc_getAssociatedObject(self,&img_key);
}
-(void)setCustomImage:(NSImage *)image
{
objc_setAssociatedObject(self,&img_key,image,
OBJC_ASSOCIATION_RETAIN);
}
@end
你可以在runtime.h
头文件中查看怎么存储通过objc_setAssociatedObject()
方法设置的变量。
/* Associated Object support. */
/* objc_setAssociatedObject() options */
enum {
OBJC_ASSOCIATION_ASSIGN = 0,
OBJC_ASSOCIATION_RETAIN_NONATOMIC = 1,
OBJC_ASSOCIATION_COPY_NONATOMIC = 3,
OBJC_ASSOCIATION_RETAIN = 01401,
OBJC_ASSOCIATION_COPY = 01403
};
这和你用@property
设置变量时的做法是一样的。
Hybrid vTable Dispatch
如果你看过现代runtime的源码你可能会遇到下面这一段话(在 objc-runtime-new.m中)
/***********************************************************************
* vtable dispatch
*
* Every class gets a vtable pointer. The vtable is an array of IMPs.
* The selectors represented in the vtable are the same for all classes
* (i.e. no class has a bigger or smaller vtable).
* Each vtable index has an associated trampoline which dispatches to
* the IMP at that index for the receiver class's vtable (after
* checking for NULL). Dispatch fixup uses these trampolines instead
* of objc_msgSend.
* Fragility: The vtable size and list of selectors is chosen at launch
* time. No compiler-generated code depends on any particular vtable
* configuration, or even the use of vtable dispatch at all.
* Memory size: If a class's vtable is identical to its superclass's
* (i.e. the class overrides none of the vtable selectors), then
* the class points directly to its superclass's vtable. This means
* selectors to be included in the vtable should be chosen so they are
* (1) frequently called, but (2) not too frequently overridden. In
* particular, -dealloc is a bad choice.
* Forwarding: If a class doesn't implement some vtable selector, that
* selector's IMP is set to objc_msgSend in that class's vtable.
* +initialize: Each class keeps the default vtable (which always
* redirects to objc_msgSend) until its +initialize is completed.
* Otherwise, the first message to a class could be a vtable dispatch,
* and the vtable trampoline doesn't include +initialize checking.
* Changes: Categories, addMethod, and setImplementation all force vtable
* reconstruction for the class and all of its subclasses, if the
* vtable selectors are affected.
**********************************************************************/
大概的意思就是,runtime会试着把最常调用的那些方法放在这个vtable中,这样会加速你app的运行速度,因为调用在这个表中的方法用的指令比objc_msgSend
少。这个vtable表由全局16个最常调用的方法组成,再往下看,你能看到自动垃圾回收和没有自动垃圾回收环境下的默认方法:
static const char * const defaultVtable[] = {
"allocWithZone:",
"alloc",
"class",
"self",
"isKindOfClass:",
"respondsToSelector:",
"isFlipped",
"length",
"objectForKey:",
"count",
"objectAtIndex:",
"isEqualToString:",
"isEqual:",
"retain",
"release",
"autorelease",
};
static const char * const defaultVtableGC[] = {
"allocWithZone:",
"alloc",
"class",
"self",
"isKindOfClass:",
"respondsToSelector:",
"isFlipped",
"length",
"objectForKey:",
"count",
"objectAtIndex:",
"isEqualToString:",
"isEqual:",
"hash",
"addObject:",
"countByEnumeratingWithState:objects:count:",
};
那你怎么知道你调用了这个vtable中的方法呢?调试的时候,你能在方法调用栈中看到一些方法:
-
objc_msgSend_fixup
代表你调用了一个正准备加入vtable的方法 -
objc_msgSend_fixedup
代表你调用的方法原先在vtable中,现在不在了 -
objc_msgSend_vtable[0-15]
代表你调用了vtable中的某一个方法,后面的数字就是方法在table中的序号
runtime可以随意增加或者删除vtable中的方法,所以一次运行过程中objc_msgSend_vtable10
对应着-length
方法,下一次运行时就不一定了。
总结
希望你能喜欢这篇文章,这是我在Des Moines Cocoaheads
演讲时的内容。ObjC runtime是一项浩大的工程,它给我么你的Cocoa/ObjC应用提供了动力,并且使很多强大的特性变成了可能。如果你还没有看过苹果的官方文档 Objective-C Runtime Programming Guide,Objective-C Runtime Reference,我希望你能看一遍。谢谢!