理解 Objc Runtime

  1. Runtime 是什么
  2. Objective-C
  3. Runtime 原理的概述
    1. 什么是Objective-C运行时?
    2. Objective-C 类和对象
      1. 为什么Objective-C的对象都要继承 NSObject
    3. 那么什么是类缓存? (objc_cache * cache)
  4. 消息发送
    1. 消息发送的步骤
    2. 消息分发的步骤
    3. Hybrid vTable Dispatch
  5. Category
    1. -category和+load方法

Runtime 是什么

一个用C和汇编语言写的Runtime库,来动态 创建类和对象、进行消息传递和转发。
(在运行时执行部分编译后的代码)

Objective-C

Objective-C 扩展了 C 语言,并加入了面向对象特性和 Smalltalk 式的消息传递机制。而这个扩展的核心是一个用 C 和 编译语言 写的 Runtime 库。它是 Objective-C 面向对象和动态机制的基石。

Objective-C 是一个动态语言,这意味着它不仅需要一个编译器,也需要一个运行时系统来动态 创建类和对象、进行消息传递和转发。理解 Objective-C 的 Runtime 机制可以帮我们更好的了解这个语言,适当的时候还能对语言进行扩展,从系统层面解决项目中的一些设计或技术问题。 Runtime的核心是 - 消息传递 (Messaging)。(动态调度)

Runtime 原理的概述

Objective-C的是一个运行时面向语言,这意味着当它可能在运行时决定如何实现而不是在编译期。 这给你很大的灵活性,你可以根据需要将消息重定向到适当的对象,或者甚至有意交换方法实现等。如果我们将它与C语言进行对比。

在很多语言,比如 C ,调用一个方法其实就是跳到内存中的某一点并开始执行一段代码。没有任何动态的特性,因为这在编译时就决定好了。而在 Objective-C 中,[object foo] 语法并不会立即执行 foo 这个方法的代码。它是在运行时给 object 发送一条叫 foo 的消息。这个消息,也许会由 object 来处理,也许会被转发给另一个对象,或者不予理睬假装没收到这个消息。多条不同的消息也可以对应同一个方法实现。这些都是在程序运行的时候决定的。

什么是Objective-C运行时?

Objective-C运行时是一个运行库,它是一个主要在C&Assembler中编写的库,它将面向对象的功能添加到C中以创建Objective-C。 这意味着它加载类信息,所有方法调度,方法转发等。Objective-C运行时本质上创建所有支持结构,使面向对象的编程与Objective-C可能。

Objective-C 类和对象

Objective-c类本身也是对象,而运行时通过创建Meta类处理这一点。 当你发送一个消息,如[NSObject alloc],你实际上是发送一个消息到类对象,该类对象需要是一个MetaClass的实例,它本身是根元类的实例。 而如果你说NSObject的子类,你的类指向NSObject作为它的超类。 然而,所有元类都指向根元类作为它们的超类。 所有的元类都只有它们响应的消息的方法列表的类方法。 所以当你发送消息到类对象,如[NSObject alloc],然后objc_msgSend()实际上通过元类查看它的响应,然后如果它找到一个方法,操作类对象。

为什么Objective-C的对象都要继承 NSObject

最初当你开始Cocoa开发,你可能没注意到我们的类一直都恪守着继承自NSObject的写法,有一件事你甚至没有意识到,发生在你身上的是将对象设置为使用Objective-C运行时。

1
MyObject *object = [[MyObject alloc] init];

执行的第一个消息是+ alloc。 如果你看看文档,它说“新实例的isa实例变量被初始化为描述类的数据结构;所有其他实例变量的内存设置为0” 所以通过继承NSObject类,我们不仅继承了一些伟大的属性,而且我们继承了在内存中容易地分配和创建我们的对象的能力.

那么什么是类缓存? (objc_cache * cache)

你或许在源码中发现了 Cache cache;

1
2
3
4
5
6
7
8
9
10
11
struct objc_class : objc_object {
Class superclass;
const char *name;
uint32_t version;
uint32_t info;
uint32_t instance_size;
struct old_ivar_list *ivars;
struct old_method_list **methodLists;
Cache cache;
...
}

看看它是什么样的结构体:

1
2
3
4
5
struct objc_cache {
unsigned int mask /* total = mask + 1 */ OBJC2_UNAVAILABLE;
unsigned int occupied OBJC2_UNAVAILABLE;
Method buckets[1] OBJC2_UNAVAILABLE;
};

可以看到一个类中 有 一个存放方法列表的数据结构,那么它到底用来干嘛的呢?

