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中,有两大基类NSObject
和NSProxy
,而NSObject也作为唯一的基协议。其他所有Objective-C的类都继承自NSObject
或NSProxy
,并遵守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类图如下:
从源码不难看出,NSObject
本质上是objc_class
结构体。而在objc_class
结构体中,除了继承得来的isa
变量,通过变量的命名,我们也可以轻易知道obj_class
还包含父类指针、缓存和类数据。我们日常使用实例对象的id
类型和类对象的Class
类型,实质是一个指向objc_object
、objc_class
结构体的指针类型。举个简单的例子:
id instanceObj = [NSObject new];
这个简单的Objective-C创建实例对象的代码中的instanceObj
实际上是一个指向objc_object
结构体的指针,通过被指向的内存空间中的objc_object
结构体中的成员变量isa
能获得NSObject
类对象的objc_class
结构体的内存地址。注意到我这里提到了NSObject
类对象,其实还有一个NSObject
元类对象。这里先给出Objective-C对于面向对象的完整实现原理图:
从图中不难看出,无论是根类还是子类,都有分为类对象和元类对象。那问题来了,为什么要区分出类对象和元类对象呢?我们先看这样一个例子:
// 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
除了存储了内存地址,还存储了额外的信息。
这里先给出对应比特代表具体的含义:
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
初始化的操作是分别将nonpointer
、extra_rc
置为1,magic
置为0x3B(0b111011),设置has_cxx_dtor
和shiftcls
。注意到第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_t
。class_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类图如下:
不难看出,class_data_bits_t
结构体能获取class_rw_t
和class_ro_t
结构体指针。而class_ro_t
结构体指针实际上是class_rw_ext_t
结构体的成员变量,而class_rw_ext_t
结构体指针实际上是class_rw_t
结构体的成员变量。这里面引入了三个重要的结构体:class_rw_t
、class_rw_ext_t
和class_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_t
、class_rw_ext_t
和class_ro_t
这三个结构体的存储方式是这样设计的:
- 编译成二进制产物存在硬盘(Flash、SSD、HDD)
在编译的过程中,自定义类的方法、协议、实例变量、属性都是确定的,所以仅需要class_ro_t
结构体。
- 进程初次运行
进程初期运行时,会调用Objective-C runtime的初始化入口_objc_init
,将objec_class
和class_ro_t
加载到内存中。
- 首次调用
类被首次调用时,将在内存中创建class_rw_t
。
- 进程运行时动态添加数据
在进程运行时动态添加方法、属性、协议等时,再创建class_rw_ext_t
来存储运行时添加的数据。
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
这里例子中,obj
与objx
的赋值实际上都依赖于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类图如下:
(这里忽略了entsize_list_tt
结构体的实现,简单说来,它一共有两个成员变量:entsizeAndFlags
记录数组单个item的大小,可能附带flag信息;count
记录数组的item
数量。从前64位起,后面才真正存储了数组数据。)
ivar实际上是instance variable的缩写,顾名思义,ivar_list_t
是实例变量数组,ivar_t
是实例变量。不难看出,ivar_t
依次存储了实例变量的内存偏移量、名称、类型、内存对齐方式和大小。于是,如果想要在运行时实现动态访问实例变量,仅需要通过名称等信息查找到对应的ivar_t
,从而找到其内存偏移量,再加上实例对象内存地址即可。
如果我们有一定的Objective-C的开发经验,一定知道两件事情:
- 无法给已经编译好的类添加extension
- 无法在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
的子类,并获取了对应的类对象FooClass
(objc_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
结构体中,分别对应baseMethods
和methods
。baseMethods
为method_list_t
类型,其实就是将方法类型method_t
结构体组织成数组进行存储,原理类似2.3.2.讲解的ivar_list_t
数组存储实现。而methods
为method_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类图如下:
不难看出,method_t
结构体实际上存在两种不同的实现,一种实现为method_t::big
结构体,另一种实现为method_t::small
结构体,为了方便讨论,我们称其为method_big
结构体与method_small
结构体。那问题来了,二者在结构上看起来没啥区别,一样存储了name
、types
和imp
,为什么要区分成两种不同的版本呢?
其实,在上面提到的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
结构体真正缩小了一半的内存空间。那么问题来了,为什么可以这么做?
如图,很直观的可以知道,假如使用method_big
结构体,因为dyld将二进制文件映射到内存的位置都是随机的,所以每次映射都需要修正method_big
结构体的指针指向。
假如使用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
结构体在获取SEL
(name
)的时候,进行了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类图如下:
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
是如何编码并存储SEL
和IMP
的:
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容量就扩容翻倍。对照代码绘制等效流程图如下:
注意到在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
是关联策略。policy
是objc_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
,即实现实例方法encodeWithCoder
和initWithCoder
。一般情况下,我们会如此实现:
@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
如此实现,带来两个弊端,一个是当属性较多时,代码实现比较繁琐;另一个是后期可拓展性不强,假如后期迭代的过程中增删属性,就需要对应着修改实例方法encodeWithCoder
和initWithCoder
。那有没有一劳永逸的方法?当然有,考虑到一般我们序列化或反序列化的过程中,仅需要存储实例变量,我们在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
配合NSCoder
的encodeValueOfObjCType
使用,这里不展开赘述。
对于需要存储属性,可以通过class_copyPropertyList
获取属性列表,通过属性不同特性(attribute)实现需要的操作。
2.5.3. 实现优雅添加业务埋点
假如我们有一个业务需求,需要在所有的ViewController
的viewDidLoad
生命周期中添加业务埋点逻辑或者一些重复性的工作,一般情况下,我们有两种实现方案。一种是将所有的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
添加SEL
为viewDidLoad
、IMP
为_I_UIViewController_Tracker_tracker_viewDidLoad
的方法。但是class_addMethod
只能给当前类(不会判断父类)原本没有的SEL
添加方法,因UIViewController
一定有SEL
为viewDidLoad
的方法,故其实在这个例子里class_addMethod
会返回NO
(但对于一些继承而来的子类仍有判断的必要)。如果class_addMethod
里成功添加了方法,那么使用class_replaceMethod
将原本SEL
为tracker_viewDidLoad
的方法替换IMP
为_I_UIViewController_viewDidLoad
即可。而这里的例子将会执行method_exchangeImplementations
的逻辑,即将SEL
为viewDidLoad
和SEL
为tracker_viewDidLoad
的方法交换IMP
。这样就实现了实例对象调用viewDidLoad
时实际上调用了_I_UIViewController_Tracker_tracker_viewDidLoad
函数,而在tracker_viewDidLoad
方法实现的最后调用了tracker_viewDidLoad
将实际上调用_I_UIViewController_viewDidLoad
函数。这样,在开发者的视角就相当于将两个不同的方法混合起来了!
UIViewController (Tracker)
的原理图如下:
再举个例子:
@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
的原理图如下:
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_fpret
、objc_msgSend_fp2ret
和objc_msgSend_stret
。其中,objc_msgSend_fpret
和objc_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
类型的imp
和const 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
函数,而且,入参self
为FooClass
的实例对象,而入参_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_msgSendSuper
与objc_msgSend
类似,入参至少有两个,并且第二个参数为SEL
值。而objc_super
有两个成员函数,receiver
和super_class
。receiver
被赋值为方法的第一个入参self
,而super_class
则在编译期间就固定为FooClass
的父类。(后面会对此展开详细讲解,先按下不表)
同样的,与objc_msgSend
类似,objc_msgSendSuper_stret
用于方法返回值为结构体类型的情况。
特别注意的是,实际上编译过程中super
会翻译为调用objc_msgSendSuper2
,与objc_msgSendSuper
不同的是,objc_super
结构体的成员变量super_class
赋值为己类而非父类。在objc_msgSendSuper2
的实现中通过receiver
的isa
获取父类,故性能上也优于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
函数就干了四件事:
- Nil test
判断入参self
是否为空,若为空返回nil
。
- Get class
将self
的isa
与ISA_MASK
做或运算,即得到当前的类。
- Get imp in cache
在isa
指向的方法缓存中尝试获取imp
,若成功获取,直接进行方法调用。(可以参考2.4.2.2.中查找方法缓存的实现)
- 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
函数,而它对是否已排序(升序)的方法列表分别调用findMethodInSortedMethodList
和findMethodInUnsortedMethodList
。而实际上,findMethodInSortedMethodList
就是个二分查找函数,而findMethodInUnsortedMethodList
就是个简单粗暴的便利函数,本身没什么难点,这里不再赘述。
简单总结成流程图如下:
至此,我们也能轻松理解了为什么子类能在不重写方法的情况下能响应父类实现的方法。这就是沿继承链查找方法的全部过程,一旦在继承链中找到方法的实现,就结束查找并在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;
}
通过代码不难看出,其实现逻辑非常简单,总结起来就是两点:
- 若为类对象(即继承链中找不到实例方法),则调用类方法
resolveInstanceMethod
- 若为元类对象(即继承链中找不到类方法),则调用类方法
resolveClassMethod
简单总结成流程图如下:
通过这个流程图,我们更能清楚地知道在进行动态方法决议的时候,调用了resolveInstanceMethod
或resolveClassMethod
后,接着又进行了沿继承链查找方法的流程。所以,为什么要调用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对消息转发的完整实现,但是查阅相关文档,我们仍能总结出消息转发的两大流程:
- 转发消息
- 转发调用
先说转发消息,转发消息实际上就是调用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
,则判定为转发消息失败,将开始转发调用的流程。转发调用与转发方法不同的是,转发调用需要先后调用两个方法:methodSignatureForSelector
和forwardInvocation
。methodSignatureForSelector
返回方法的签名,实际上可以理解为返回方法的同样的,我们举个例子:
@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))
),到最后调用对应的方法,对应的简化版流程图如下:
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
元类对象的superClass
是NSObject
类对象。于是乎,我们在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
的子类必须实现methodSignatureForSelector
和forwardInvocation
。我们这里的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
-
halfrost blog
-
掘金社区
今天的文章深入浅出Objective-C runtime分享到此就结束了,感谢您的阅读。
版权声明:本文内容由互联网用户自发贡献,该文观点仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请发送邮件至 举报,一经查实,本站将立刻删除。
如需转载请保留出处:https://bianchenghao.cn/14043.html