Objective-C Runtime机制简析

Objective-C在C的基础上添加了面向对象的特性,同时它是一种动态编程语言,将静态语言在编译和链接时需要做的一些事情给延后到运行时执行。例如方法的调用,只有在程序执行的时候,才能具体定位到哪个类的哪个方法。这就需要一个运行时库,就是Runtime。

1. 类的结构和定义

在Objective-C中,类实际上是一个objc_class结构体,其定义如下:

typedef struct objc_class *Class;
struct objc_class {
   Class isa  OBJC_ISA_AVAILABILITY;

#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

} OBJC2_UNAVAILABLE;

struct objc_object {
   Class isa  OBJC_ISA_AVAILABILITY;
};

可以看到,在objc2.0中,除了isa指针外,objc_class的其他成员变量皆已被弃用。
其中isa是objc_class结构体的指针,它指向当前类的meta class。

  • meta class 与 class
    在objc中,class存储类的实例方法(-),meta class存储类的类方法(+),class的isa指针指向meta class。下文会对此详细介绍。

objc_object结构体就是objc中的对象,它仅包含一个isa指针,指向当前对象所属的类。 我们常用的 id 实质上就是一个objc_object类型的指针。

图 1.1

如图1.1所示,一个对象(Instance of Subclass)的isa指针指向它所属的类 Subclass(class),Subclass(class)的isa指针指向 Subclass(meta),Subclass(meta)的isa指针指向Root class(meta)。Root class(meta)的isa指针指向本身。
同时,Root class(meta)的父类是Root class(class),即NSObject,NSObject的父类为nil。

2. 方法的调用

在这里需要先了解几个概念
SEL
SEL是objc_selector类型指针,是根据特定规则生成的方法的唯一标识。需要注意的是,只要方法名相同,生成的SEL就相同,与这个方法属于哪个类没有关系。

typedef struct objc_selector *SEL;

IMP
如果说,SEL是方法名,那么IMP就是方法的实现。IMP指针定义了一个方法的入口,指向了实现方法的代码块的内存地址。

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

objc_method
在objc中,方法实质上是一个objc_method指针。其中,method_name相当于objc_method的hash值,runtime通过method_name找到相应的方法入口(method_imp),从而执行方法的代码块。

struct objc_method {
    SEL method_name                                          OBJC2_UNAVAILABLE;
    char *method_types                                       OBJC2_UNAVAILABLE;
    IMP method_imp                                           OBJC2_UNAVAILABLE;
}                                                            OBJC2_UNAVAILABLE;

调用一个方法时具体做了什么?
在Objective-C中,方法的调用采用如下方式:

[object methodWithArg:arg];

在编译期间,以上代码会被转化为

objc_msgSend(object, methodWithArg, arg)

可以把它看作是发送消息的过,其中object为消息的接收体,它可能是一个对象,也可能是一个类。若为对象,则是实例方法(- 方法);反之,则是类方法(+方法)。mehodWithArg、arg是具体的消息内容。
object接收到消息之后,若是实例方法,则会从其所属的类Subclass(class)的methodLists去寻找methodWithArg:方法。若未找着,则到其父类Superclass(class)的methodLists中寻找。以此类推,直到根类NSObject,若仍未找着,就crash。
同理,若是类方法,则从对象所属类的meta class开始寻找。

3. 在Objective-C 2.0中的变化

前面提到过在objc2.0中,objc_class只剩下一个isa指针。由于Xcode对API进行了一定的封装,类的信息并未全部对开发者开放。我们不妨通过阅读Objective-C 2.0的源码去分析,可以通过 官网浏览,或者从github上下载源码。
从objc-runtime-new.h中可以看到objc_class的定义(只截取关键代码,下文同)

struct objc_object {
    isa_t isa;
};
struct objc_class : objc_object {
    // Class ISA;
    Class superclass;
    cache_t cache;             // formerly cache pointer and vtable
    class_data_bits_t bits;    // class_rw_t * plus custom rr/alloc flags

    class_rw_t *data() { 
        return bits.data();
    }
};

其中,superclass指向父类,cache缓存指针、方法入口等,用于提高效率。bits用于存储类名、类版本号、方法列表、协议列表等信息,替代了Objective-C1.0中methodLists、protocols等成员变量。

class_data_bits_t结构体
class_data_bits_t结构体中只有一个64位的指针bits,它相当于 class_rw_t 指针加上 rr/alloc 等标志位。其中class_rw_t指针存在于4~47位(从1开始计)。

图 3.1
#define FAST_IS_SWIFT         (1UL<<0)
#define FAST_DATA_MASK        0x00007ffffffffff8UL

is_swift标记位标示是否为swift的类。通过进行位运算可以得到一个class_rw_t类型指针。
class_rw_t结构体的定义如下

struct class_rw_t {
    uint32_t flags;
    uint32_t version;

    const class_ro_t *ro;

    method_array_t methods;
    property_array_t properties;
    protocol_array_t protocols;

    Class firstSubclass;
};

其中methods存储方法列表、properties存储属性列表、protocols存储协议列表。注意到这里有一个class_ro_t类型指针,我们会在下文详细介绍。

dyld加载镜像
dyld是objc的动态链接库,在程序运行时,会将镜像加载进内存。

  • 镜像
    工程的编译产物,包括一些动态链接库、Foundation等等,是一些二进制文件。

