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类图如下:

深入浅出Objective-C runtime

从源码不难看出,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关于面向目标的完好完结原理图

深入浅出Objective-C runtime

从图中不难看出,无论是根类仍是子类,都有分为类目标和元类目标。那问题来了,为什么要差异出类目标和元类目标呢?咱们先看这样一个比如:

// 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除了存储了内存地址,还存储了额定的信息。

深入浅出Objective-C runtime
这儿先给出对应比特代表详细的意义:

  • 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感兴趣的同学能够参阅以下链接:

  • blog.devtang.com/2014/05/30/…
  • developer.apple.com/videos/play…

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类图如下:

深入浅出Objective-C runtime

不难看出,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结构体。

深入浅出Objective-C runtime

  • 进程初度运转

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

  • 首次调用

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

深入浅出Objective-C runtime

  • 进程运转时动态增加数据

在进程运转时动态增加办法、特点、协议等时,再创立class_rw_ext_t来存储运转时增加的数据。

深入浅出Objective-C runtime

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类图如下:

深入浅出Objective-C runtime

(这儿疏忽了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类图如下:

深入浅出Objective-C runtime

不难看出,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结构体真实缩小了一半的内存空间。那么问题来了,为什么能够这么做?

深入浅出Objective-C runtime

如图,很直观的能够知道,假设运用method_big结构体,因为dyld将二进制文件映射到内存的方位都是随机的,所以每次映射都需求批改method_big结构体的指针指向。

深入浅出Objective-C runtime

假设运用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类图如下:

深入浅出Objective-C runtime

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容量就扩容翻倍。对照代码制作等效流程图如下:

深入浅出Objective-C runtime

留意到在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)的原理图如下:

深入浅出Objective-C runtime

再举个比如:

@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的原理图如下:

深入浅出Objective-C runtime


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便是个简略粗犷的便利函数,本身没什么难点,这儿不再赘述。

简略总结成流程图如下:

深入浅出Objective-C runtime

至此,咱们也能轻松了解了为什么子类能在不重写办法的状况下能呼应父类完结的办法。这便是沿承继链查找办法的全部进程,一旦在承继链中找到办法的完结,就完毕查找并在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

简略总结成流程图如下:

深入浅出Objective-C runtime

通过这个流程图,咱们更能清楚地知道在进行动态办法抉择的时分,调用了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))),到最终调用对应的办法,对应的简化版流程图如下:

深入浅出Objective-C runtime

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充沛了解,尽量不要运用它!!!


参阅链接

  • Apple

    • opensource.apple.com/source/objc…
    • developer.apple.com/documentati…
    • developer.apple.com/library/arc…
  • halfrost blog

    • halfrost.com/objc\_life/
    • halfrost.com/objc\_runti…
    • halfrost.com/objc\_runti…
    • halfrost.com/how\_to\_us…
  • 社区

    • /post/684490…
    • /post/684490…
    • /post/684490…
    • /post/684490…
    • /post/697689…
    • /post/697583…