一个 class 往往只有 20% 的函数会被经常调用,可能占总调用次数的 80% 。每个消息都需要遍历一次 objc_method_list 并不合理。如果把经常被调用的函数缓存下来,那可以大大提高函数查询的效率。这也就是 objc_class 中另一个重要成员 objc_cache 做的事情 - 再找到 foo 之后,把 foo 的 method_name 作为 key ,method_imp 作为 value 给存起来。当再次收到 foo 消息的时候,可以直接在 cache 里找到,避免去遍历 objc_method_list.
(Hash表的方法实现)

当Objective-C运行时通过跟踪它的isa指针检查对象时,它可以找到一个实现许多方法的对象。然而,你可能只调用它们的一小部分,并且每次查找时,搜索所有选择器的类分派表没有意义。所以类实现一个缓存,每当你搜索一个类分派表,并找到相应的选择器,它把它放入它的缓存。所以当objc_msgSend()查找一个类的选择器,它首先搜索类缓存。这是基于这样的理论:如果你在类上调用一个消息,你可能以后再次调用该消息。所以如果我们考虑到这一点,这意味着如果我们有一个NSObject子类,名为MyObject并运行以下代码

1
2
3
4
5
6
7
8
9
10
MyObject *obj = [[MyObject alloc] init];
@implementation MyObject
-(id)init {
if(self = [super init]){
[self setVarA:@”blah”];
}
return self;
}
@end

消息发送

I’m sorry that I long ago coined the term “objects” for this topic because it gets many people to focus on the lesser idea. The big idea is “messaging” – that is what the kernal[sic] of Smalltalk is all about… The key in making great and growable systems is much more to design how its modules communicate rather than what their internal properties and behaviors should be.

Alan Kay 曾多次强调 Smalltalk 的核心不是面向对象,面向对象只是 the lesser ideas,消息传递 才是 the big idea。

消息传递的关键藏于 objc_object 中的 isa 指针和 objc_class 中的 class dispatch table。

在 Objective-C 中,类、对象和方法都是一个 C 的结构体,从 objc/objc.h 头文件中,我们可以找到他们的定义:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
struct objc_class : objc_object {
Class superclass;
const char *name;
uint32_t version;
uint32_t info;
uint32_t instance_size;
struct old_ivar_list *ivars;
struct old_method_list **methodLists;
Cache cache;
struct old_protocol_list *protocols;
// CLS_EXT only
const uint8_t *ivar_layout;
struct old_class_ext *ext;
/.../
}

struct objc_ivar_list ivars OBJC2_UNAVAILABLE; // 该类的成员变量链表
struct objc_method_list *
methodLists OBJC2_UNAVAILABLE; // 方法定义的链表

1
2
3
4
5
6
7
8
struct old_ivar_list {
int ivar_count;
#ifdef __LP64__
int space;
#endif
/* variable length structure */
struct old_ivar ivar_list[1];
};
1
2
3
4
5
6
7
8
9
10
11
struct old_method_list {
void *obsolete;
int method_count;
#ifdef __LP64__
int space;
#endif
/* variable length structure */
// 可变长的方法数组
struct old_method method_list[1];
};

objc_method_list 本质是一个有 objc_method 元素的可变长度的数组。一个 objc_method 结构体中有函数名,也就是SEL,有表示函数类型的字符串 (见 Type Encoding) ,以及函数的实现IMP。

这里有一些你可能感兴趣的代码: Cache,protocol_List,class_extension

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
typedef struct objc_cache *Cache OBJC2_UNAVAILABLE;
#define CACHE_BUCKET_NAME(B) ((B)->method_name)
#define CACHE_BUCKET_IMP(B) ((B)->method_imp)
#define CACHE_BUCKET_VALID(B) (B)
#ifndef __LP64__
#define CACHE_HASH(sel, mask) (((uintptr_t)(sel)>>2) & (mask))
#else
#define CACHE_HASH(sel, mask) (((unsigned int)((uintptr_t)(sel)>>3)) & (mask))
#endif
struct objc_cache {
unsigned int mask /* total = mask + 1 */ OBJC2_UNAVAILABLE;
unsigned int occupied OBJC2_UNAVAILABLE;
Method buckets[1] OBJC2_UNAVAILABLE;
};
1
2
3
4
5
struct old_protocol_list {
struct old_protocol_list *next;
long count;
struct old_protocol *list[1];
};
1
2
3
4
5
struct old_class_ext {
uint32_t size;
const uint8_t *weak_ivar_layout;
struct old_property_list **propertyLists;
};

好了 接下来让我们接触Runtime的核心机制,消息机制

