注:本文是对 Colin Wheeler 的 Understanding the Objective-C Runtime 的翻译。

初学 Objective-C(以下简称ObjC) 的人很容易忽略一个 ObjC 特性 —— ObjC Runtime。这是因为这门语言很容易上手,几个小时就能学会怎么使用,所以程序员们往往会把时间都花在了解 Cocoa 框架以及调整自己的程序的表现上。然而 Runtime 应该是每一个 ObjC 都应该要了解的东西,至少要理解编译器会把

[target doMethodWith:var1];

编译成:

objc_msgSend(target,@selector(doMethodWith:),var1);

这样的语句。理解 ObjC Runtime 的工作原理,有助于你更深入地去理解 ObjC 这门语言,理解你的 App 是怎样跑起来的。我想所有的 Mac/iPhone 开发者,无论水平如何,都会从中获益的。

ObjC Runtime 是开源的

ObjC Runtime 的代码是开源的,可以从这个站点下载: opensource.apple.com

这个是所有开源代码的链接: http://www.opensource.apple.com/source/

这个是ObjC rumtime 的源代码: http://www.opensource.apple.com/source/objc4/
4应该代表的是build版本而不是语言版本,现在是ObjC 2.0

动态 vs 静态语言

ObjC
是一种面向runtime(运行时)的语言,也就是说,它会尽可能地把代码执行的决策从编译和链接的时候,推迟到运行时。这给程序员写代码带来很大的灵活
性,比如说你可以把消息转发给你想要的对象,或者随意交换一个方法的实现之类的。这就要求 runtime
能检测一个对象是否能对一个方法进行响应,然后再把这个方法分发到对应的对象去。我们拿 C 来跟 ObjC 对比一下。在 C 语言里面,一切从
main 函数开始,程序员写代码的时候是自上而下地,一个 C 的结构体或者说类吧,是不能把方法调用转发给其他对象的。举个栗子:

#include < stdio.h >

int main(int argc, const char **argv[])
{
printf("Hello World!");
return ;
}

这段代码被编译器解析,优化后,会变成一堆汇编代码:

.text
.align ,0x90
.globl _main
_main:
Leh_func_begin1:
pushq %rbp
Llabel1:
movq %rsp, %rbp
Llabel2:
subq $, %rsp
Llabel3:
movq %rsi, %rax
movl %edi, %ecx
movl %ecx, -(%rbp)
movq %rax, -(%rbp)
xorb %al, %al
leaq LC(%rip), %rcx
movq %rcx, %rdi
call _printf
movl $, -(%rbp)
movl -(%rbp), %eax
addq $, %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。但是在这之后就不晓得发生什么了。

什么是 Objective-C Runtime?

ObjC Runtime 其实是一个 Runtime 库,基本上用 C 和汇编写的,这个库使得 C 语言有了面向对象的能力(脑中浮现当你乔帮主参观了施乐帕克的 SmallTalk 之后嘴角一抹浅笑)。这个库做的事前就是加载类的信息,进行方法的分发和转发之类的。

Objective-C Runtime 术语

再往下深谈之前咱先介绍几个术语。

  • 2 Runtimes

    目前说来Runtime有两种,一个 Modern Runtime 和一个 Legacy Runtime。Modern Runtime 覆盖了64位的Mac OS X Apps,还有 iOS Apps,Legacy Runtime 是早期用来给32位 Mac OS X Apps 用的,也就是可以不用管就是了。

  • 2 Basic types of Methods

    一种 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

    一个 Selector 事实上是一个 C 的结构体,表示的是一个方法。定义是:

    typedef struct objc_selector  *SEL; 

    使用起来就是:

    SEL aSel = @selector(movieTitle); 

    这样可以直接取一个selector,如果是传递消息(类似于C的方法调用)就是:

    [target getMovieTitleForObject:obj];

    在 ObjC 里面,用'[]’括起来的表达式就是一个消息。包括了一个 target,就是要接收消息的对象,一个要被调用的方法还有一些你要传递的参数。类似于 C 函数的调用,但是又有所不同。事实上上面这个语句你仅仅是传递了 ObjC 消息,并不代表它就会一定被执行。target 这个对象会检测是谁发起的这个请求,然后决策是要执行这个方法还是其他方法,或者转发给其他的对象。

  • Class

    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 (Method Implementations)

    接下来看看啥是IMP。

    typedef id (*IMP)(id self,SEL _cmd,...); 

    一个 IMP 就是一个函数指针,这是由编译器生成的,当你发起一个 ObjC 消息之后,最终它会执行的那个代码,就是由这个函数指针指定的。

  • Objective-C Classes

    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 这个类对象执行方法调用。

