理解 Objecive-C Runtime
本文翻译自 http://cocoasamurai.blogspot.jp/2010/01/understanding-objective-c-runtime.html
开始学习Cocoa/Objective-C时,Objective-C Runtime是一个容易被人们忽视的特性。原因是Objective-C(语言)更容易在数小时内入门,Cocoa的初学者会花费大量的时间在Cocoa Framework上并学习它的工作原理。但是,关于runtime除了知道编译器会把代码[target doMethodWith:var1];
转换为objec_msgSend(target, @selector(doMethodWith:), var1);
之外,应该了解更多的细节。了解了Objective—C的运行时在做什么有助于你更深入地理解Objective-C本身以及你的app是如何运行的。我认为Mac/iPhone开发者从本文中会收获一些东西,不论你的水平经验如何。
Objective-C Runtime 是开源的
Objective-C Runtime 是开源的,其开源地址是:http://opensource.apple.com。事实上,我会首选研究Objective-C Rumtime的源码来弄明白该语言的原理,而不是去查看与它相关的Apple文档。你可以在此处下载到Runtime源码。
Dynamic vs Static Languages
Objective-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!"
然后将它和一个库链接并且将其变为可执行的。对比Objective-C,虽然过程是类似的,但是编译器生成的代码依赖于Objective-C运行时库的存在。当开始学习Objective-C时,我们被告知(过分简单地)Objective-C方括号内的代码:
[self doSomethingWithVar:var1];
被转换成
objc_msgSend(self, @selector(doSomethingWithVar:), var1);
但除此之外,运行时之后在做什么我们并不清楚。
Objective-C的运行时是什么?
Objective-C 的运行时是一个Runtime库,它主要是由C语言和汇编语言所编写而成,为 C语言添加了面向对象(Object Oriented)的能力,从而产生了Objective-C。它会加载类信息,所有的方法发生,方法转发等等。Objective-C的运行时本质上是创建了所有使它面向对象编程成为可能的支持结构。
Objective-C 运行时术语
在继续深入之前,让我们对一些术语进行统一阐述。
2 Runtimes
就Mac和iPhone的开发者而言,有两种运行时:Modern Runtime和Legacy Runtime。Modern Runtime包含了所有64位的Mac OS X
应用和所有iPhone OS
应用。Legacy Runtime包含了剩下的(所有32位Mac OS X
应用)。
Method
有两种基本类型的方法。实例方法(Instance Methods),以对象实例操作的方法,‘-’开始,如-(void)doFoo;
。类方法(Class Methods),以类操作的方法,’+’开始,如+(id)alloc;
。方法和C语言的函数类似,它们是一小组执行任务的代码:
- (NSString *)movieTitle
{
return @"Futurama: Into the Wild Green Yonder";
}
Selector
Objective-C中的一个selector本质上就是一个C语言的数据结构,作为一种方式来辨识你想让对象去执行的Objective-C的方法。在运行时它是这样定义的:
typedef struct objc_selector *SEL;
像这样使用:
SEL aSel = @selector(movieTitle);
Message
[target getMovieTitleForObject:obj];
Objective-C的Message是介于两个方括号’[]’之间的任何的东西,包含了给之发送消息的目标(target
),想要它去执行的方法(getMovieTitle:
)以及你想要发送给它的任何参数(obj
)。Objective-C的消息发送虽然和C语言的函数调用类似,但实质不同。事实上,给对象发送消息并不意味着它会执行该条消息。对象会检查消息发送者是谁然后基于此决定执行不同的方法或转发此条消息给其他的目标对象。
Class
如果查看一个类的运行时,你会看到这个:
typedef struct objc_class *Class;
typedef struct objc_object {
Class isa;
} *id;
可以看到有一个Objective-C类的结构体和一个对象的结构体。具有objc_object
的都是定义为isa
的类指针,这就是我们所谓的“isa指针”。这个isa
指针是所有Objective-C Runtime所需要的来检查对象的类是什么,然后当你在发送消息给对象时开始查看它是否对selectors有响应。最后的id
指针,默认情况下不会告诉我们关于Objective-C对象的信息,除非它们是Objective-C的对象。当有一个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与Objecive-C运行时是兼容的,因此它们被当作对象一样能够对诸如-retain
,-release
,-copy
等这样的消息有响应。
IMP(Method Implementations)
typedef id (*IMP)(id self, SEL _cmd, ...);
IMP是指向编译器为你生成的方法实现的函数指针。如果是刚开始学习Objective-C,你没必要接触这些,但这是我们即将要讲到的Objective-C运行时如何调用你的方法的内容。
Objective-C 类
Objective-C的类中到底是什么?Objective-C的类基本的实现是这样的
@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
可以看到,一个类引用了其父类,它的名字,实例变量,方法,缓存以及它声明的协议。当对发送给你的类或其实例消息有响应时运行时需要这样的信息。
那么是类定义了对象还是对象本身?这是如何实现的
早先我说过,Objective-C的类本身也是对象,运行时是通过创建元类(Meta Class)来处理这个问题。当发送了一条这样的消息[NSObject alloc]
时实际上是将消息发送给了类对象(class object),而类对象需要成为一个元类的实例,而元类本身又是根元类(root meta class)的实例。假如你有一个继承自NSObject
的类,你的类会指向NSObject
来作为自己的父类。但是所有元类都是指向根元类作为自己的父类。所有的元类可响应的消息方法列表中只有类方法。因此当给类对象发送如[NSObject alloc]
的消息时,实际上objc_msgSend()
会浏览元类查看其响应,如果找到了一个方法,那么就会以类对象来操作。
为什么我们的子类继承自Apple的类
当开始Cocoa开发时,辅导教材会告诉你诸如子类化NSObject
,然后开始编程,你会从仅是继承自Apple类收获不少。你甚至都不会意识到你所使用的对象是和Objective-C的运行时一起执行的。当给我们的类分配实例时,它看起来是这样的…
MyObject *object = [[MyObject alloc] init];
执行的第一条消息是+alloc
。如果查看这个文档,它讲到“新实例的isa
实例变量被初始化为一个描述该类的数据结构;其他所有实例变量的内存被设置为0。”因此,继承自Apple的类不仅继承了一些属性,而且还继承了在内存中能够简单分配创建我们对象的能力,它匹配了运行时要求(和一个指向我们的类的isa
指针)的结构体的内存是我们类的大小。
什么是类缓存?(objc_cache *cache)
当Objective-C运行时依照一个对象的isa
指针来检查该对象时,它会发现对象会实现了很多方法。但是你只能调用其中的一小部分,在运行时查表时,每次都为所有的selector搜索类的分派表(dispatch table)是没有意义的。所以类会实现缓存,不论什么时候在搜索类的分派表并找到了对应的选择器(selector),类会将它缓存起来。当objc_msgSend()
在类中搜索某个选择器时首先会搜索类缓存。这是基于这样的理论:如果在类上调用一个消息,你可能在以后还会再次调用。所以如果我们考虑到这一点,意味着如果有一个名为MyObject
的子类继承自NSObject
执行以下代码
MyObject *obj = [[MyObject alloc] init];
@implementation MyObject
- (id)init {
if (self = [super init]) {
[self setVarA:@”blah”];
}
return self;
}
@end
就会存在以下行为:
- 首先会执行
[MyObject alloc]
。但是在MyObject
中没有实现alloc,因此在该类中不会找到+alloc
,然后依照指向NSObject
的超类指针 - 我们询问
NSObject
是否对+alloc
有响应,有。+alloc
检查receiver的类是MyObject
类并为它分配一块内存,然后初始化它的指向MyObject
类的isa
指针,现在我们有了一个实例,最后我们为类对象将+alloc
缓存进NSObject
的类缓存中。 - 目前为止我们发送了一个类消息,现在要发送一个实例消息,它只是调用了
-init
或者我们的指定初始化方法(designated initialize)。当然我们的类会对那样的消息有响应,所以-(id)init
也被缓存。 - 然后调用
self = [super init];
。super
是一个指向对象超类的神奇的关键字,所以我们可以去NSObject
中并调用它的初始化方法。这样做是为了确保OOP继承性正确地运行以便你的父类可以正确的初始化其变量,然后你可以正确初始化自己的变量以及在需要的时候重写父类。就NSObject
而言,不会有什么重要的事情,但并不总是这样。有时会有重要的初始化。比如
#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[]) {
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]));
[pool drain];
return 0;
}
如果你是刚学习Cocoa,我猜你的回答可能会是
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
这是因为在Objective-C中+alloc
会返回一个潜在类的对象,-init
又会返回另外一个类的对象。(可参见Apple官方文档)
那么objc_msgSend
中到底发生了什么
实际上objc_msgSend()
中发生了很多事情。假如有以下代码
[self printMessageWithString:@"Hello World!"];
实际上会被编译器转换为
objc_msgSend(self, @selector(pringMessageWithSting:), @"Hello, World!");
在上句代码中,我们跟随目标对象isa
指向查表的指针来查看对象(或它的任何一个超类)是否对selector@selector(printMessageWithString:)
有响应。假如在类的分派表或缓存中找到了selector,我们跟随函数指针并执行它。所以,objc_msgSend()
不会有返回值,它会开始执行接着跟随指向你的方法的指针,然后你的方法返回值,就看起像是objc_msgSend()
返回值一样。Bill Bumgarner讲了比我更多关于objc_msgSend()
的细节(Part 1, Part 2 &Part 3)。但我会总结一下他讲的是什么以及你看到的Objective-C运行时代码…
- 检查忽略的selectors&短路。显然如果在垃圾回收机制下运行我们可以不调用
-retain
,-release
等。 - 检查空的目标。不同于其他语言,Objective-C中是可以给对象发送空消息的而且总有你想要发送的理由。假设我们有一个非空目标…
- 那么我们需要在类中找到
IMP
,首先我们会搜索它的类缓存,如果找到接着跟随指针跳到函数。 - 如果在缓存中没有找到
IMP
,那么接下来会搜索类的分派表,如果找到就跟随并跳到该指针 - 如果在缓存和类的dispatch table中找不到
IMP
,就会跳转到消息转发机制。
这意味着最终你的代码会由编译器转换为C语言的函数。因此,你所写的像这样的方法…
- (int)doComputeWithNum:(int)aNum;
会被转换为
int aClass_doComputeWithNum(aClass *self, SEL _cmd, int aNum);
Objective-C Runtime会通过调用指向这些方法的函数指针来调用你的方法。你不能直接调用这些转换后的方法,但是Cocoa Framework为你提供了一个获取该指针的方法…
//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);
这样,就可以直接访问并在运行时调用该函数,甚至如果确定需要执行一个特定的方法,可以使用这样的途径来规避运行时的动态性。这与Objective-C Runtime调用你的方法使用的是同样的途径,但其使用的是objc_msgSend()
。
Objective-C 消息转发(Message Forwarding)
在Objective-C中,将消息发送给本身都不知道如何响应的对象是合法的(有时甚至是有意而为之)。苹果在其官方文档中给出的原因之一是,为了模拟Objective-C本身不支持的多继承性,抑或你只是想要抽象你的设计并将另外一个对象/类隐藏在处理消息的场景之下。这是运行时所必须做的。其工作原理类似于此:
step1
Runtime搜索了你的类及其所有的父类的类缓存和类分派表,但是并未找到指定的方法
step2
Objective-C Runtime就会对你的类调用+ (BOOL) resolveInstanceMethod:(SEL)aSEL
方法。这样就给你一个机会来提供方法实现并告诉运行时你已经实现了此方法,如果它开始搜索,此时就会找到所需的方法。应该这样来完成…定义一个函数…
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@:”
是方法的返回值及其参数。可以在Type Encodings来查看你可以在该部分放置什么。
step3
然后Runtime调用- (id)forwardingTargetForSelector:(SEL)aSelector
方法。这样做给你一个机会(由于我们不能实现该方法(见step2))将Objective-C运行时指向另外一个应该对该消息有响应的对象,此外最好在调用消耗更多进程的方法- (void)forwardInvocation:(NSInvocation *)anInvocation
之前来做。可以这样实现
- (id)forwardingTargetForSelector:(SEL)aSelector
{
if (aSelector == @selector(mysteriousMethod:)) {
return alternateObject;
}
return [super forwardingTargetForSelector:aSelector];
}
显然此方法中不会返回self
,否则就会形成死循环。
step4
然后Runtime最后一次尝试将消息发送给预期的目标,并调用- (void)forwardInvocation:(NSInvocation *)anInvocation
方法。如果你没有见过NSIvocation,其本质就是对象形式的Objective-C Message。一旦拥有一个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:
,如果你想要在最后一次机会做一些事情,就可以重写它。
Non Fragile ivars(Modern Runtime)
最近我们在modern runtime中获得的是Non Fragile ivars概念。当编译你的类时,编译器就会生成一个ivar布局,其显示了在类中从哪来访问你的变量。这是一个获取指向你的对象的指针的低层次细节,查看与开始对象指向的字节相关的ivar偏移量,并且读取的字节数是你正在读取的变量类型的大小。因此你的ivar布局看起来是这样的,左侧列是字节偏移量。
此处有NSObject
的ivar布局,然后子类化NSObject
并扩展其添加我们自己的ivar。直到苹果公布更新或所有新的Mac OS 10.x之后会有这种情况发生
自定义对象被抹除是因为我们有部分重叠的超类。唯一能够阻止这种情况的选择是苹果是否坚持以前的布局,如果是,那么他们的Frameworks将不会推进,因为他们ivar的布局被冻结。在fragile ivars之下你必须重编译你的继承自苹果的类以修复配伍性。那么在non fragile ivars下发生了什么?
在non fragile ivars之下,编译器生成了与fragile ivars相同的ivar布局。但是,当运行时检测到有部分重叠超类时,它会调整该类新添加变量的偏移量,这样在子类中你添加的变量会得到维护。
Objective-C Associated Objects
最近Associated References被引入到Mac OS X 10.6系统中。不像其他语言一样,Objective-C不支持给对象动态的添加变量。所以直到现在,你不得不全力构建基础设施以假装正在给一个类添加变量。现在在Mac OS X 10.6系统中Objective-C Runtime本身会支持给这样做。如果我们想要给每个现存的类比如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
如果看看Modern 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.
**********************************************************************/
这背后的目的是运行时尝试存储selectors在vtable里,如此反过来它会提升你的app的运行速度,因为它比objc_msgSend()
使用了更少的指令。该vtable有16个selectors,它们构成了所有selector被全局调用的大多数,事实上进一步查看代码,你会看到垃圾回收(Garbage Collected)&非垃圾回收(non Garbage Collected)app的默认selector…
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:",
};
那么你如何知道是否处理它?在调试的时候你会看到其中一个方法在栈轨迹中被调用。所有的这些你应该基本上像objc_msgSend()
用于调试一样地对待…objc_msgSend_fixup
发生在运行时分配这些方法之一时调用vtable中的一个时段。。。。
总结
Objective-C Runtime是一项强大的特性,它为apps赋予了我们所期许的强大的功能。如果你还没有开始Runtime学习,那希望你能在Apple这些文档里学习到如何利用Objective-C Runtime。
Objective-C Runtime
Objective-C Runtime Programming Guide
(此外,关于Objective-C Runtime的更多文章可以阅读Mike Ash的博客)