消息发送的步骤

  1. Check for ignored selectors (GC) and short-circuit.如果 selector 是需要被忽略的垃圾回收用到的方法,则将 IMP 结果设为 _objc_ignored_method,这是个汇编程序入口,可以理解为一个标记。(OSX)
  2. Check for nil target.检查对象是否为nil
    • If nil & nil receiver handler configured, jump to handler
    • If nil & no handler (default), cleanup and return.
  3. Search the class’s method cache for the method IMP 在cache 中查找IMP
    • If found, jump to it.找到,跳转到相应的内存地址
    • Not found: lookup the method IMP in the class itself 未找到,在类的method_list中查找
      • If found, jump to it.找到,跳转
      • If not found, jump to forwarding mechanism.未找到,进入消息分发的步骤

消息分发的步骤

  1. resolveInstanceMethod/resolveClassMethod 方法解析,这里可以动态添加方法(添加了即可返回YES)
  2. forwardingTargetForSelector 把Selector 转发给其他实例响应
  3. methodSignatureForSelector,invokeWithTarget,doesNotRecognizeSelector 添加方法签名,让其他实例来处理方法的调用

关于objc_msgSend函数
事实上,在编译时你写的 Objective-C 函数调用的语法都会被翻译成一个 C 的函数调用 - objc_msgSend() 。

关于消息分发三个步骤的Example:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
// 第一步
// 成功解析的实例方法
/*
+ (BOOL)resolveInstanceMethod:(SEL)sel {
NSString *selectorString = NSStringFromSelector(sel);
if ([selectorString isEqualToString:@"mysteriousMethod"]) {
class_addMethod(self.class, @selector(mysteriousMethod), (IMP)functionForMethod1, "@:");
}
return [super resolveInstanceMethod:sel];
}
*/
// 在没有找到方法时,会先调用此方法,可用于动态添加方法
// 返回 YES 表示相应 selector 的实现已经被找到并添加到了类中,否则返回 NO
+ (BOOL)resolveInstanceMethod:(SEL)sel {
return YES;
}
// 第二步
// 如果第一步的返回 NO 或者直接返回了 YES 而没有添加方法,该方法被调用
// 在这个方法中,我们可以指定一个可以返回一个可以响应该方法的对象
// 如果返回 self 就会死循环
- (id)forwardingTargetForSelector:(SEL)aSelector
{
if(aSelector == @selector(xxx:)){
return self.alternateObject;
}
return [super forwardingTargetForSelector:aSelector];
}
// 第三步
// 如果 `forwardingTargetForSelector:` 返回了 nil,则该方法会被调用,系统会询问我们要一个合法的『类型编码(Type Encoding)』
// 若返回 nil,则不会进入下一步,而是无法处理消息
- (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector {
return [NSMethodSignature signatureWithObjCTypes:"v@:"];
}
// 当实现了此方法后,-doesNotRecognizeSelector: 将不会被调用
// 如果要测试找不到方法,可以注释掉这一个方法
// 在这里进行消息转发
- (void)forwardInvocation:(NSInvocation *)anInvocation {
// 我们还可以改变方法选择器
[anInvocation setSelector:@selector(notFind)];
// 改变方法选择器后,还需要指定接受者
[anInvocation invokeWithTarget:self];
}
- (void)notFind {
NSLog(@"没有实现 -mysteriousMethod 方法,并且成功的转成了 -notFind 方法");
}

你可能忽略了一个细节 V-Table
如果你学过C++,你可能会了解到Hybrid vTable Dispatch(虚拟表分发).
你可以参考我的这篇文章iOS 调用机制

Hybrid vTable Dispatch

新的 Objc-runtime-new.m 这样写到

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
/***********************************************************************
* 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.
**********************************************************************/
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
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:",
};

Runtime 通过 vTable 的方式 加速调用类的常用方法。

Category

但是category则完全不一样,它是在运行期决议的。
就category和extension的区别来看,我们可以推导出一个明显的事实,extension可以添加实例变量,而category是无法添加实例变量的(因为在运行期,对象的内存布局已经确定,如果添加实例变量就会破坏类的内部布局,这对编译型语言来说是灾难性的)

-category和+load方法

我们知道,在类和category中都可以有+load方法,那么有两个问题:
1)、在类的+load方法调用的时候,我们可以调用category中声明的方法么?
2)、这么些个+load方法,调用顺序是咋样的呢?

1)、可以调用,因为附加category到类的工作会先于+load方法的执行
2)、+load的执行顺序是先类,后category,而category的+load执行顺序是根据编译顺序决定的。

部分内容引用和翻译自
http://www.friday.com/bbum/2009/12/18/objc_msgsend-part-1-the-road-map/
http://cocoasamurai.blogspot.com/2010/01/understanding-objective-c-runtime.html

script>