深入浅出Objective-C runtime

3,436 阅读47分钟

1. 前言

1.1. 什么是Objective-C?

1.1.1. 概念

Objective-C是一种通用、高级、面向对象的编程语言。它扩展了标准的ANSI C编程语言,将Smalltalk式的消息传递机制加入到ANSI C中。目前主要支持的编译器有GCC和Clang(采用LLVM作为后端)。

Objective-C的商标权属于苹果公司,苹果公司也是这个编程语言的主要开发者。苹果在开发NeXTSTEP操作系统时使用了Objective-C,之后被OS X和iOS继承下来。现在Objective-C与Swift是OS X和iOS操作系统、及与其相关的API、Cocoa和Cocoa Touch的主要编程语言。

(From 维基百科

简而言之,Objective-C是C的超集。而与C语言不同的是,虽然Objective-C关于C的部分是静态的,但是关于面向对象的部分是动态的。所谓的静态,指的是在编译期间所有的函数、结构体等都确定好了内存地址,调用行为都被解析为内存地址+偏移量。而动态指的是,代码的合法性会延迟到运行的过程中校验,调用行为会被解析为调用底层语言的接口。

1.1.2. 编译过程

在Apple官方IDE Xcode中,其编译的过程,可以简单的理解为编译器前端Clang先将Objective-C源码预处理成C/C++源码,再接下去进行编译成IR的过程。可以在Terminal中使用Clang查看Objective-C源码的编译流程。如下所示:

$ # 假设Objective-C源文件为main.m, 生成的C++源文件则为同目录下的main.cpp
$ clang -ccc-print-phases main.m
               +- 0: input, "main.m", objective-c
            +- 1: preprocessor, {0}, objective-c-cpp-output
         +- 2: compiler, {1}, ir
      +- 3: backend, {2}, assembler
   +- 4: assembler, {3}, object
+- 5: linker, {4}, image
6: bind-arch, "x86_64", {5}, image

也可以使用Clang将Objective-C源码翻译成C/C++源码。如下所示:

$ # 假设Objective-C源文件为main.m, 生成的C++源文件则为同目录下的main.cpp
$ clang -rewrite-objc main.m

1.2. 什么是runtime?

1.2.1. 概念

执行时期(Run time)在计算机科学中代表一个计算机程序从开始执行到终止执行的运作、执行的时期。与执行时期相对的其他时期包括:设计时期(design time)、编译时期(compile time)、链接时期(link time)、与加载时期(load time)。

(From 维基百科

简而言之,runtime是计算机程序正在运行中的状态。而我们集中关注的是Objective-C程序中在runtime里的语言特性及实现原理。

1.2.2. Objective-C runtime

The Objective-C runtime is a runtime library that provides support for the dynamic properties of the Objective-C language, and as such is linked to by all Objective-C apps. Objective-C runtime library support functions are implemented in the shared library found at /usr/lib/libobjc.A.dylib.

You typically don't need to use the Objective-C runtime library directly when programming in Objective-C. This API is useful primarily for developing bridge layers between Objective-C and other languages, or for low-level debugging.

(From Apple

简单翻译一下,Objective-C runtime是Objective-C这门语言为了支持语言的动态特性而催生出的底层动态链接库。它提供的底层API能比较方便地与其他语言进行交互。

虽然Objective-C自身是开源的,但是支持其动态语言特性的runtime库却有不同的实现版本。除了Apple官方对macOS量身定制的runtime库,GNU也开源了一份相同API的runtime库。

如果想要使用Xcode调试Objective-C runtime源码,可以参考Objective-C runtime 源码调试

接下来关于Objective-C runtime的剖析全部基于Apple开源的objc4-838.1。但是,由于不同的CPU架构对应的runtime源码实现有所不同(源码中通过宏的方式来区分),为了简化这部分的叙述,故以x86-64为例。

本文调试环境

Mac机器配置:

  • macOS Monterey (macOS 12)
  • Intel® Core™ i7-9750H

Xcode配置:

  • Version 13.2.1 (13C100)
  • objc4-838.1

PAY ATTENTION

  • 为了方便讲解,对部分源码做了一定的改动,但不影响其主要逻辑
  • 以下内容需要有一定的Objective-C和C/C++基础

2. 剖析Objective-C的面向对象

在Objective-C中,有两大基类NSObjectNSProxy,而NSObject也作为唯一的基协议。其他所有Objective-C的类都继承自NSObjectNSProxy,并遵守NSObject协议。NSObject是日常研发中经常使用的基类,所以,我们接下来重点需要探究的是Objective-C面向对象的实现以及其与NSObject的联系。

2.1. 面向对象的实现

要想搞清楚Objective-C是如何实现面向对象的,首要任务是剖析NSObject。先给出NSObject的实现源码:

typedef struct objc_class *Class;
typedef struct objc_object *id;

union isa_t {
    uintptr_t bits;
    Class cls;
    struct {
      uintptr_t nonpointer        : 1;
      uintptr_t has_assoc         : 1;
      uintptr_t has_cxx_dtor      : 1;
      uintptr_t shiftcls          : 44;
      uintptr_t magic             : 6;
      uintptr_t weakly_referenced : 1;
      uintptr_t unused            : 1;
      uintptr_t has_sidetable_rc  : 1;
      uintptr_t extra_rc          : 8;
    };
};

struct objc_object {
    isa_t isa;
};

struct objc_class : objc_object {
    Class superclass;
    cache_t cache;
    class_data_bits_t bits;
};

@interface NSObject <NSObject> {
    Class isa;
}
@end

将源码简单转化为UML类图如下:

image.png

从源码不难看出,NSObject本质上是objc_class结构体。而在objc_class结构体中,除了继承得来的isa变量,通过变量的命名,我们也可以轻易知道obj_class还包含父类指针、缓存和类数据。我们日常使用实例对象的id类型和类对象的Class类型,实质是一个指向objc_objectobjc_class结构体的指针类型。举个简单的例子:

id instanceObj = [NSObject new];

这个简单的Objective-C创建实例对象的代码中的instanceObj实际上是一个指向objc_object结构体的指针,通过被指向的内存空间中的objc_object结构体中的成员变量isa能获得NSObject类对象的objc_class结构体的内存地址。注意到我这里提到了NSObject类对象,其实还有一个NSObject元类对象。这里先给出Objective-C对于面向对象的完整实现原理图:

image.png

从图中不难看出,无论是根类还是子类,都有分为类对象和元类对象。那问题来了,为什么要区分出类对象和元类对象呢?我们先看这样一个例子:

// define FooClass
@interface FooClass : NSObject
+ (void)sayHi;
- (void)sayHi;
@end

@implementation FooClass
+ (void)sayHi {
    NSLog(@"+ FooClass: Hi");
}

-(void)sayHi {
    NSLog(@"- FooClass: Hi");
}
@end

// some other function
FooClass *foo = [FooClass new];
[foo sayHi];
[FooClass sayHi];

在这个例子中,19行调用的是实例方法,20行调用的是类方法。之前有提到过,实际上Objective-C调用方法会的代码实际上会改写为调用runtime的API,这两个方法调用都会改写为以下代码:

objc_msgSend(foo, @selector(sayHi));
objc_msgSend((id)objc_getClass("FooClass"), @selector(sayHi));

objc_getClass("FooClass")这个方法会返回一个Class类型,通过被指向内存空间中的objc_class结构体中的成员变量isa能获得FooClass元类对象的objc_class结构体的内存地址。通过这样的逻辑,当objc_msgSend的第一个入参为实例对象指针时,就能找到类对象,并调用对应的方法;当objc_msgSend的第一个入参为类对象指针时,就能找到元类对象,并调用对应的方法。这样,在Objective-C的任何方法调用上,都能统一由objc_msgSend收敛。并且,在Objective-C的实现中,也会将实例方法存放在类对象的objc_class结构体内,而将类方法存放在元类对象的objc_class结构体内。这样,开发者就能轻松的调用实例方法或者类方法了。

2.2. isa“指针”

讲完了NSObject以及Objective-C在面向对象上的大致实现,接下来我们细致分析一下isa“指针”。注意到我这里打上了双引号,也就是意味着isa并不仅仅是一个指针。isa的类型为isa_t联合体。虽然现在的CPU对内存的寻址空间达到了64位之多,“理论上”能支持2^64字节的内存,但是实际上我们物理内存远远达不到这个量级,所以实际上64位编译环境的C/C++指针类型是“非常”浪费空间的。为了达到极致的内存控制,isa_t除了存储了内存地址,还存储了额外的信息。

image.png 这里先给出对应比特代表具体的含义:

  • nonpointer (1)

是否为非纯指针(即有无附带额外信息),当为0时,isa为纯指针;当为1时,isa并非纯指针。

  • has\_assoc (1)

当前对象是否具有或者曾经具有关联对象(与runtime API的objc_setAssociatedObject等有关)。

  • has_cxx_dtor (1)

当前对象是否具有Objective-C或者C++的析构器。

  • shiftcls (43)

保存指向的objc_class结构体内存地址。

  • magic (6)

用于调试器判断当前对象是真的对象还是没有初始化的空间。在x86-64中,判断其是否等于0x3B(0b111011)。

  • weakly_referenced (1)

当前对象是否被弱引用指针指向或者曾经被弱引用指针指向。

  • unused (1)

当前对象是否已经废弃(正在释放内存)。

  • has_sidetable_rc (1)

当前对象的引用计数是否由散列表记录(当引用计数过大时,由额外的散列表存储)

  • extra_rc (8)

存放当前对象的引用计数(当溢出时,由额外的散列表存储,此时将has_sidetable_rc置为1)

objc_object结构体的成员函数具有isa的初始化函数:

#define ISA_MAGIC_VALUE 0x001d800000000001ULL

struct objc_object {
    isa_t isa;
    
    void initInstanceIsa(Class cls, bool hasCxxDtor);
    void initIsa(Class newCls, bool nonpointer, bool hasCxxDtor);
};

union isa_t {
    Class cls;
    
    void setClass(Class newCls, objc_object *obj);
};

inline void 
objc_object::initInstanceIsa(Class cls, bool hasCxxDtor) {
    initIsa(cls, true, hasCxxDtor);
}

inline void 
objc_object::initIsa(Class cls, bool nonpointer, bool hasCxxDtor) { 
    isa_t newisa(0);
    if (!nonpointer) {
        newisa.setClass(cls, this);
    } else {
        newisa.bits = ISA_MAGIC_VALUE;
        newisa.has_cxx_dtor = hasCxxDtor;
        newisa.setClass(cls, this);
        newisa.extra_rc = 1;
    }
    isa = newisa;
}

inline void
isa_t::setClass(Class newCls, objc_object *obj) {
    shiftcls = (uintptr_t)newCls >> 3;
}

根据这段源码,isa初始化的操作是分别将nonpointerextra_rc置为1,magic置为0x3B(0b111011),设置has_cxx_dtorshiftcls。注意到第31行的setClass函数,对shiftcls赋值为newCls右移3位。那问题来了,为什么要右移3位呢?其实在C/C++语言中,结构体会做内存对齐,所以在64位系统中的结构体的内存地址的末三位为0。虽然macOS在x86-64上内存寻址空间为0x7fffffe00000(约为128TB),但仅需43位即可保存需要的内存地址信息。

而同样的,要想从isa中获取Class指针,仅需shiftcls的内容。源码实现如下:

#define ISA_MASK 0x00007ffffffffff8ULL

union isa_t {
    Class cls;
    
    Class getClass(bool authenticated);
};

inline Class
isa_t::getClass(bool authenticated) {
    uintptr_t clsbits = bits;
    clsbits &= ISA_MASK;
    return (Class)clsbits;
}

实际上,并不是所有的实例对象都有isa“指针”。Apple早在WWDC 2013就提出了Tagged Pointers技术,在64位机器上将数据巧妙地“存储”到实例对象的“指针”内,所以这些实例对象也可以简单理解为“伪对象”。由于这些“伪对象”本身不是指针类型,所以也没有objc_object结构,自然也没有isa“指针”。为了方便叙述,全篇都将不会讨论Tagged Pointers,所有实例对象默认具备objc_object结构。

对Tagged Pointers感兴趣的同学可以参考以下链接:

2.3. 数据存储bits

2.3.1. 数据存储结构

接下来,重点讲解的是class_data_bits_tclass_data_bits_t也是一个结构体,存储了方法、属性、协议、实例变量布局等数据。先给出它的源码:

#define FAST_DATA_MASK 0x00007ffffffffff8UL
#define RW_REALIZED (1<<31)

struct explicit_atomic : public std::atomic<T> {
    explicit explicit_atomic(T initial) noexcept : std::atomic<T>(std::move(initial)) {}
    operator T() const = delete;
}

struct class_data_bits_t {
    friend objc_class;
    
    uintptr_t bits;
    
    class_rw_t *data() const {
        return (class_rw_t *)(bits & FAST_DATA_MASK);
    }
    
    const class_ro_t *safe_ro() const {
        class_rw_t *maybe_rw = data();
        if (maybe_rw->flags & RW_REALIZED) {
            return maybe_rw->ro();
        } else {
            return (class_ro_t *)maybe_rw;
        }
    }
};

struct class_rw_t {
    uint32_t flags;
    uint16_t witness;
    explicit_atomic<uintptr_t> ro_or_rw_ext;
    
    Class firstSubclass;
    Class nextSiblingClass;
    
    using ro_or_rw_ext_t = PointerUnion<const class_ro_t, class_rw_ext_t, PTRAUTH_STR("class_ro_t"), PTRAUTH_STR("class_rw_ext_t")>;
    
    const ro_or_rw_ext_t get_ro_or_rwe() const { return ro_or_rw_ext_t{ro_or_rw_ext}; }
    
    class_rw_ext_t *ext() const { return get_ro_or_rwe().dyn_cast<class_rw_ext_t *>(&ro_or_rw_ext); }
    
    const class_ro_t *ro() const {         
        auto v = get_ro_or_rwe();         
        if (v.is<class_rw_ext_t *>()) {             
            return v.get<class_rw_ext_t *>(&ro_or_rw_ext)->ro;         
        }         
        return v.get<const class_ro_t *>(&ro_or_rw_ext);     
    }
    
    const method_array_t methods() const {         
        auto v = get_ro_or_rwe();         
        if (v.is<class_rw_ext_t *>()) {             
            return v.get<class_rw_ext_t *>(&ro_or_rw_ext)->methods;         
        } else {             
            return method_array_t{v.get<const class_ro_t *>(&ro_or_rw_ext)->baseMethods};         
        }     
    }
    
    const property_array_t properties() const {         
        auto v = get_ro_or_rwe();         
        if (v.is<class_rw_ext_t *>()) {             
            return v.get<class_rw_ext_t *>(&ro_or_rw_ext)->properties;         
        } else {             
            return property_array_t{v.get<const class_ro_t *>(&ro_or_rw_ext)->baseProperties};         
        }     
    }
    
    const protocol_array_t protocols() const {         
        auto v = get_ro_or_rwe();         
        if (v.is<class_rw_ext_t *>()) {             
            return v.get<class_rw_ext_t *>(&ro_or_rw_ext)->protocols;         
        } else {             
            return protocol_array_t{v.get<const class_ro_t *>(&ro_or_rw_ext)->baseProtocols};         
        }     
    }
};

struct class_rw_ext_t {     
    DECLARE_AUTHED_PTR_TEMPLATE(class_ro_t)     
    class_ro_t_authed_ptr<const class_ro_t> ro;     
    method_array_t methods;     
    property_array_t properties;     
    protocol_array_t protocols;     
    char *demangledName;     
    uint32_t version; 
};

struct class_ro_t {
    uint32_t flags;     
    uint32_t instanceStart;     
    uint32_t instanceSize;
    uint32_t reserved;
    
    union {         
        const uint8_t *ivarLayout;         
        Class nonMetaclass;     
    };
    
    explicit_atomic<const char *> name;
    WrappedPtr<method_list_t, method_list_t::Ptrauth> baseMethods;
    protocol_list_t *baseProtocols;
    const ivar_list_t *ivars;
    
    const uint8_t *weakIvarLayout;     
    property_list_t *baseProperties;
    
    _objc_swiftMetadataInitializer __ptrauth_objc_method_list_imp _swiftMetadataInitializer_NEVER_USE[0];
};

简单转化为UML类图如下:

image.png

不难看出,class_data_bits_t结构体能获取class_rw_tclass_ro_t结构体指针。而class_ro_t结构体指针实际上是class_rw_ext_t结构体的成员变量,而class_rw_ext_t结构体指针实际上是class_rw_t结构体的成员变量。这里面引入了三个重要的结构体:class_rw_tclass_rw_ext_tclass_ro_t。这里简单解释一下,rw代表的是read-write,即可读写;ext代表的是extension,即拓展;ro代表的是read-only,即只读。所以顾名思义,class_rw_t存储了读写数据,class_ro_t存储了只读数据,而class_rw_ext_t存储了class_rw_t的拓展数据。

那问题来了,为什么搞了三个不同的数据结构呢(早些年Apple的实现其实不是如此)?其实,这是Apple为了节约内存做出的改变。在WWDC 2020中,专门有一期视频讲解了Apple在2020对Objective-C runtime上的改变——Advancements in the Objective-C runtime

简单的总结一下,Apple将内存分为两类,一类是Dirty Memory,指的是在进程的运行中需要一直存在于内存中,也就是说进程在运行的过程中会对Dirty Memory进行读写操作;另一类是Clean Memory,指的是在进程的运行过程中不需要一直存在于内存中,也就是说进程在运行的过程中并不会对Clean Memory进行写操作,也就是说Clean Memory是只读的。这样一来,当内存紧张时可以丢弃Clean Memory,当有读需求的时候再从硬盘中载入到内存中。这样的设计尤其对iOS友好,众所周知,iOS并没有macOS中内存swap能力,所以优先使用Clean Memory是WWDC 2020对Objective-C runtime的一个重大改进。基于此,Apple对于class_rw_tclass_rw_ext_tclass_ro_t这三个结构体的存储方式是这样设计的:

  • 编译成二进制产物存在硬盘(Flash、SSD、HDD)

在编译的过程中,自定义类的方法、协议、实例变量、属性都是确定的,所以仅需要class_ro_t结构体。

image.png

  • 进程初次运行

进程初期运行时,会调用Objective-C runtime的初始化入口_objc_init,将objec_classclass_ro_t加载到内存中。

image.png

  • 首次调用

类被首次调用时,将在内存中创建class_rw_t

image.png

  • 进程运行时动态添加数据

在进程运行时动态添加方法、属性、协议等时,再创建class_rw_ext_t来存储运行时添加的数据。

image.png

2.3.2. 实例变量的存储实现

为了了解实例变量在Objective-C中是如何实现的,我们先写个简单的例子:

@interface FooClass : NSObject

@property (nonatomic, strong) id someObject;

@end

@implementation FooClass

@end

这个例子中,我们定义了一个NSObject的子类FooClass,并且具有一个属性someObject。我们知道,在Objective-C中,如果我们要使用直接使用someObject的实例变量,可以直接在FooClass的方法中直接调用_someObject。那为什么可以这么做呢?我们使用Clang将这段Objective-C源码翻译成C/C++:

typedef struct objc_object NSObject;
struct NSObject_IMPL {
  Class isa;
};

typedef struct objc_object FooClass;
struct FooClass_IMPL {
  struct NSObject_IMPL NSObject_IVARS;
  id _someObject;
};

其实翻译后的C/C++接近十万余行,故只关注我们关心的FooClass的实例变量实现。可以看到,实际上FooClass_IMPL结构体才是FooClass实例对象的实现,并且在FooClass_IMPL结构体中,_someObject是其成员变量。

至此,答案呼之欲出了,我们在FooClass的方法中直接调用_someObject实际上是编译器将_someObject硬编码成内存偏移量(原理等同于在结构体方法中调用成员变量)。

这时候,问题又来了,我们在FooClass的方法除了直接调用_someObject外,还可以使用点方法self.someObject或者getter方法[self someObject]来获取实例变量对应的值。getter方法没什么好说的,实际上它就是在getter方法中使用硬编码内存偏移量的形式来获取实例变量的(重写getter方法的话就不一定如此了)。而点方法不同,如果对Objective-C稍微有点了解就知道,实际上点方法依赖于KVC(Key Value Coding),它首先会在方法列表中遍历查找getter或setter方法,假如没有查找到,就在实例变量列表中遍历查找对应的实例变量。再给出个简单的例子:

@interface FooClass : NSObject {
    id _someObject;
}

@end

@implementation FooClass

- (void)foo {
    id obj = self.someObject;
    id objx = [self valueForKey:@"someObject"];
}

@end

这里例子中,objobjx的赋值实际上都依赖于KVC。刚刚说到实例变量列表,那什么是实例变量列表呢?而且我们刚刚也一直在强调硬编码内存偏移量,意思是还存在“软编码”内存偏移量吗?

直接给出答案,class_ro_t中的ivars保存了所有实例变量的名称、大小与内存偏移量等信息。先看看ivars的定义:

struct class_ro_t {
    /***/
    const ivar_list_t *ivars;
    /***/
};

typedef struct ivar_t *Ivar;

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

    uint32_t alignment() const {
        if (alignment_raw == ~(uint32_t)0) return 1U << WORD_SHIFT;
        return 1 << alignment_raw;
    }
};

struct ivar_list_t : entsize_list_tt<ivar_t, ivar_list_t, 0> {
    bool containsIvar(Ivar ivar) const {
        return (ivar >= (Ivar)&*begin()  &&  ivar < (Ivar)&*end());
    }
};

简单转化为UML类图如下:

image.png

(这里忽略了entsize_list_tt结构体的实现,简单说来,它一共有两个成员变量:entsizeAndFlags记录数组单个item的大小,可能附带flag信息;count记录数组的item数量。从前64位起,后面才真正存储了数组数据。)

ivar实际上是instance variable的缩写,顾名思义,ivar_list_t是实例变量数组,ivar_t是实例变量。不难看出,ivar_t依次存储了实例变量的内存偏移量、名称、类型、内存对齐方式和大小。于是,如果想要在运行时实现动态访问实例变量,仅需要通过名称等信息查找到对应的ivar_t,从而找到其内存偏移量,再加上实例对象内存地址即可。

如果我们有一定的Objective-C的开发经验,一定知道两件事情:

  1. 无法给已经编译好的类添加extension
  2. 无法在category中添加实例变量

其实,这两件事情都表达了一个意思,无法改变已经编译好的类的内存布局。这里简单讲解一下,以添加实例变量为例,我们知道实例变量列表仅存在于class_ro_t中,要想实现添加实例变量的操作,就要让class_ro_t实现写入操作。其实,在Objective-C的runtime API中,提供有添加实例变量的方法class_addIvar。先给出一个简单的例子:

@interface FooClass : NSObject {
    NSString *_name;
}

@end

@implementation FooClass

@end

// some other function
FooClass *foo = [FooClass new];
[foo setValue:@"foo" forKey:@"name"];
NSLog(@"foo.name = %@", [foo valueForKey:@"name"]);

那我们该如何在运行时中动态新建FooClass并且添加实例变量_someObject呢?如下所示:

// some other function
Class FooClass = objc_allocateClassPair([NSObject class], "FooClass", 0);
class_addIvar(FooClass, "_name", sizeof(NSString *), log2(sizeof(NSString *)), @encode(NSString *));
objc_registerClassPair(FooClass);

id foo = [FooClass new];
[foo setValue:@"foo" forKey:@"name"];
NSLog(@"foo.name = %@", [foo valueForKey:@"name"]);

这里,我们先用objc_allocateClassPair创建了NSObject的子类,并获取了对应的类对象FooClassobjc_allocateClassPair的命名也是有讲究的,classPair,意思就是创建了类对,表面return的是类对象,实则元类对象也同时创建好,并被指向于类对象的isa)。接着使用class_addIvar添加实例变量,最后用objc_registerClassPair完成FooClass的注册。(其中@encode的作用为将类型转化为字符串编码,具体对应关系可以参考Apple的Type Encodings,这里不做过多的赘述。)

至此,我们已然成功使用runtime的API实现运行时动态添加实例变量。再回到上面的问题,那又是为什么编译好的类无法使用category或者extension添加实例变量呢?或者说,我们可以给编译好的类调用class_addIvar完成添加实例变量的操作吗?答案当然是否定的,我们可以试一下给NSObject添加实例变量:

// some other function
BOOL isSuccess = class_addIvar([NSObject class], "_name", sizeof(NSString *), log2(sizeof(NSString *)), @encode(NSString *));
if (isSuccess) {
    NSLog(@"NSObject can add ivar at runtime");
} else {
    NSLog(@"NSObject can't add ivar at runtime");
}

运行这段代码,我们能从控制台中获得答案:NSObject can't add ivar at runtime。这是又是为什么呢?其实Apple的官方文档已经说的很清楚了:

This function may only be called after objc_allocateClassPair and before objc_registerClassPair. Adding an instance variable to an existing class is not supported.

(From Apple

objc_registerClassPair注册类之后,class_ro_t将彻底成为一个只读结构体,禁止任何试图修改class_ro_t成员变量的行为。其实,在class_addIvar的实现中,我们也能看出端倪:

#define RW_CONSTRUCTING (1<<26)
#define UINT32_MAX 4294967295U

BOOL class_addIvar(Class cls, const char *name, size_t size, uint8_t alignment, const char *type) {
    if (!cls) return NO;

    if (!type) type = "";
    if (name  &&  0 == strcmp(name, "")) name = nil;
    
    checkIsKnownClass(cls);
    ASSERT(cls->isRealized());
    
    // No class variables
    if (cls->isMetaClass()) {
        return NO;
    }
    
    // Can only add ivars to in-construction classes.
    if (!(cls->data()->flags & RW_CONSTRUCTING)) {
        return NO;
    }
    
    // Check for existing ivar with this name, unless it's anonymous.
    // Check for too-big ivar.
    if ((name  &&  getIvar(cls, name))  ||  size > UINT32_MAX) {
        return NO;
    }
    
    class_ro_t *ro_w = make_ro_writeable(cls->data());
    
    ivar_list_t *oldlist, *newlist;
    if ((oldlist = (ivar_list_t *)cls->data()->ro()->ivars)) {
        size_t oldsize = oldlist->byteSize();
        newlist = (ivar_list_t *)calloc(oldsize + oldlist->entsize(), 1);
        memcpy(newlist, oldlist, oldsize);
        free(oldlist);
    } else {
        newlist = (ivar_list_t *)calloc(ivar_list_t::byteSize(sizeof(ivar_t), 1), 1);
        newlist->entsizeAndFlags = (uint32_t)sizeof(ivar_t);
    }

    uint32_t offset = cls->unalignedInstanceSize();
    uint32_t alignMask = (1<<alignment)-1;
    offset = (offset + alignMask) & ~alignMask;

    ivar_t& ivar = newlist->get(newlist->count++);
    
    ivar.offset = (int32_t *)(int64_t *)calloc(sizeof(int64_t), 1);
    *ivar.offset = offset;
    ivar.name = name ? strdupIfMutable(name) : nil;
    ivar.type = strdupIfMutable(type);
    ivar.alignment_raw = alignment;
    ivar.size = (uint32_t)size;
    ro_w->ivars = newlist;
    cls->setInstanceSize((uint32_t)(offset + size));

    return YES;
}

第10-11和第18-21行的判断是为了确保当前类处于构造中状态,即已调用objc_allocateClassPair,且未调用objc_registerClassPair。第13-16行的判断是为了确保当前类非元类对象,即无法为元类对象添加实例变量。而已经完成编译的类,在进行判断!(cls->data()->flags & RW\_CONSTRUCTING)时为true,导致无法运行到后续的添加ivar的逻辑。换句话说,完成编译的类已经处于已构造完成并完成注册的状态,即可以视为已调用了objc_registerClassPair,故无法在运行时动态添加实例变量。实际上,也不难理解,假如能在运行时添加实例变量,那必定会改变实例对象的内存布局,而先前的已经创建的实例变量的内存布局无法随之改变,则必将为后续的程序运行带来无法预测的安全隐患。

2.3.3. 方法的存储实现

ivars的存储实现类似,方法也是由数组的结构进行存储。不同的是,编译时确定的方法存储在class_ro_t结构体中,运行时动态添加的方法存储在class_rw_ext_t结构体中,分别对应baseMethodsmethodsbaseMethodsmethod_list_t类型,其实就是将方法类型method_t结构体组织成数组进行存储,原理类似2.3.2.讲解的ivar_list_t数组存储实现。而methodsmethod_array_t类型,是将方法列表类型method_list_t组织成数组进行存储,实现原理也比较简单,在此不做过多赘述。接下来,我们重点分析method_t结构体:

struct method_t {
    struct big {
        SEL name;
        const char *types;
        IMP imp;
    };
    
    struct small {
        RelativePointer<const void *> name;
        RelativePointer<const char *> types;
        RelativePointer<IMP, false> imp;
    };
    
    bool isSmall() const {
        return ((uintptr_t)this & 1) == 1;
    }
    
    small &small() const {
        ASSERT(isSmall());
        return *(struct small *)((uintptr_t)this & ~(uintptr_t)1);
    }
    
    big &big() const {
        ASSERT(!isSmall());
        return *(struct big *)this;
    }
    
    ALWAYS_INLINE SEL name() const {
        if (isSmall()) {
            if (small().inSharedCache()) {
                return (SEL)small().name.get(sharedCacheRelativeMethodBase());
            } else {
                return *(SEL *)small().name.get();
            }
        } else {
            return big().name;
        }
    }
    
    const char *types() const {
        return isSmall() ? small().types.get() : big().types;
    }
    
    IMP imp(bool needsLock) const {
        return isSmall() ? small().imp.get() : big().imp;
    }
    
    static uintptr_t sharedCacheRelativeMethodBase() {
        return (uintptr_t)@selector(🤯);
    }            
}

template <typename T, bool isNullable = true>
struct RelativePointer: nocopy_t {
    int32_t offset;
    
    void *getRaw(uintptr_t base) const {
        if (isNullable && offset == 0)
            return nullptr;
        uintptr_t signExtendedOffset = (uintptr_t)(intptr_t)offset;
        uintptr_t pointer = base + signExtendedOffset;
        return (void *)pointer;
    }
    
    void *getRaw() const {
        return getRaw((uintptr_t)&offset);
    }    
    
    T get(uintptr_t base) const {
        return (T)getRaw(base);
    }
    
    T get() const {
        return (T)getRaw();
    }    
};

简单转化为UML类图如下:

image.png

不难看出,method_t结构体实际上存在两种不同的实现,一种实现为method_t::big结构体,另一种实现为method_t::small结构体,为了方便讨论,我们称其为method_big结构体与method_small结构体。那问题来了,二者在结构上看起来没啥区别,一样存储了nametypesimp,为什么要区分成两种不同的版本呢?

其实,在上面提到的Apple在WWDC 2020发布的视频Advancements in the Objective-C runtime就有对此的讲解。这里简单总结一下,实际上顾名思义,method_big结构体代表了内存占用大的实现版本(以前的实现其实就是method_big结构体版本),method_small结构体代表了内存占用小的实现版本。我们知道,一个method_t结构体需要存储的有三条重要信息,根据method_big结构体的实现,我们知道存储的三个信息都为指针类型,故在64位系统中一个method_t结构体就需要占用24个字节。而在method_small结构体中,我们发现它存储的并不是指针类型,而是内存的偏移量,并且类型为int32_t,所以在method_small结构体仅占用12个字节,比起method_big结构体真正缩小了一半的内存空间。那么问题来了,为什么可以这么做?

image.png

如图,很直观的可以知道,假如使用method_big结构体,因为dyld将二进制文件映射到内存的位置都是随机的,所以每次映射都需要修正method_big结构体的指针指向。

image.png

假如使用method_small结构体,dyld可以直接把method_small结构体进行映射,而不需要额外的修正操作。这是因为dylib中变量的内存位置总是“相邻”的,即相对于一个变量,另一个变量的内存偏移量总在-2GB~+2GB之间,而这个偏移量在编译时就已经确定了,并且在dyld对dylib进行加载及映射到内存的过程中并不会改变这个偏移量,或者说变量间的相对位置是不变的,所以,实际上dylib上的method_t结构体并不需要完整的64位数据(整个指针)来索引到相关的数据,仅需记录32位的偏移量数据即可索引到相关数据。

综上可知,method_small结构体相比起method_big结构体,在内存占用上更小,加载的时间代价也更小。但method_big结构体的优势在于更加灵活,内存索引空间也更大。所以,dylib会尽量优化为method_small结构体,而在运行时可能需要动态修改method_t的可执行文件仍会采用method_big结构体。

有些人可能注意到method_small结构体在获取SELname)的时候,进行了inSharedCache()判断。这个与dyld的共享缓存有关,具体可以参考Apple在WWDC 2017发布的视频——App Startup Time: Past, Present, and Future,这里不展开讲解。

2.4. 方法缓存cache

2.4.1. 缓存结构

在实践中,一个类的方法往往只有部分是会经常被调用的,如果所有方法都需要到方法列表里面去查找(方法列表的实现是数组,通过遍历列表来实现查找),那么就会造成效率低下。所以,Objective-C在实现的里就考虑使用缓存来保存经常调用的方法。而cache_t结构体存储了方法缓存:

typedef uint32_t mask_t; 

struct cache_t {
    explicit_atomic<uintptr_t> _bucketsAndMaybeMask;
    union {
        struct {
            explicit_atomic<mask_t>    _maybeMask;
            uint16_t                   _flags;
            uint16_t                   _occupied;
        }
        explicit_atomic<preopt_cache_t *> _originalPreoptCache;
    }
    static constexpr uintptr_t bucketsMask = ~0ul;
    
    mask_t mask() const;
    struct bucket_t *buckets() const;
    unsigned capacity() const;
    mask_t occupied() const;
}

struct bucket_t {
    explicit_atomic<SEL> _sel;
    explicit_atomic<uintptr_t> _imp;
    
    inline SEL sel() const { return _sel.load(memory_order_relaxed); }
    inline IMP imp(UNUSED_WITHOUT_PTRAUTH bucket_t *base, Class cls) const {
        uintptr_t imp = _imp.load(memory_order_relaxed);
        if (!imp) return nil;
        return (IMP)(imp ^ (uintptr_t)cls);
    }    
}

mask_t cache_t::mask() const {
    return _maybeMask.load(memory_order_relaxed);
}

struct bucket_t *cache_t::buckets() const {
    uintptr_t addr = _bucketsAndMaybeMask.load(memory_order_relaxed);
    return (bucket_t *)(addr & bucketsMask);
}

unsigned cache_t::capacity() const {
    return mask() ? mask()+1 : 0; 
}

mask_t cache_t::occupied() const {
    return _occupied;
}

简单转化为UML类图如下:

image.png

cache_t结构体其实很简单,通过buckets()得到缓存散列表(哈希表);通过capacity()得到缓存散列表的总容量;通过occupied()得到缓存散列表有多少个被占用的bucket。而bucket_t结构体存储了方法选择器SEL和对应的函数指针IMP

这里我们注意到,bucket_t获取IMP是通过方法imp(UNUSED_WITHOUT_PTRAUTH bucket_t *base, Class cls)获取的。第一个入参在非指针身份认证的系统里没有用处(在iPhone X开始,增加了指针身份认证的安全检验过程,可以在Apple的Preparing Your App to Work with Pointer Authentication文档中了解详情,这里不做赘述),本文的编译平台没有指针身份认证的过程,故忽略。第二个入参用于函数指针的解码,而解码的过程也十分简单,就是将缓存所在的类对象或者元类对象的Class指针与bucket_t结构体实际存储的_imp做一次异或运算。既然存在解码过程,必然存在编码过程,接下来我们看看bucket_t是如何编码并存储SELIMP的:

enum Atomicity { Atomic = true, NotAtomic = false };
enum IMPEncoding { Encoded = true, Raw = false };

struct bucket_t {
    uintptr_t encodeImp(UNUSED_WITHOUT_PTRAUTH bucket_t *base, IMP newImp, UNUSED_WITHOUT_PTRAUTH SEL newSel, Class cls) const {
        if (!newImp) return 0;
        return (uintptr_t)newImp ^ (uintptr_t)cls;
    }    

    template<Atomicity atomicity, IMPEncoding impEncoding>
    void set(bucket_t *base, SEL newSel, IMP newImp, Class cls) {
        ASSERT(_sel.load(memory_order_relaxed) == 0 ||
               _sel.load(memory_order_relaxed) == newSel);
        uintptr_t newIMP = (impEncoding == Encoded
                            ? encodeImp(base, newImp, newSel, cls)
                            : (uintptr_t)newImp);
        if (atomicity == Atomic) {
            _imp.store(newIMP, memory_order_relaxed);
        
            if (_sel.load(memory_order_relaxed) != newSel) {
                _sel.store(newSel, memory_order_release);
            }
        } else {
            _imp.store(newIMP, memory_order_relaxed);
            _sel.store(newSel, memory_order_relaxed);
        }
    }
}

通过方法encodeImp()可以发现,函数指针的编码过程也是将缓存所在的类对象或者元类对象的Class指针与函数指针做一次异或运算。这里面涉及的数学原理很简单,简而言之就是A==A^B^B。而方法set()中,我们注意到当为原子写入时(atomicity == Atomic),_sel的写入的内存顺序是memory_order_release。这是因为objc_msgSend对方法缓存的读写不进行加锁操作,但是当_imp有值而_sel为空对objc_msgSend来说是安全的,而_sel不为空且_imp为旧值对objc_msgSend来说是不安全的。故当需要原子写入时,需要确保当进行_sel的写入时,_imp已经完成写入操作,所以选择_sel的写入的内存顺序为memory_order_release

2.4.2. 读写缓存

2.4.2.1. 添加方法缓存

接下来讲解cache_t结构体是如何添加方法缓存的,照例先上源码:

#define CACHE_END_MARKER 1
enum {
    INIT_CACHE_SIZE_LOG2 = 2,
    INIT_CACHE_SIZE      = (1 << INIT_CACHE_SIZE_LOG2),
    MAX_CACHE_SIZE_LOG2  = 16,
    MAX_CACHE_SIZE       = (1 << MAX_CACHE_SIZE_LOG2),    
};

struct cache_t {
    void incrementOccupied();
    void setBucketsAndMask(struct bucket_t *newBuckets, mask_t newMask);
    size_t bytesForCapacity(uint32_t cap);
    bucket_t *endMarker(struct bucket_t *b, uint32_t cap);
    bucket_t *allocateBuckets(mask_t newCapacity);
    
    void reallocate(mask_t oldCapacity, mask_t newCapacity, bool freeOld);
    void collect_free(bucket_t *oldBuckets, mask_t oldCapacity);
        
    void insert(SEL sel, IMP imp, id receiver);
};

void cache_t::insert(SEL sel, IMP imp, id receiver) {
    mask_t newOccupied = occupied() + 1;
    unsigned oldCapacity = capacity(), capacity = oldCapacity;
    if (isConstantEmptyCache()) {
        if (!capacity) capacity = INIT_CACHE_SIZE;
        reallocate(oldCapacity, capacity, false);
    } else if (newOccupied + CACHE_END_MARKER > cache_fill_ratio(capacity)) {
        capacity = capacity ? capacity * 2 : INIT_CACHE_SIZE;
        if (capacity > MAX_CACHE_SIZE) {
            capacity = MAX_CACHE_SIZE;
        }
        reallocate(oldCapacity, capacity, true);
    }
    
    bucket_t *b = buckets();
    mask_t m = capacity - 1;
    mask_t begin = cache_hash(sel, m);
    mask_t i = begin;
    do {
        if (b[i].sel() == 0) {
            incrementOccupied();
            b[i].set<Atomic, Encoded>(b, sel, imp, cls());
            return;
        }
        if (b[i].sel() == sel) {
            return;
        }
    } while ((i = cache_next(i, m)) != begin);
}

static inline mask_t cache_next(mask_t i, mask_t mask) {
    return (i+1) & mask;
}

static inline mask_t cache_fill_ratio(mask_t capacity) {
    return capacity * 3 / 4;
}

static inline mask_t cache_hash(SEL sel, mask_t mask) {
    uintptr_t value = (uintptr_t)sel;
    return (mask_t)(value & mask);
}

void cache_t::incrementOccupied() {
    _occupied++;
}

void cache_t::reallocate(mask_t oldCapacity, mask_t newCapacity, bool freeOld) {
    bucket_t *oldBuckets = buckets();
    bucket_t *newBuckets = allocateBuckets(newCapacity);

    setBucketsAndMask(newBuckets, newCapacity - 1);
    
    if (freeOld) {
        collect_free(oldBuckets, oldCapacity);
    }
}

void cache_t::setBucketsAndMask(struct bucket_t *newBuckets, mask_t newMask) {
    _bucketsAndMaybeMask.store((uintptr_t)newBuckets, memory_order_release);
    _maybeMask.store(newMask, memory_order_release);
    _occupied = 0;
}

size_t cache_t::bytesForCapacity(uint32_t cap) {
    return sizeof(bucket_t) * cap;
}

bucket_t *cache_t::endMarker(struct bucket_t *b, uint32_t cap) {
    return (bucket_t *)((uintptr_t)b + bytesForCapacity(cap)) - 1;
}

bucket_t *cache_t::allocateBuckets(mask_t newCapacity) {
    bucket_t *newBuckets = (bucket_t *)calloc(bytesForCapacity(newCapacity), 1);
    bucket_t *end = endMarker(newBuckets, newCapacity);
    
    end->set<NotAtomic, Raw>(newBuckets, (SEL)(uintptr_t)1, (IMP)newBuckets, nil);

    return newBuckets;
}

可以看出,添加方法缓存的实现是非常简单的,就是超过3/4容量就扩容翻倍。对照代码绘制等效流程图如下:

image.png

注意到在buckets扩容的过程中,是直接将扩容前的buckets释放掉而不是将其重新完整拷贝。这是其实是为了性能考虑,因为如果将旧的缓存拷贝到新缓存上会导致时间代价太大。

还有一点需要注意的是buckets的最后一个bucket_sel被设为1、_imp被设为第一个bucket的内存地址。

2.4.2.2. 查找方法缓存

为了极致的性能考虑,Apple使用汇编语言来实现查找方法缓存,并且提供了一个C/C++的API(cache_getImp())。而且,查找方法缓存是不对缓存进行加锁一类的读写互斥处理的(如何防止同时读写出现问题,参考2.4.2.1.讲解的bucket_t结构体set()方法的实现,这里不做赘述)。汇编语言终归是过于晦涩了,这里我们使用C++在遵照原汇编实现性能考虑的基础上对其进行了一定的改写,如下:

extern "C" IMP cache_getImp(Class cls, SEL sel, IMP value_on_constant_cache_miss = nil);

// asm to C++
IMP cache_getImp(Class cls, SEL sel, IMP value_on_constant_cache_miss = nil) {
    struct cache_t cache = cls->cache;
    
    if (cache.occupied() == 0) {
        return value_on_constant_cache_miss;
    }
    
    struct bucket_t *buckets = cache.buckets();
    mask_t mask = cache.mask();
    mask_t begin = (mask_t)((uintptr_t)sel & mask);
    struct bucket_t *bucket = (struct bucket_t *)((uintptr_t)buckets + 16 * begin);
    
    do {
        SEL _sel = bucket->_sel;
        
        if (_sel == sel) {
            return (IMP)(bucket->_imp ^ (uintptr_t)cls);
        }
        if (_sel == (SEL)0) {
            return value_on_constant_cache_miss;
        }
        
        if (_sel != (SEL)1) {
            bucket = (struct bucket_t *)((uintptr_t)bucket + 16);
        } else {
            bucket = (struct bucket_t *)(bucket->_imp);
        }
    } while (true);
    
    return value_on_constant_cache_miss;
}

可以看到,我们这段代码是直接使用buckets的内存地址加内存偏移量对其进行遍历读取的,这样做的目的是让CPU少做一次乘法运算(常规的数组读取buckets[i]实际上是buckets+ i * sizeof(bucket_t))。这里,我们也能清楚的知道为什么在插入方法缓存时需要将最后一个bucket存储上第一个bucket的内存地址,原因就是为了方便汇编语言在遍历到最后一个bucket时跳转到第一个bucket进行下一次遍历。

2.5. 小结

以上,我们讲解了Objective-C在面向对象上的实现及其实现结构,并且,我们可以知道了Objective-C的类可以在运行时动态地进行一定的修改。那我们来看看,实际在开发工作中,我们如何将这些知识合理的运用。

2.5.1. 实现给category添加属性

首先看一个category的定义:

@interface FooClass (Foo)

@property (nonatomic, strong) id fooObj;

@end

在一些场景中,我们可能需要给已经编译好的类添加属性来实现一些特定代码逻辑。可是在2.3.2.中,我们已经讲解了category是无法添加实例变量的,如此一来,FooClass (Foo)就无法自动给属性fooObj生成setter函数和getter函数。那是不是对此我们就毫无办法了呢?当然不是!在2.2.中,我们提到isa的低2比特代表has_assoc,即表示当前对象是否具有或者曾经具有关联对象。何为关联对象?即一个对象能关联另一个对象,并且拥有它的生命周期控制权。(可能有点绕,想要详细了解的同学可以参考这个链接: nshipster.cn/associated-…

对应的,要想使用关联对象,得使用对应的runtime API:

void objc_setAssociatedObject(id object, const void *key, id value, objc_AssociationPolicy policy);

id objc_getAssociatedObject(id object, const void *key);

看起来非常简单,objc_setAssociatedObject表示设置关联对象,objc_getAssociatedObject表示获取关联对象。它们的第一个入参object都是被关联的对象,而第二个入参key都是一个64位的键值(虽然他是指针类型,但实际上它并不会尝试去读取指针指向的内容,故将其理解为64位的键值比较合理)。objc_setAssociatedObject的第三个入参value是关联对象,而第四个入参policy是关联策略。policyobjc_AssociationPolicy类型,而objc_AssociationPolicy其实是个枚举类型,定义如下:

typedef OBJC_ENUM(uintptr_t, objc_AssociationPolicy) {
    OBJC_ASSOCIATION_ASSIGN = 0,

    OBJC_ASSOCIATION_RETAIN_NONATOMIC = 1,
    OBJC_ASSOCIATION_COPY_NONATOMIC = 3,
                                         
    OBJC_ASSOCIATION_RETAIN = 01401,
    OBJC_ASSOCIATION_COPY = 01403
 ;

通过命名,也能知道每个枚举值对应的具体含义这里就不再赘述了。言归正传,我们来看看它具体如何实现给category添加属性:

@implementation FooClass (Foo)

- (void)setFooObj:(id)obj {
    objc_setAssociatedObject(self, @selector(fooObj), obj, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
}

- (id)fooObj {
    return objc_getAssociatedObject(self, _cmd);
}

@end

注意到,我们这里给入参key赋的是SEL值,后面的讲解会提到同一个selector名对应同一个SEL值,故将其作为唯一标识符赋给入参key是合理的。

至此,我们就实现了给category添加属性。

其实,关联对象也可以关联上类对象,这样就能实现“类属性”的操作。方法与上述方法大差不差,这里不展开赘述。

2.5.2. 实现高效序列化与反序列化对象

众所周知,想要将一个Objective-C对象序列化存储到disk上或反序列化读取到memory上,需要实现协议NSCoding,即实现实例方法encodeWithCoderinitWithCoder。一般情况下,我们会如此实现:

@interface FooClass : NSObject <NSCoding>

@property (nonatomic, strong) id obj1;
/***/
@property (nonatomic, strong) id objN;

@end

@implementation FooClass

- (void)encodeWithCoder:(NSCoder *)coder {
    [coder encodeObject:self.obj1 forKey:@"obj1"];
    /***/
    [coder encodeObject:self.objN forKey:@"objN"];
}

- (nullable instancetype)initWithCoder:(NSCoder *)coder {
    if (self = [super init]) {
        self.obj1 = [coder decodeObjectForKey:@"obj1"];
        /***/
        self.objN = [coder decodeObjectForKey:@"objN"];
    }
    return self;
}

@end

如此实现,带来两个弊端,一个是当属性较多时,代码实现比较繁琐;另一个是后期可拓展性不强,假如后期迭代的过程中增删属性,就需要对应着修改实例方法encodeWithCoderinitWithCoder。那有没有一劳永逸的方法?当然有,考虑到一般我们序列化或反序列化的过程中,仅需要存储实例变量,我们在2.4.2.2.中讲解过实例变量实质上就是ivar,可以通过获取ivar数组进行遍历编解码操作。这里给出所有实例变量都为Objective-C对象的实现方式:

@implementation FooClass

- (void)encodeWithCoder:(NSCoder *)coder {
    unsigned int outCount = 0;
    Ivar *vars = class_copyIvarList([self class], &outCount);
    for (unsigned int index = 0; index < outCount; ++index) {
        Ivar var = vars[index];
        NSString *key = [NSString stringWithCString:ivar_getName(var) encoding:typeUTF8Text];

        id value = [self valueForKey:key];
        [coder encodeObject:value forKey:key];
    }
}

- (nullable instancetype)initWithCoder:(NSCoder *)coder {
    if (self = [super init]) {
        unsigned int outCount = 0;
        Ivar *vars = class_copyIvarList([self class], &outCount);
        for (unsigned int index = 0; index < outCount; ++index) {
            Ivar var = vars[i];
            NSString *key = [NSString stringWithCString:ivar_getName(var) encoding:typeUTF8Text];
            
            id value = [coder decodeObjectForKey:key];
            [self setValue:value forKey:key];
        }
    }
    return self;
}

@end

当实例变量存在非Objective-C对象时,使用runtime APIivar_getTypeEncoding配合NSCoderencodeValueOfObjCType使用,这里不展开赘述。

对于需要存储属性,可以通过class_copyPropertyList获取属性列表,通过属性不同特性(attribute)实现需要的操作。

2.5.3. 实现优雅添加业务埋点

假如我们有一个业务需求,需要在所有的ViewControllerviewDidLoad生命周期中添加业务埋点逻辑或者一些重复性的工作,一般情况下,我们有两种实现方案。一种是将所有的ViewController定义成继承UIViewController的子类,然后重写方法viewDidLoad

@interface FooVC_1_1 :  UIViewController

@end

@implementation FooVC_1_1

- (void)viewDidLoad {
    [super viewDidLoad];
    
    /**
    tracker operation
    */
    
    /**
    other operation
    */
}

@end

/***/

@interface FooVC_1_N :  UIViewController

@end

@implementation FooVC_1_N

- (void)viewDidLoad {
    [super viewDidLoad];
    
    /**
    tracker operation
    */
    
    /**
    other operation
    */
}

@end

这种方法的缺点就是太繁琐了,并且如果是UIViewController实例对象将无法执行埋点逻辑。

另一种方法是定义一个BaseViewController,在BaseViewController中重写方法viewDidLoad并且让其它的ViewController继承BaseViewController

@interface BaseViewController : UIViewController

- (void)viewDidLoad {
    [super viewDidLoad];
    
    /**
    tracker operation
    */
}

@end

@interface FooVC_2_1 :  BaseViewController

@end

@implementation FooVC_2_1

- (void)viewDidLoad {
    [super viewDidLoad];
    
    /**
    other operation
    */
}

@end

/***/

@interface FooVC_2_N :  BaseViewController

@end

@implementation FooVC_2_N

- (void)viewDidLoad {
    [super viewDidLoad];

    /**
    other operation
    */
}

@end

这种方法的缺点同样是UIViewController实例对象无法执行埋点逻辑,并且每次新增一个埋点逻辑都需要在BaseViewController的源码文件中进行修改。

考虑到在2.3.3.中我们讲解过,方法的实质是method_t结构体,理论上在运行时是可以修改method_t结构体的成员变量imp,即达到了Hook方法的效果。于是,Objective-C的runtime中最有“魅力”的操作——方法混合(Method Swizzling)诞生了!

@interface UIViewController (Tracker)

@end

@interface UIViewController (Tracker)

+ (void)load {
    static dispatch_once_t onceFlag;
    dispatch_once(&onceFlag, ^{
        Class class = [self class];
        SEL originalSelector = @selector(viewDidLoad);
        SEL swizzledSelector = @selector(tracker_viewDidLoad);
        Method originalMethod = class_getInstanceMethod(class, originalSelector);
        Method swizzledMethod = class_getInstanceMethod(class, swizzledSelector);
        BOOL didAddMethod = class_addMethod(class,
                                            originalSelector,
                                            method_getImplementation(swizzledMethod),
                                            method_getTypeEncoding(swizzledMethod));
        if (didAddMethod) {
            class_replaceMethod(class,
                                swizzledSelector,
                                method_getImplementation(originalMethod),
                                method_getTypeEncoding(originalMethod));
        } else {
            method_exchangeImplementations(originalMethod, swizzledMethod);
        }
    });
}

- (void)tracker_viewDidLoad {
    /**
    tracker operation
    */
    
    [self tracker_viewDidLoad];
}

@end

类方法load在类和类别加载的时候会自动调用(对此感兴趣的可以参考链接: developer.apple.com/documentati… ),于是,我们可以在此方法中,先尝试使用class_addMethod添加SELviewDidLoadIMP_I_UIViewController_Tracker_tracker_viewDidLoad的方法。但是class_addMethod只能给当前类(不会判断父类)原本没有的SEL添加方法,因UIViewController一定有SELviewDidLoad的方法,故其实在这个例子里class_addMethod会返回NO(但对于一些继承而来的子类仍有判断的必要)。如果class_addMethod里成功添加了方法,那么使用class_replaceMethod将原本SELtracker_viewDidLoad的方法替换IMP_I_UIViewController_viewDidLoad即可。而这里的例子将会执行method_exchangeImplementations的逻辑,即将SELviewDidLoadSELtracker_viewDidLoad的方法交换IMP。这样就实现了实例对象调用viewDidLoad时实际上调用了_I_UIViewController_Tracker_tracker_viewDidLoad函数,而在tracker_viewDidLoad方法实现的最后调用了tracker_viewDidLoad将实际上调用_I_UIViewController_viewDidLoad函数。这样,在开发者的视角就相当于将两个不同的方法混合起来了!

UIViewController (Tracker)的原理图如下:

image.png

再举个例子:

@interface BaseViewControler : UIViewController

@end

@interface BaseViewControler

+ (void)load {
    static dispatch_once_t onceFlag;
    dispatch_once(&onceFlag, ^{
        Class class = [self class];
        SEL originalSelector = @selector(viewDidLoad);
        SEL swizzledSelector = @selector(tracker_viewDidLoad);
        Method originalMethod = class_getInstanceMethod(class, originalSelector);
        Method swizzledMethod = class_getInstanceMethod(class, swizzledSelector);
        BOOL didAddMethod = class_addMethod(class,
                                            originalSelector,
                                            method_getImplementation(swizzledMethod),
                                            method_getTypeEncoding(swizzledMethod));
        if (didAddMethod) {
            class_replaceMethod(class,
                                swizzledSelector,
                                method_getImplementation(originalMethod),
                                method_getTypeEncoding(originalMethod));
        } else {
            method_exchangeImplementations(originalMethod, swizzledMethod);
        }
    });
}

- (void)tracker_viewDidLoad {
    /**
    tracker operation
    */
    
    [self tracker_viewDidLoad];
}

@end

BaseViewController的原理图如下:

image.png


3. 消息发送与转发

虽然都在说Objective-C模仿了Smalltalk式的消息传递机制,但大部分人对Smalltalk不甚了解。这里不会讲解有关Smalltalk的内容,不过我们倒是可以看一下Smalltalk的经典消息传递语法:

receiver message

有没有发现它跟Objective-C的方法调用语法很相似?

[receiver message];

Objective-C在方法调用上与Smalltalk的区别就是多了一对中括号。其实这是Objective-C为了方便编译器实现嵌套的方法调用解析,故意偷的懒。

言归正传,上面的讲解中其实说到了一点,实际上编译器会将Objective-C的方法调用语法翻译成调用Objective-C的runtime API。以这个方法调用为例:

objc_msgSend(receiver, @selector(message));

我们也可以通过自然语言来理解这个过程——"Send message to receiver"。

接下来,我们围绕Objective-C的方法调用(消息传递)机制进行讲解。

实际上,与objc_msgSend类似作用的runtime API还有三个objc_msgSend_fpretobjc_msgSend_fp2retobjc_msgSend_stret。其中,objc_msgSend_fpretobjc_msgSend_fp2ret在arm上没有作用,x86-64分别用于方法返回类型为long double_Complex long double的情况。而objc_msgSend_stret用于方法返回值为结构体类型的情况。

3.1. 重新认识消息

我们日常开发中,经常使用@selector()来获取方法选择器SEL,那什么是具体什么是SEL呢?这里直接给出定义:

typedef struct objc_selector *SEL;
typedef id _Nullable (*IMP)(id _Nonnull, SEL _Nonnull, ...);

通过定义,我们能清楚的知道,SEL实际上是objc_selector结构体指针。那问题又来了,什么是objc_selector结构体呢?可惜的是,runtime源码中并没有给出objc_selector结构体的实现,并且Apple官方文档和源码的注释中都提到了一点:

Defines an opaque type that represents a method selector.

(From Apple

也就是说,objc_selector结构体可以理解为一个神秘的类型,并且实际上,我们可以直接把SEL当作方法选择器的64位UID来使用,即理解成:

typedef uintptr_t SEL;

@selector()其实是sel_registerName()的语法糖,sel_registerName()的定义是:

SEL sel_registerName(const char *name);

它的作用就是将方法选择器的名称注册到全局散列表(哈希表)中,并返回一个SEL。所以,实际上SEL的值仅仅与方法选择器的字符串名有关,并且在当前进程生命周期中,无论何时调用@selector(),都能返回一样的SEL值。故将其理解为方法选择器的64位UID也无可厚非(实际上,还有另一个API,sel_getUid(),它的实现与sel_registerName()一摸一样,只是API的名称不同)。

至此,其实我们也能明白为什么Objective-C不支持方法重载,就是因为SEL仅仅与方法选择器的名称有关,不管入参的类型或者方法返回的类型如何改变,只要名称不变,SEL的值就恒定不变。

我们知道,Objective-C的方法调用实际上是模拟向接收者发送消息的过程,而消息指的是就是SEL的值,或者说SEL就是消息名。消息名本身仅存储了字符串信息,而接收者如何消费消息,仅通过消息名是不够的。于是,我们需要通过消息名能查找到对应的方法,才能实现消息响应的过程。这时候注意到在2.3.3.讲解的方法类型method_t结构体,除了存储了SEL类型的name之外,还存储了IMP类型的impconst char *类型的types。这样,我们就能通过消息名找到相同消息名的方法,实现方法调用。

于是,我们接下来研究一下method_t结构体。这里先给出IMP的定义:

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

我们看到,IMP实际上是函数指针类型。这样我们就能在method_t结构体中找到对应的实现函数,继而实现方法调用等一系列操作。注意到IMP的至少有两个入参,第一个是id类型,第二个是SEL类型。那又是为什么如此设计呢?这里我们给个例子:

@interface FooClass : NSObject

- (void)foo;

@end

@implementation FooClass

- (void)foo {
    return;
}

@end

在这个例子中,我们在FooClass内定义一个简单的实例方法foo。接着,我们使用Clang将这段Objective-C源码翻译成C/C++:

static void _I_FooClass_foo(FooClass *self, SEL _cmd) {
    return;
}

这里我们很清楚的看到,实例方法foo被翻译为静态函数_I_FooClass_foo(这个命名是有讲究的,后面会对此进行讲解),而且也很清楚看到有两个入参self_cmd。这时候可能有些同学反应过来了,我们日常中使用的self实际上不是什么特殊的关键字,而是翻译后的静态函数的第一个入参。这里直接给出结论,所有的Objective-C方法都存在两个隐藏入参——self_cmd。当通过Objective-C方法调用的方式进行方法调用时,第一个入参self会被赋值为接收者(receiver),第二个入参_cmd会被赋值为消息(message、selector)。

所以,当我们调用FooClass的实例方法foo时,实际上调用的是_I_FooClass_foo函数,而且,入参selfFooClass的实例对象,而入参_cmd@selector(foo)的返回值。

这时候可能有人会疑惑了,self_cmd都是函数的入参,那super呢?实际上,super不是函数入参,而是objc_msgSendSuper的语法糖。举个例子说明:

- (void)foo {
    [super foo];
    return;
}

我们这里调用了[super foo],实际上这个语法糖等价于:

- (void)foo {
    struct objc_super fooSuperClass;
    fooSuperClass.receiver = self;
    fooSuperClass.super_class = [FooClass superclass];
    objc_msgSendSuper(&fooSuperClass, @selector(foo));
    return;
}

从这个例子,我们能得出,objc_msgSendSuperobjc_msgSend类似,入参至少有两个,并且第二个参数为SEL值。而objc_super有两个成员函数,receiversuper_classreceiver被赋值为方法的第一个入参self,而super_class则在编译期间就固定为FooClass的父类。(后面会对此展开详细讲解,先按下不表)

同样的,与objc_msgSend类似,objc_msgSendSuper_stret用于方法返回值为结构体类型的情况。

特别注意的是,实际上编译过程中super会翻译为调用objc_msgSendSuper2,与objc_msgSendSuper不同的是,objc_super结构体的成员变量super_class赋值为己类而非父类。在objc_msgSendSuper2的实现中通过receiverisa获取父类,故性能上也优于objc_msgSendSuper

另外,除了特殊情况不建议开发者将super翻译成objc_msgSendSuper进行调用,因为容易带来无限递归的隐患。(可以思考为什么上面的代码示例中,fooSuperClass.super_class赋值为[FooClass superclass],而不是[self superclass]

接着探讨方法,对于实例方法foo,翻译后是静态函数_I_FooClass_foo,那假如我们定义一个同样命名的类方法foo呢?再举个例子:

@interface FooClass : NSObject

- (void)foo;
+ (void)foo;

@end

@implementation FooClass

- (void)foo {
    return;
}

+ (void)foo {
    return;
}

@end

使用Clang将这段Objective-C源码翻译成C/C++:

static void _I_FooClass_foo(FooClass * self, SEL _cmd) {
    return;
}

static void _C_FooClass_foo(Class self, SEL _cmd) {
    return;
}

同样的,我们尝试构建FooClass的category,并添加同样命名为foo的实例方法和类方法:

@interface FooClass (FooCategory)

- (void)foo;
+ (void)foo;

@end

@implementation FooClass (FooCategory)

- (void)foo {
    return;
}

+ (void)foo {
    return;
}

@end

使用Clang将这段Objective-C源码翻译成C/C++:

static void _I_FooClass_FooCategory_foo(FooClass * self, SEL _cmd) {
    return;
}

static void _C_FooClass_FooCategory_foo(Class self, SEL _cmd) {
    return;
}

至此,我们已经很轻易的得出Objective-C方法实际对应的函数命名方式:

_prefix_className(_categoryName)_methodName

其中,前缀I和C分别表示实例方法(Instance Method)与类方法(Class Method)。通过命名方式,我们也能得知为什么Objective-C虽然不支持方法重载,却能通过类别重写方法,因为通过类别重写的方法本质上就不是同一个函数。

我们注意到,method_t结构体还有const char *类型的成员变量types,它描述的是函数指针的返回类型和入参类型。因为我们在实际运用中,除了希望接收者(receiver)处理消息,还能根据不同的附带参数返回我们想要的返回类型。所以我们需要一个字符串类型的变量来描述这些不同的类型。还是举个简单的例子:

@interface FooClass : NSObject

- (void)sayHelloTo:(id)foo;

@end

@implementation FooClass

- (void)sayHelloTo:(id)foo {
    NSLog(@"%@ %@ %@", self, NSStringFromSelector(_cmd), foo);
}

@end

假如我们需要在运行时添加同样功能的方法,可以如下操作:

void sayHello(id self, SEL _cmd, id foo) {
    NSLog(@"%@ %@ %@", self, NSStringFromSelector(_cmd), foo);
}

// some other function
class_addMethod([FooClass class], self(sayHelloTo:), sayHello, "v@:@");

注意到,字符串"v@:@"就描述了sayHello函数的返回值类型和入参类型,具体可以参考Apple的Type Encodings,这里不做过多的赘述。

至此,我们能完整且清楚地知道消息具体的实现方式,以及在方法调用这个场景下,消息是如何与方法代码进行绑定的。

3.2. 消息发送

3.2.1. 沿继承链查找方法

在3.1.中,我们讲解了消息,接下来,我们的重点就是探究消息是如何进行发送的。我们知道,在Objective-C里面发送消息的大部分场景中,实际上是调用objc_msgSend函数,在runtime的源码中,objc_msgSend函数的实现是由汇编语言实现的(采用汇编语言实现objc_msgSend函数除了有性能和CPU架构上的考虑,还有就是汇编语言能更优雅地应对可变参数,不过这里不做深入探讨)。这里我们仍使用C++在遵照原汇编实现性能考虑的基础上对其进行了一定的改写(没有想到一个比较好的可变参转发,如下:

enum {
    LOOKUP_INITIALIZE = 1,
    LOOKUP_RESOLVER = 2,
    LOOKUP_NIL = 4,
    LOOKUP_NOCACHE = 8,
};

id objc_msgSend(id self, SEL _cmd, ...) {
    if (!self) {
        return nil;
    }
    
    Class cls = (self -> isa) & ISA_MASK;
    
    IMP imp = cache_getImp(cls, _cmd);
    if (imp) {
        return imp(self, _cmd, ...);
    }
    
    imp = lookUpImpOrForward(self, _cmd, cls, LOOKUP_INITIALIZE | LOOKUP_RESOLVER);
    return imp(self, _cmd, ...);
}

可以看到,实际上objc_msgSend函数就干了四件事:

  1. Nil test

判断入参self是否为空,若为空返回nil

  1. Get class

selfisaISA_MASK做或运算,即得到当前的类。

  1. Get imp in cache

isa指向的方法缓存中尝试获取imp,若成功获取,直接进行方法调用。(可以参考2.4.2.2.中查找方法缓存的实现)

  1. Lookup imp in method list

在方法列表中查找imp,并直接调用。

objc_msgSendSuper函数与objc_msgSend函数在实现上基本无异,只是在【2. Get class】里当前类取的是objc_super结构体的成员变量super_class

注意到,我们在方法列表中查找imp时,调用了函数lookUpImpOrForward,直接给出源码:

IMP lookUpImpOrForward(id inst, SEL sel, Class cls, int behavior) {
    const IMP forward_imp = (IMP)_objc_msgForward_impcache;
    IMP imp = nil;
    Class curClass = cls;
    
    for (;;) {
        method_t *meth = getMethodNoSuper_nolock(curClass, sel);
        if (meth) {
            imp = meth->imp(false);
            goto done;
        }

        if ((curClass = curClass->getSuperclass()) == nil)) {
            imp = forward_imp;
            break;
        }
        
        imp = cache_getImp(curClass, sel);
        if (imp == forward_imp) {
            break;
        }
        if (imp) {
            goto done;
        }
    }
    
    if (behavior & LOOKUP_RESOLVER) {
        behavior ^= LOOKUP_RESOLVER;
        return resolveMethod_locked(inst, sel, cls, behavior);
    }
    
done:
    cls->cache.insert(sel, imp, receiver)
    if ((behavior & LOOKUP_NIL) && imp == forward_imp)) {
        return nil;
    }
    return imp;    
}

static method_t *
getMethodNoSuper_nolock(Class cls, SEL sel) {
    auto const methods = cls->data()->methods();
    for (auto mlists = methods.beginLists(), end = methods.endLists();
         mlists != end;
         ++mlists)
    {
        method_t *m = search_method_list_inline(*mlists, sel);
        if (m) return m;
    }

    return nil;    
}

static method_t *
search_method_list_inline(const method_list_t *mlist, SEL sel)
{
    int methodListIsFixedUp = mlist->isFixedUp();
    int methodListHasExpectedSize = mlist->isExpectedSize();
    
    if (methodListIsFixedUp && methodListHasExpectedSize) {
        return findMethodInSortedMethodList(sel, mlist);
    } else {
        return findMethodInUnsortedMethodList(sel, mlist);
    }
}

不难看出,lookUpImpOrForward函数先是在通过getMethodNoSuper_nolock函数在当前类的方法列表中查找,若查找不到则先是在父类的方法缓存查找,再是在父类的方法列表中查找,直至找到或当前查找类为nil。注意到getMethodNoSuper_nolock函数在遍历方法列表时,调用了search_method_list_inline函数,而它对是否已排序(升序)的方法列表分别调用findMethodInSortedMethodListfindMethodInUnsortedMethodList。而实际上,findMethodInSortedMethodList就是个二分查找函数,而findMethodInUnsortedMethodList就是个简单粗暴的便利函数,本身没什么难点,这里不再赘述。

简单总结成流程图如下:

image.png

至此,我们也能轻松理解了为什么子类能在不重写方法的情况下能响应父类实现的方法。这就是沿继承链查找方法的全部过程,一旦在继承链中找到方法的实现,就结束查找并在objc_msgSend实现方法调用;若是遍历了整个继承链都找不到方法的实现,就会尝试动态方法决议。

3.2.2. 动态方法决议

在3.2.1.中讲解过,当在继承链上找不到方法的实现时,将尝试动态方法决议。何为动态方法决议?英文原词为“resolve method”,不过我个人认为这个英文词对于国人理解起来有点费劲,还是就中文译名作出解释:“动态方法决议”指的就是在一个统一的方法里判断是否新增一个方法(这就是“决议”一词的精髓所在)。那究竟是哪个方法呢?其实是两个:

+ (BOOL)resolveClassMethod:(SEL)sel;

+ (BOOL)resolveInstanceMethod:(SEL)sel;

这两个本身都是类方法,resolveClassMethod作为类方法在继承链搜索不到时调用的决议方法;resolveInstanceMethod作为实例方法在继承链搜索不到时调用的决议方法。

接下来我们直接给出动态方法决议的实现:

static IMP
resolveMethod_locked(id inst, SEL sel, Class cls, int behavior) {
    if (!cls->isMetaClass()) {
        resolveInstanceMethod(inst, sel, cls);
    } 
    else {
        resolveClassMethod(inst, sel, cls);
        if (!lookUpImpOrNilTryCache(inst, sel, cls)) {
            resolveInstanceMethod(inst, sel, cls);
        }
    }

    return lookUpImpOrForwardTryCache(inst, sel, cls, behavior);
}

static void 
resolveInstanceMethod(id inst, SEL sel, Class cls) {
    SEL resolve_sel = @selector(resolveInstanceMethod:);
    if (!lookUpImpOrNilTryCache(cls, resolve_sel, cls->ISA(true))) {
        return;
    }

    BOOL (*msg)(Class, SEL, SEL) = (typeof(msg))objc_msgSend;
    bool resolved = msg(cls, resolve_sel, sel);

    IMP imp = lookUpImpOrNilTryCache(inst, sel, cls);
}

static void 
resolveClassMethod(id inst, SEL sel, Class cls) {
    SEL resolve_sel = @selector(resolveClassMethod:);
    if (!lookUpImpOrNilTryCache(inst, resolve_sel, cls)) {
        return;
    }

    Class nonmeta = getMaybeUnrealizedNonMetaClass(cls, inst);
    BOOL (*msg)(Class, SEL, SEL) = (typeof(msg))objc_msgSend;
    bool resolved = msg(nonmeta, resolve_sel, sel);
    
    IMP imp = lookUpImpOrNilTryCache(inst, sel, cls);
}

IMP lookUpImpOrNilTryCache(id inst, SEL sel, Class cls, int behavior) {
    return _lookUpImpTryCache(inst, sel, cls, behavior | LOOKUP_NIL);
}

IMP lookUpImpOrForwardTryCache(id inst, SEL sel, Class cls, int behavior) {
    return _lookUpImpTryCache(inst, sel, cls, behavior);
}

static IMP 
_lookUpImpTryCache(id inst, SEL sel, Class cls, int behavior) {
    IMP imp = cache_getImp(cls, sel);
    if (imp != NULL) goto done;
    if (imp == NULL) {
        return lookUpImpOrForward(inst, sel, cls, behavior);
    }

done:
    if ((behavior & LOOKUP_NIL) && imp == (IMP)_objc_msgForward_impcache) {
        return nil;
    }
    return imp;
}

通过代码不难看出,其实现逻辑非常简单,总结起来就是两点:

  1. 若为类对象(即继承链中找不到实例方法),则调用类方法resolveInstanceMethod
  2. 若为元类对象(即继承链中找不到类方法),则调用类方法resolveClassMethod

简单总结成流程图如下:

image.png

通过这个流程图,我们更能清楚地知道在进行动态方法决议的时候,调用了resolveInstanceMethodresolveClassMethod后,接着又进行了沿继承链查找方法的流程。所以,为什么要调用resolveMethod?它有什么作用?为什么又需要新一轮的沿继承链查找方法的流程?注意到,我们为什么把resolve method翻译成动态方法决议,这里的“动态”才是精髓所在!通常,我们可以去实现这个方法来为在继承链中找不到的方法而临时进行动态添加该方法的操作。举个例子:

void foo(id self, SEL _cmd) {
    if (object_isClass(self)) {
        NSLog(@"Class method, %@, was resolved!", NSStringFromSelector(_cmd));
    } else {
        NSLog(@"Instance method, %@, was resolved!", NSStringFromSelector(_cmd));
    }
}

@interface FooClass : NSObject

+ (void)foo;
- (void)foo;

@end

@implementation FooClass

+ (BOOL)resolveInstanceMethod:(SEL)sel {
    if (sel == @selector(foo)) {
        class_addMethod([self class], @selector(foo), foo, "v@:");
        return YES;
    }
    return [super resolveInstanceMethod:sel];
}

+ (BOOL)resolveClassMethod:(SEL)sel {
    if (sel == @selector(foo)) {
        class_addMethod(objc_getMetaClass(object_getClassName(self)), @selector(foo), foo, "v@:");
        return YES;
    }
    return [super resolveClassMethod:sel];
}

@end

// some other function
[FooClass foo];
Foo *obj = [FooClass new];
[obj foo];

执行37-39行代码,输出如下:

Class method, foo, was resolved!
Instance method, foo, was resolved!

至此,我们成功将方法的调用与方法的添加都放在了运行时的同一时刻。

3.3. 消息转发

如果在继承链中查找不到方法,并且在动态方法决议后仍无法在继承链中查找到方法,则消息发送的全部过程结束,接下来将开始消息转发。

在3.2.1.中,我们在lookUpImpOrForward的源码中不难看到,当在继承链中查找不当方法,会返回一个特殊的函数指针_objc_msgForward_impcache。我们知道,在汇编实现的objc_msgSend中,会直接调用lookUpImpOrForward返回的函数指针,也就是说,消息转发实际上是_objc_msgForward_impcache这个特殊的函数指针的函数实现。不过_objc_msgForward_impcache也是汇编语言实现的,这里也简单将其使用C++进行改写,如下:

id _objc_msgForward_impcache(id self, SEL _cmd, ...) {
    return _objc_msgForward(self, _cmd, ...);
}

id _objc_msgForward(id self, SEL _cmd, ...) {
    return _objc_forward_handler(self, _cmd, ...);
}

实际上,就是调用了_objc_forward_handler函数,而事实上,Apple从这里开始就不进行代码开源了。不过在源码中,Apple给了一个默认实现,如下:

void objc_defaultForwardHandler(id self, SEL sel) {
    _objc_fatal("%c[%s %s]: unrecognized selector sent to instance %p "
                "(no message forward handler is installed)", 
                class_isMetaClass(object_getClass(self)) ? '+' : '-', 
                object_getClassName(self), sel_getName(sel), self);
}

这里,我们能知道,每次我们调用没有实现的方法时,编译器报错【xxxxx: unrecognized selector sent to instance xxxxx】是进行了类似objc_defaultForwardHandler的逻辑了。

虽然,我们从源码中无法窥探Apple对消息转发的完整实现,但是查阅相关文档,我们仍能总结出消息转发的两大流程:

  1. 转发消息
  2. 转发调用

先说转发消息,转发消息实际上就是调用forwardingTargetForSelector方法,返回一个可以处理此消息的对象。举个例子:

@interface FooClassA : NSObject

+ (void)foo;
- (void)foo;

@end

@implementation FooClassA

+ (void)foo {
    NSLog(@"%@ invoke class method %@", self, NSStringFromSelector(_cmd)); 
}

- (void)foo {
    NSLog(@"%@ invoke instance method %@", self, NSStringFromSelector(_cmd)); 
}

@end

@interface FooClassB : NSObject

+ (void)foo;
- (void)foo;

@end

@implementation FooClassB

- (id)forwardingTargetForSelector:(SEL)aSelector {
    if (aSelector == @selector(foo)) {
        id fooA = [FooClassA new];
        return fooA;
    }
    return [super forwardingTargetForSelector:aSelector];
}

+ (id)forwardingTargetForSelector:(SEL)aSelector {
    if (aSelector == @selector(foo)) {
        return [FooClassA class];
    }
    return [super forwardingTargetForSelector:aSelector];
}

@end

// some other function
[FooClassB foo];
FooClassB *fooB = [FooClassB new];
[fooB foo];

执行第47-49行的代码,输出如下:

FooClassA invoke class method foo
<FooClassA: xxxxx> invoke instance method foo

所以,分别实现forwardingTargetForSelector的类方法和实例方法可以根据不同的方法转发给不同的对象。

需要注意的是,在NSObject的实现中,forwardingTargetForSelector返回的是nil

如果forwardingTargetForSelector中返回了nil,则判定为转发消息失败,将开始转发调用的流程。转发调用与转发方法不同的是,转发调用需要先后调用两个方法:methodSignatureForSelectorforwardInvocationmethodSignatureForSelector返回方法的签名,实际上可以理解为返回方法的同样的,我们举个例子:

@interface FooClassA : NSObject

+ (void)foo;
- (void)foo;

@end

@implementation FooClassA

+ (void)foo {
    NSLog(@"%@ invoke class method %@", self, NSStringFromSelector(_cmd)); 
}

- (void)foo {
    NSLog(@"%@ invoke instance method %@", self, NSStringFromSelector(_cmd)); 
}

@end

@interface FooClassB : NSObject

+ (void)foo;
- (void)foo;

@end

@implementation FooClassB

- (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector {
    if (aSelector == @selector(foo)) {
        return [NSMethodSignature signatureWithObjCTypes:"v@:"];
        // return [FooClassA instanceMethodSignatureForSelector:aSelector];
    }
    return [super methodSignatureForSelector:aSelector];
}

+ (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector {
    if (aSelector == @selector(foo)) {
        return [NSMethodSignature signatureWithObjCTypes:"v@:"];
        // return [FooClassA methodSignatureForSelector:aSelector];
    }
    return [super methodSignatureForSelector:aSelector];
}

- (void)forwardInvocation:(NSInvocation *)anInvocation {
    if ([anInvocation selector] == @selector(foo)) {
        FooClassA *fooA = [FooClassA new];
        [anInvocation invokeWithTarget:fooA];
        return;
    }
    return [super forwardInvocation:anInvocation];
}

+ (void)forwardInvocation:(NSInvocation *)anInvocation {
    if ([anInvocation selector] == @selector(foo)) {
        [anInvocation invokeWithTarget:[FooClassA class]];
        return;
    }
    return [super forwardInvocation:anInvocation];
}

@end

// some other function
[FooClassB foo];
FooClassB *fooB = [FooClassB new];
[fooB foo];

执行第63-65行的代码,输出如下:

FooClassA invoke class method foo
<FooClassA: xxxxx> invoke instance method foo

实际上不难看出,methodSignatureForSelector就是将需要转发的消息进行一次方法签名,即将其返回类型和入参类型包装成NSMethodSignature类型。然后,runtime系统内部将其与消息名(SEL值)和入参一同包装成NSInvocation。接着就调用forwardInvocation实现最后的转发调用流程。

需要注意的是,若在methodSignatureForSelector中返回的方法签名不符合Objective-C的方法签名的基本要求(即返回类型为基本类型或结构体,并且第一个入参为id类型,第二个入参为SEL类型),则在包装NSInvocation时就会报错。

并且,在NSObject的实现中,methodSignatureForSelector会在继承链中查找到对应方法的方法类型,并将其包装成NSMethodSignature类型。若找不到将会报错【xxxxx: unrecognized selector sent to instance xxxxx】。

同样的,在NSObject的实现中,forwardInvocation会直接报错【xxxxx: unrecognized selector sent to instance xxxxx】。

至此,消息转发流程结束!

3.4. 小结

在3.2.和3.3.中,我们讲解了消息发送和消息转发的完整流程。那我们再总结一下,从我们使用消息发送的语法糖([receiver message])或直接使用runtime API(objc_msgSend(receiver, @selector(message))),到最后调用对应的方法,对应的简化版流程图如下:

image.png

3.4.1. 小试牛刀

先通过一个简单的题目来检验一下我们在3.中的学习成果:

@interface NSObject (Foo)

+ (void)foo;
- (void)foo;

@end

@implementation NSObject (Foo)

- (void)foo {
    NSLog(@"%@ invoke %@", self, NSStringFromSelector(_cmd));
}

@end

// some other function
[NSObject foo];

在这段代码中,执行第17行会发生什么?会crash吗?

我们简单分析一下,当执行[NSObject foo]时,首先会在NSObject类对象的isa获取NSObject元类对象。然后先在NSObject元类对象中查找foo方法,显然查找不到。接着会在NSObject元类对象的superClass中继续查找foo方法。在2.1.中,我们知道NSObject元类对象的superClassNSObject类对象。于是乎,我们在NSObject类对象中查找到foo方法的实现,然后调用foo方法。所以,并不会crash,并且能正常得到输出:

NSObject invoke foo

再来个简单的题目:

@interface FooClass : NSObject

+ (void)foo;

@end

@implementation FooClass

+ (void)foo {
    NSLog(@"[self class] : %@", [self class]);
    NSLog(@"[super class] : %@", [super class]);
}

@end

// some other function
[FooClass foo];

在这段代码中,执行第17行代码会输出什么?

可能有些同学会把[super class]理解为[self superclass],然后就认为第11行应该输出【[super class] : NSObject】。实际上这个是错的。我们还是简单分析一下,之前我们在3.1.中讲解过,super实际上是objc_msgSendSuper的语法糖。我们可以将[super class]简单翻译一下:

struct objc_super fooSuperClass;
fooSuperClass.receiver = self;
fooSuperClass.super_class = [FooClass superclass];
objc_msgSendSuper(&fooSuperClass, @selector(class));

注意到,我们这里的receiver仍为self,只是当我们沿着继承链查找方法时,是从super_class开始查找,也就是NSObject元类对象。于是,当我们找到class方法时,调用方式如下:

// objc_msgSendSuper(struct objc_super *super, SEL _cmd, ...)
IMP *imp = lookUpImpOrForward(super->receiver, @selector(class), super->super_clas, LOOKUP_INITIALIZE | LOOKUP_RESOLVER);
imp(super->receiver, @selector(class));

我们知道,NSObject元类对象的class方法的实现就是return self,所以调用class方法的imp时候,得到的就是第一个入参super->receiver,也就是FooClass类对象。故这个题目的输出是:

[self class] : FooClass
[super class] : FooClass

至此,相信大家应该对消息发送有了更加深刻的认识。

3.4.2. 面向切面编程(AOP)

可能对于很多同学来说,面向对象编程(OOP,Object-oriented programming)在学习和日常工作中,运用的比较多。面向切面编程,可能就不甚了解了。这里先给出维基百科的定义:

面向切面的程序设计(Aspect-oriented programming,AOP,又译作面向方面的程序设计、剖面导向程序设计),是计算机科学中的一种程序设计思想,旨在将横切关注点与业务主体进行进一步分离,以提高程序代码的模块化程度。通过在现有代码基础上增加额外的通知(Advice)机制,能够对被声明为“切点(Pointcut)”的代码块进行统一管理与装饰。

(From 维基百科

可能有点晦涩难懂,这用一句简单的话概括一下:这种在运行时,动态地将代码切入到类的指定方法、指定位置上的编程思想就是面向切面编程。

这时可能就有同学想到了,我们在2.5.3.中使用方法混合(Method Swizzling)就是一种AOP思想。不过这里,我们再介绍一种AOP的实现方式:

@protocol Tracker <NSObject>

- (void)invoke:(NSInvocation *)invocation withTarget:(id)target;

@end

@interface SuperDelegate : NSProxy

+ (instancetype)createWithTarget:(id)delegate;

- (void)addTrackSelector:(SEL)selector withTracker:(id<Tracker>)tracker;

@end

@interface SuperDelegate ()

@property (nonatomic, weak) id delegate;
@property (nonatomic, strong) NSMutableDictionary<NSValue *, NSMutableArray<id<Tracker>> *> *selectorDict;

@end

@implementation SuperDelegate

+ (instancetype)createWithTarget:(id)delegate {
    SuperDelegate *proxy = [SuperDelegate alloc];
    proxy.delegate = delegate;
    proxy.selectorDict = [NSMutableDictionary dictionary];
    return proxy;
}

- (void)addTrackSelector:(SEL)selector withTracker:(id<Tracker>)tracker {
    NSValue *selectorValue = [NSValue valueWithPointer:selector];
    if (!self.selectorDict[selectorValue]) {
        self.selectorDict[selectorValue] = [NSMutableArray array];
    }
    [self.selectorDict[selectorValue] addObject:tracker];
}

- (NSMethodSignature *)methodSignatureForSelector:(SEL)sel {
    return [self.delegate methodSignatureForSelector:sel];
}

- (void)forwardInvocation:(NSInvocation *)invocation {
    SEL selector = invocation.selector;
    NSValue *selectorValue = [NSValue valueWithPointer:selector];
    NSArray<id<Tracker>> *trackers = self.selectorDict[selectorValue];
    for (id<Tracker> tracker in trackers) {
        [tracker invoke:invocation withTarget:self.delegate];
    }
    
    [invocation invokeWithTarget:self.delegate];
}

@end

这里,我们创建了一个SuperDelegate的类,故名思义,我们把它作为一个“超级代理”使用。注意到,SuperDelegate继承自NSProxy,而不是我们常见的NSObject。其实NSProxy可以理解为一个抽象类,他本身不具备实例化的能力,即并没有init方法,并且它在消息发送上也与NSObject有所不同,当它在继承链上查找不到方法,就直接进行转发调用(forward invocation),即没有动态方法决议(resolve method)和转发消息(forward selector)的过程(NSProxy的详细文档见Apple官方文档:developer.apple.com/documentati… ,这里不再赘述)。除此之外,继承自NSProxy的子类必须实现methodSignatureForSelectorforwardInvocation。我们这里的SuperDelegate就是充分使用了这个特性,提供了addTrackSelector:withTracker:方法,即将要追踪的消息与追踪器进行绑定,当SuperDelegate接收到被追踪的消息时,会自动调用追踪器的invoke:withTarget:。故只需要实现Tracker协议的类,即可对其添加埋点等业务需求。这里提供一个简单的应用场景:

@interface CollectionViewTracker : NSObject <Tracker>

@end

@implementation CollectionViewTracker

- (void)invoke:(NSInvocation *)invocation withTarget:(id)target {
    if (invocation.selector == @selector(collectionView:didSelectItemAtIndexPath:)) {
        /**
        tracker operation
        */
    }
}

@end

// ViewController setup UICollectionView
id<UICollectionViewDelegate, UICollectionViewDataSource> superDelegate = [SuperDelegate createWithTarget:self];
CollectionViewTracker *tracker = [CollectionViewTracker new];
[superDelegate addTrackSelector:@selector(collectionView:didSelectItemAtIndexPath:) withTracker:tracker];
collectionView.delegate = superDelegate;
collectionView.dataSource = superDelegate;

在这个场景中,我们实现了一个CollectionViewTracker类,专门负责UICollectionView的点击埋点处理。当我们将其添加到我们的SuperDelegate中,即实现了UICollectionView的点击事件添加埋点功能。


4. 总结

通过本次学习,相信我们对Objective-C有了更加充分的认识,也能理解它作为一门动态语言的在iOS客户端研发中的巨大优势。不过,成也萧何,败也萧河,正是因为它过于动态,将大部分的方法调用的延迟到运行时校验,导致很多时候debug的难度也增大不少。而且,由于它在方法调用上,需要经过漫长的消息发送以及消息转发链路,所以往往性能上比不上C++、新兴语言Swift等静态语言。最后,最重要的一句话,也是把Apple开发者文档上的话照搬翻译一下:如果不是对Objective-C runtime API充分了解,尽量不要使用它!!!


参考链接