Objective-C运行时对于刚刚学习cocoa/Objective-c的新手来说是一个很宏观的基础特征。这个原因是Ojbective-c是一门在几个小时之内就可以学会的语言,但是初学者将会花费大量的时间纠结在Cocoa Framework和弄明白他是怎么工作的。但是每一个人都至少得知道运行时在细节上是怎么工作的要超过像这样的代码 [target doMethodWith:var1](可以被编译器翻译成objc_msgSend(target,@selector(doMethodWith:),var1);)理解运行时可以帮助你对objective-c有一个更深的认识,并且知道自己的程序是怎样原型的。我想Mac/iphone开发者将能够从这里获取很多东西,忽略掉你的经验。
Objective-c运行时是开源的
Objective-c运行时是一开源的,并且可以从 获得。实际上测试Objective-c是我解释它怎么运行的第一种方法,要比阅读拼过的文档要好,在这个问题上。你可以下载当前版本的运行时。
动态语言和静态语言的比较
Objective-c是一个运行时的面向对象的怨言,这意味着它将在运行时决定执行什么而不是在编译时。这给了你很大的便利性,直接按照你的需要向对象发送消息。或者你甚至可以故意的改变方法实现等等。。。这需要对能够对对象内省来看起能够和不能够执行哪些方法并且将方法发送给对象的运行时的使用。如果我们和像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运行时是一个运行时库。,这是一个用C和汇编写的库,为了给C语言提供面向对象的能够,以创造Objective-C。这意味它加载类信息,分配方法,执行方法等等。Objective-c主要提供了让Objective-C面向对象的能力。
Ojbective-C 运行时术语
于是在我们更进一步之前,我们先了解一些在这个页面中出现的基本术语。对于Mac和Iphone开发者而言有两个运行时环境:现代运行时环境和遗留下来的运行时环境,涵盖64位Mac OS X程序和所有Iphone OS程序的遗留下来的运行时环境,涵盖所有程序。这里有两个种基本的方法。 实例方法(以“-”开始),一个作用于对象实例的动作。还有类方法(以“+”开始)。这种方法就像C方法一样是一段代码的结合一完成一个简单的任务,比如
-(NSString *)movieTitle { return @"Futurama: Into the Wild Green Yonder"; }
选择器A 选择器在Objective-C中是一个重要的C数据结构提供了辨识具体的Objective-C方法在你需要的对象上执行的方法。在运行时这个定义像:
typedef struct objc_selector *SEL;
并且使用时像
SEL aSel = @selector(movieTitle);Message
消息
[target getMovieTitleForObject:obj];
一个Objective-C消息是在[]之间的所有东西,它由你要发送消息的对象和你要执行的方法以及你要发送的参数组成。一个Objective-C消息与C函数调用相比是不同的。实际上你发送了一个消息给一个对象并不意味着这个对象会执行这个方法。对象将会检车谁是这个这小的发送者然后决定是执行一个别的方法还是传递这个消息给其他的对象。如果你观察类在运行时的情况,你将会发现
typedef struct objc_class *Class; typedef struct objc_object { Class isa; } *id;
很明显这里有很多东西正在进行。我们有一个Objective-C的数据结构。所有的objc对象有一个ISA的类指针。这就是我们用“isa pointer”所描述的。 ISA指针是所有Ojective-c运行时所需要的,去辨别对象,和对象类型然后判断其是否对一个选择器有响应当你发送小时时。然后我们看到了对象指针。对象指针默认没有告诉我任何东西,除了这是个Objective-C对象之外。当你有一个对象指针,你可以询问这个对象的类型,检查是否响应某个方法等等,然后执行当你知道这个指针指向的是一个什么对象。你能发现同样的东西关于Blocks在LLVM/CLang文档中。
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被设计成能够与Objective-C运行时结合,方便把他们当成对象处理,以使他们能够相应这些消息。
typedef id (*IMP)(id self,SEL _cmd,...);
IMP是编译器将会产生给你的函数指针。如果你刚接触Objective-C,你不用直接处理这些知道很长一段时间之后,但是这是Objective-C运行时像你看到的一样之行你的方法的基础实现。对于Objective-C类来说什么是一个Objetive-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类本身也是对象。并且运行时通过创建元信息来处理它。当你发送一个消息,你实际上是发送了一个消息给类对象。并且类对象必须是一个元类型的实例。当你说要从NSObject继承的时候,你的类指针指向NSObject做为它的父类。无论怎样,所有元类型都指向根元类型做为他们的弗雷。所有的元类型很简单的有用类方法他们能够响应的消息的列表。所以,当你发送一个小时给一个类对象的时候,实际上检查了元类型来确定它时候能够响应,并且如果可以找到一个方法,在类上执行该方法。
为什么我们从苹果的类型继承
在你最开始开发Cocoa程序的时候,教程都出要从NSobject结成,然后开始写代码,你会享受从苹果的类继承带来的乐趣。一件事情你甚至没有注意到,就是到底法身了什么当你组织你的对象在Objectiv-C运行时环境上。但你实例化了你的类,像这样:
第一个执行的消息是alloc。如果你查阅文档,它说“一个新实例的ISA实例被初始化成描述这个类的一个数据结构,所有其他的实例的没存被设置成0”。于是从苹果累继承我们不止继承了一些属性,而且我们还继承了简单的内存分配和在内存中创建符合运行时期望大小的我们的对象。
类Cache是个什么东西?
MyObject *object = [[MyObject alloc] init];
第一个执行的消息是alloc。如果你查阅文档,它说“一个新实例的ISA实例被初始化成描述这个类的一个数据结构,所有其他的实例的没存被设置成0”。于是从苹果累继承我们不止继承了一些属性,而且我们还继承了简单的内存分配和在内存中创建符合运行时期望大小的我们的对象。
类Cache是个什么东西?
当Objective-C运行时通过跟踪一个对象的ISA指针来检查一个对象的时候,它将会发现一个实现了很多方法的对象。无论怎样,你只会调用他们很少的一部分并且完全没有意义去检查类的方法执行表每次检查的时候。于是类实现了一个Cache,当你查找类的dispatch table并且想找到对应的方法的时,将会把方法放入cache中。于是当Ojbc_msgSend()检查一个类查找一个方法的时候,它会先检查cache。这个操作基于这样的理论,当你在一个类上执行一个方法一次之后,你将会执行相同的方法在之后的一段时间内。于是,如果我们着重考虑一下这个,如果我们有一个叫做MyObject的NSObject的子类,并且运行下列代码
MyObject *obj = [[MyObject alloc] init]; @implementation MyObject -(id)init { if(self = [super init]){ [self setVarA:@”blah”]; } return self; } @end
这些将会发生,
(1)
[MyObject alloc]
将会第一个执行。MyObject类没有实现alloc,于是我们查找
+alloc
失败在这个类中,然后我们跟踪着他的父类指针
NSObject
(2)我们查询
NSObject
是否对
+alloc
响应,并且的确如此.
+alloc
查找接受类
MyObject
并且分配内存,将ISA指针指向MyObject类并且我们有了一个实例,最后我们将alloc放入这个类的缓存中
(3)
现在我们正在发送一个类消息,但是我们发送了一个实例消息(被乘坐init或者我们的构造器)。当然我们的类响应该消息并且放入了cache。
(4)
然后 self = [super inti]被调用。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[]) { 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; }
现在如果你刚接触Cocoa,如果我问你将会打印什么你会说。
因为在Objective-C中存在这样的可能,alloc返回一个类型,而init返回另外一个类型。
NSMutableArray NSMutableArray NSArray NSArray MyObject MyObjectbut this is what happens
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返回另外一个类型。
在objc_msgSend时发生了什么?
实际上在objc_msgSend()中发生了很多事情。让我看一下假设我们有这样的代码...
[self printMessageWithString:@"Hello World!"];
它实际上被编译器翻译成了
1. 检查忽略的Selector和Short Circuit,很明显当我们运行在有垃圾回收机制的情况的时候我们将会忽略retai和release等等
objc_msgSend(self,@selector(printMessageWithString:),@"Hello World!");从这里我们跟踪目标对象的ISA指针来查找和判断这个对象是否响应 @selector(printMessageWithString:)。假设我们找到了这个选择器在这个类的dispatch表或者cache中。我们循着函数指针然后执行它。然后objc_msgSend()从来不返回。它开始执行,并且跟踪指向你的方法的指针,然后你的方法返回。这看起来像objc_msgSend()返回的一样。Bill Bumgarner 想要解释更多的东西关于objc_msgSend()比我在这里。但是总结起来,他说的和你在运行时看到的。
1. 检查忽略的Selector和Short Circuit,很明显当我们运行在有垃圾回收机制的情况的时候我们将会忽略retai和release等等
2.
检查nil目标。不想其他语言,nil在objective-C中是一个完全合法,并且这里有很多原因你也愿意这样。如果我们有一个非空的对象我们将会继续
3.
然后我们查找IMP在这个雷尚,我们现在cache中检查它,如果找到了就循着指针跳转到这个函数
4.
如果IMP没有在cache中找到,我们就检查dispatch table。如果找到了我们就跳转到这个函数之行
5.
如果IMP没有在cahce和dispatch table中找到,我们就会跳转到转发机制。这意味着在你的代码被编译器转化成了C函数。
-(int)doComputeWithNum:(int)aNum将会翻译成:
int aClass_doComputeWithNum(aClass *self,SEL _cmd,int aNum)
并且Objective-C运行时通过调用对应的函数指针来调用你的方法。现在我说你不能直接调用这些翻译后的方法,虽然cocoa framework提供了方法来获取这些指针。
通过这种方法你可以直接找到函数入口并且在运行时调用它。甚至使用它改变运行时,当你真的想这么做,并且知道什么方法被执行的时候。这是同样的方法来之行你的方法,但是应该使用objc_msgSend();
//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);
通过这种方法你可以直接找到函数入口并且在运行时调用它。甚至使用它改变运行时,当你真的想这么做,并且知道什么方法被执行的时候。这是同样的方法来之行你的方法,但是应该使用objc_msgSend();
Objective-C消息转发
在Objective-c中发送一个消息给一个你不知道它会怎么响应的对象是完全合法的(甚至有些是有是一种设计模式)。苹果在他们的文档中给出的一个原因是来模拟没有提供的多重继承。或者你想绝对化你的设计,并且隐藏消息实现。这是一件运行时非常需要的事情。它这样工作
1.
运行时检查累缓存和累的dipatch表还有他的弗雷的,但是没有找到特定方法
2.
运行时将会调用在你的类上调用 + (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]; }
在函数最后的“v@:”是这个函数方法的返回值和参数列表。你可以查看Type Encodings章节看看你能够在这里放些什么。
3.
运行时然后调用方法 - (id)forwardingTargetForSelector:(SEL)aSelector。这样做给了你一个机会重定向运行时到另外一个可以响应该消息的对象。这比之行花费更多的要好。你可以这么做
- (id)forwardingTargetForSelector:(SEL)aSelector { if(aSelector == @selector(mysteriousMethod:)){ return alternateObject; } return [super forwardingTargetForSelector:aSelector]; }
很明显你不会想在这个方法中返回self,这回导致一个死循环。
4.
运行时将会至少尝试一次将消息发送到这个重定向的目标上。如果你从来没有见到错NSInvocation,这是一个很重要的Objective-C对象形式的消息。一旦你有了一个NSINvocation你可以改变任何东西包括他的目标,方法,参数。任何你可以做的。。。。
-(void)forwardInvocation:(NSInvocation *)invocation { SEL invSEL = invocation.selector; if([altObject respondsToSelector:invSEL]) { [invocation invokeWithTarget:altObject]; } else { [self doesNotRecognizeSelector:invSEL]; } }
默认情况下继承NSobject的方法实现,只是简单的调用 -doesNotRecognizeSelector: 。你可以重载这个方法,如果你想获得一次机会做点什么。
Objective-C 联合对象
一个最近被添加进 Mac OS X 10.6 Snow Leopard 的东西是联合引用。Objective-c不支持动态添加成员变量,不像其他一些原生支持这个的语言。所以在次之前你必须构件一个非常大的数据结构来假装你正在个体一个类添加一个变量。现在在 Mac OS X 10.6 Snow Leopard 运行时原生支持这个。如果我们想要添加一个变量给每一个已经存在的类,比如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 };然后也能知道怎么把值取出来:objc_getAssociatedObject();
还可以取消关联:objc_removeAssociateObjects();