深入理解 Objective-C Runtime 机制

4,011 阅读11分钟

注:这篇文章适合对Runtime有一定了解的同学进一步理解 可以先看看这篇iOS Runtime(一) 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运行时。

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

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

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

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

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

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 NSObject的类缓存为类对象3)到目前为止,我们发送了一个类消息,但现在我们发送一个实例消息,只是调用-init或我们指定的初始化。当然我们的类响应这个消息所以 - (id)init get的放入缓存(4)然后self = [super init]被调用。super 是一个指向对象超类的魔术关键字,所以我们去NSObject并调用它的init方法。这是为了确保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;
}

结果是

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返回另一个类的对象。

消息发送

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 头文件中,我们可以找到他们的定义:

id objc_msgSend ( id self, SEL op, ... );
typedef struct objc_object *id;
struct objc_object {
    Class isa  OBJC_ISA_AVAILABILITY;
};
typedef struct objc_class *Class;
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; // 方法定义的链表

struct old_ivar_list {
    int ivar_count;
#ifdef __LP64__
    int space;
#endif
    /* variable length structure */
    struct old_ivar ivar_list[1];
};
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。

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;
};
struct old_protocol_list {
    struct old_protocol_list *next;
    long count;
    struct old_protocol *list[1];
};
struct old_class_ext {
    uint32_t size;
    const uint8_t *weak_ivar_layout;
    struct old_property_list **propertyLists;
};

消息发送的步骤

  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.未找到,进入消息分发的步骤

消息分发的步骤

  • 在对象类的 dispatch table 中尝试找到该消息。如果找到了,跳到相应的函数IMP去执行实现代码;
  • 如果没有找到,Runtime 会发送 +resolveInstanceMethod: 或者 +resolveClassMethod: 尝试去 resolve 这个消息;
  • 如果 resolve 方法返回 NO,Runtime 就发送 -forwardingTargetForSelector: 允许你把这个消息转发给另一个对象;
  • 如果没有新的目标对象返回, Runtime 就会发送 -methodSignatureForSelector: 和 -forwardInvocation: 消息。你可以发送 -invokeWithTarget: 消息来手动转发消息或者发送 -doesNotRecognizeSelector: 抛出异常。

objc_msgSend函数

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

Hybrid vTable Dispatch

新的 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.
**********************************************************************/
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执行顺序是根据编译顺序决定的。

部分内容引用和翻译自
www.friday.com/bbum/2009/1…
cocoasamurai.blogspot.com/2010/01/und…

最近会每日一篇的把博客上的文章迁移到掘金,希望大家关注我。

本文的附赠的Runtime一些用法的Sample
github.com/JunyiXie/XJ…