为啥我们要继承 Apple Classes

初学 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 的类我们不仅是获得了很多很好用的属性,而且也继承了这种内存分配的方法。

那么啥是 Class Cache(objc_cache *cache)

刚刚我们看到 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

这段代码是这样执行的:

  1. [MyObject alloc] 先被执行。但是由于 MyObject 这个类没有 +alloc 这个方法,于是去父类 NSObject 查找。
  2. 检测 NSObject 是否响应 +alloc 方法,发现响应,于是检测 MyObject 类,根据其所需的内存空间大小开始分配内存空间,然后把 isa 指针指向 MyObject 类。那么 +alloc 就被加进 cache 列表里面了。
  3. 完了执行 -init 方法,因为 MyObject 响应该方法,直接加入 cache。
  4. 执行 self = [super init] 语句。这里直接通过 super 关键字调用父类的 init 方法,确保父类初始化成功,然后再执行自己的初始化逻辑。

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 ;
}

如果你是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 是对普通数组的封装,内部实现是复杂的,但是对外隐藏了复杂性。

OK说回 objc_msgSend 这个方法

这个方法做的事情不少,举个栗子:

[self printMessageWithString:@"Hello World!"];

这句语句被编译成这样:

objc_msgSend(self,@selector(printMessageWithString:),@"Hello World!");

这个方法先去查找 self 这个对象或者其父类是否响应 @selector(printMessageWithString:),如果从这个类的方法分发表或者 cache 里面找到了,就调用它对应的函数指针。如果找不到,那就会执行一些其他的东西。步骤如下:

  1. 检测这个 selector 是不是要忽略的。比如 Mac OS X 开发,有了垃圾回收就不理会 retain, release 这些函数了。
  2. 检测这个 target 是不是 nil 对象。ObjC 的特性是允许对一个 nil 对象执行任何一个方法不会 Crash,因为会被忽略掉。
  3. 如果上面两个都过了,那就开始查找这个类的 IMP,先从 cache 里面找,完了找得到就跳到对应的函数去执行。
  4. 如果 cache 找不到就找一下方法分发表。
  5. 如果还找不到就要开始消息转发逻辑了。

在编译的时候,你定义的方法比如:

-(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 非常重要的一个特性,大概的步骤如下:

  1. 查找该类及其父类的 cahce 和方法分发表,在找不到的情况下执行2。
  2. 执行 + (BOOL) resolveInstanceMethod:(SEL)aSEL 方法。

    这就给了程序员一次机会,可以告诉 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

  3. 接下来 Runtime 会调用 – (id)forwardingTargetForSelector:(SEL)aSelector 方法。
    这就给了程序员第二次机会,如果你没办法在自己的类里面找到替代方法,你就重载这个方法,然后把消息转给其他的Object。

    - (id)forwardingTargetForSelector:(SEL)aSelector
    {
    if(aSelector == @selector(mysteriousMethod:)){
    return alternateObject;
    }
    return [super forwardingTargetForSelector:aSelector];
    }

    这样你就可以把消息转给别人了。当然这里你不能 return self,不然就死循环了=.=

  4. 最后,Runtime 会调用 – (void)forwardInvocation:(NSInvocation *)anInvocation 这个方法。NSInvocation 其实就是一条消息的封装。如果你能拿到 NSInvocation,那你就能修改这条消息的 target, selector 和 arguments。举个栗子:
    -(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,所以有兴趣的读者可以自行查阅原文,这里就不详谈鸟。

References

Objective-C Runtime Programming Guide

Objective-C Runtime Reference

Runtime运行时的那点事儿的更多相关文章

  1. iOS开发——高级特性&Runtime运行时特性详解

    Runtime运行时特性详解 本文详细整理了 Cocoa 的 Runtime 系统的知识,它使得 Objective-C 如虎添翼,具备了灵活的动态特性,使这门古老的语言焕发生机.主要内容如下: 引言 ...

  2. ios - runtime运行时应用---交换方法

    runtime运行时用法之一 --- 交换类的方法,此处简单写了把系统的UIView的setBackgroundColor的方法换成了自定义的pb_setBackgroundColor 首先创建UIV ...

  3. runtime 运行时机制 完全解读

    runtime 运行时机制 完全解读   目录[-] import import 我们前面已经讲过一篇runtime 原理,现在这篇文章主要介绍的是runtime是什么以及怎么用!希望对读者有所帮助! ...

  4. 编译器设计-RunTime运行时环境

    编译器设计-RunTime运行时环境 Compiler Design - Run-Time Environment 作为源代码的程序仅仅是文本(代码.语句等)的集合,要使其活动,它需要在目标计算机上执 ...

  5. Objective-C Runtime 运行时之四:Method Swizzling

    理解Method Swizzling是学习runtime机制的一个很好的机会.在此不多做整理,仅翻译由Mattt Thompson发表于nshipster的Method Swizzling一文. Me ...

  6. Objective-C Runtime 运行时之一:类与对象

    Objective-C语言是一门动态语言,它将很多静态语言在编译和链接时期做的事放到了运行时来处理.这种动态语言的优势在于:我们写代码时更具灵活性,如我们可以把消息转发给我们想要的对象,或者随意交换一 ...

  7. Objective-O Runtime 运行时初体验

    Objective-C语言是一门动态语言,它将很多静态语言在编译和链接时期做的事放到了运行时来处理.这种动态语言的优势在于:我们写代码时更具灵活性,如我们可以把消息转发给我们想要的对象,或者随意交换一 ...

  8. iOS-浅谈runtime运行时机制-runtime简单使用(转)

    转自http://www.cnblogs.com/guoxiao/p/3583432.html 由于OC是运行时语言,只有在程序运行时,才会去确定对象的类型,并调用类与对象相应的方法.利用runtim ...

  9. iOS开发之runtime运行时机制

    最近参加三次面试都有被问到runtime,因为不太懂runtime我就只能支支吾吾的说点零碎.我真的好几次努力想看一看runtime的知识,因为知道理解它对理解OC代码内部变化有一定帮助,不过真心觉得 ...

随机推荐

  1. kafka迁移数据目录

    问题 先前存储kafka日志的磁盘空间太小,zabbix警报不断,于是加了磁盘,将日志存到新磁盘上. 解决方案 依次在每台机器上操作,保证有机器能响应producer和consumer的操作. 加磁盘 ...

  2. 夺命雷公狗ThinkPHP项目之----企业网站5之栏目的添加(主要是图片上传)

    我们照老,先老搞定控CategoryController.class.php制器,代码如下所示: <?php namespace Admin\Controller; use Think\Cont ...

  3. 夺命雷公狗---DEDECMS----12dedecms全局标签的使用以及嵌套标签的使用

    在网站开发中,在很多页面可能会使用到同一个变量,比如路径网站信息等,所以我们可以用全局变量来使用. 默认的放在: 进去里面看下就会发现很多的常量都是在这里定义的: 我们在实际开发的时候可以将我们在多个 ...

  4. ThinkPHP讲解(六)——添加数据

    添加数据到数据库有三种方式 第一种:使用数组添加 $model=D("Info"); //实例化对象 //添加数据的第一种方式:使用数组添加 //要添加的数组,必须是关联数组,ke ...

  5. [记录]使用openGL显示点云的一个程序

    #include <GL/glut.h> #include <stdio.h> #include <iostream> using namespace std; v ...

  6. Sql Server 检测死锁的SQL语句

    首先创建一个标量值函数DigLock,用来递归检测SqlServer中的每一个会话是否存在加锁循环,如果该函数最终返回1则表示检测到了加锁循环 (也就是说检测到了死锁),如果最终返回0则表示没有检测到 ...

  7. origin 8.5 曲线拟合,延长曲线范围

    1. 输入数据并选择Y轴数据 2 非线性拟合 Analysis—Fitting—Nonlinear Curve Fit—Open Dialog 3.选择拟合曲线类型 在origin7.5中选择曲线类型 ...

  8. 【和小强学移动app测试2】移动终端app测试点归纳(持续更新)

      以下所有测试最后必须在真机上完整的执行 1.安装.卸载测试 在真机上的以及通过91等第三方的安装与卸载 安装在手机上还是sd卡上 2.启动app测试 3.升级测试 数字签名.升级覆盖安装.下载后手 ...

  9. Thinkphp用exp表达式执行mysql语句,查询某字段不为空is not null,自动增值

    Thinkphp用exp表达式执行mysql语句,查询某字段不为空is not null,自动增值 Thinkphp 的文档经常不够完整的表达MYSQL的各种组合,is not null在thinkp ...

  10. c sharp学习 问题记录

    1.函数的调用可以嵌套,定义不可以 C# 参考之方法参数关键字:params.ref及out C#中方法的参数的四种类型