在程序初始化方法_objc_init中注册了两个回调

 dyld_register_image_state_change_handler(dyld_image_state_bound,1/*batch*/, &map_2_images);
 dyld_register_image_state_change_handler(dyld_image_state_dependents_initialized, 0/*not batch*/, &load_images);

其中, map_2_images方法的注释为:Process the given images which are being mapped in by dyld,即处理由dyld映射的给定镜像。它的调用如下:
map_2_images → map_images_nolock → _read_images → realizeAllClasses
realizeAllClasses会完成对镜像中所有类的加载和预处理,它最终会调用realizeClass来处理每一个类,而realizeClass又通过调用methodizeClass来对类结构体的methods列表赋值。
可以通过添加符号断点,来直观的查看这几个方法的调用关系,如图3.2。

图 3.2

+load方法
+load方法会在main方法之前被调用,所有使用到的类的load方法都会被调用。先调用父类的+load方法,再调用子类的+load方法;先调用主类的+load方法,再调用分类的+load方法。

图 3.3

图3.3是+load方法的调用栈。load_images 方法是每个镜像加载完毕的回调。

const char *
load_images(enum dyld_image_states state, uint32_t infoCount,
            const struct dyld_image_info infoList[])
{
    bool found;

    // Return without taking locks if there are no +load methods here.
    found = false;
    for (uint32_t i = 0; i < infoCount; i++) {
        if (hasLoadMethods((const headerType *)infoList[i].imageLoadAddress)) {
            found = true;
            break;
        }
    }
    if (!found) return nil;

    recursive_mutex_locker_t lock(loadMethodLock);

    // Discover load methods
    {
        rwlock_writer_t lock2(runtimeLock);
        found = load_images_nolock(state, infoCount, infoList);
    }

    // Call +load methods (without runtimeLock - re-entrant)
    if (found) {
        call_load_methods();
    }

    return nil;
}

load_Images会判断镜像是否实现了+load方法,并且调用load_images_nolock方法找到所有+load方法,之后通过call_load_methods调用所有的+load方法。

class_ro_t
class_ro_t与class_rw_t的最大区别在于一个是只读的,一个是可读写的,实质上ro就是readonly的简写,rw是readwrite的简写。

struct class_ro_t {
    const char * name;
    method_list_t * baseMethodList;
    protocol_list_t * baseProtocols;
    const ivar_list_t * ivars;
};

在编译之后,class_ro_t的baseMethodList就已经确定。当镜像加载的时候,methodizeClass方法会将 baseMethodList 添加到class_rw_t的methods列表中,之后会遍历category_list,并将category的方法也添加到methods列表中。
这里的category指的是分类,基于此,category能扩充一个类的方法。这是开发时经常需要使用到。
class_ro_t在内存中是不可变的。在运行期间,动态给类添加方法,实质上是更新class_rw_t的methods列表。
baseProtocols与baseMethodList类似。
objc_object、objc_class、class_rw_t、class_ro_t的关系如图3.4。

图 3.4

类的理解与方法的调用

  • 对象方法:前面提过,调用对象方法,相当于给对象发送消息,例如[obj methodWithArg: arg] 。 当obj_object接收到消息后,通过其isa指针找到对应的objc_class,objc_class又通过其data() 方法,查询class_rw_t的methods列表。若有,则返回;否则,到其父类寻找。以此类推,直到根类,若在根类中仍没有该方法,则crash。

  • 类方法: 在objc中,类本身也是一个对象。objc_class继承自objc_object,有一个isa指针,指向其所属的类,即meta class。可以这样理解,类是meta class 的对象。所以,当调用类方法是,例如[classObj methodWithArg: arg],classObj也会通过其isa指针到其所属的类(meta class)中寻找。这也就是为什么说,图1.1 里class 存储对象方法,meta class 存储类方法。

  • meta class的isa指针:meta class本身也是一个对象,它的isa指针指向的也是其所属的类。子meta class 的isa指针指向NSObjct 的meta class。 NSObjct 的meta class 的isa指针指向自身。当然,由于苹果进行了封装,在开发中基本不可能直接去使用meta class。

对象的成员变量寻址
前面提过,在objc_object中只有一个isa指针。实际上当我们调用 +alloc 方法来初始化一个对象时,也仅仅在内存中生成了一个objc_object结构体,并根据其instanceSize来分配空间,将其isa指针指向所属的类。
类的成员变量ivar_t存储在class_ro_t中的ivar_list_t * ivars中,ivar_t的定义如下:

struct ivar_t {
    int32_t *offset;
    const char *name;
    const char *type;
    uint32_t size;
}

其中offset 是成员变量相对于对象内存地址的偏移量,正是通过它来完成变量寻址。
当我们使用对象的成员变量时,如 myObject.var ,编译器会将其转化为object_getInstanceVariable(myObject, 'var', **value) 找到其ivar_t结构体ivar,然后调用object_getIvar(myObject, ivar)来获取成员变量的内存地址。其计算公式如下:

id *location = (id *)((char *)obj + ivar_offset);

基于此,虽然多个对象的isa指针指向同一个objc_class,但由于对象的内存地址不一样,所以它们的实例变量存储位置也不一样,从而实现对象与类之间的多对一关系。

推荐阅读更多精彩内容