Objective-C 2.0 中类与对象的定义

Objective-C 的运行时系统(Runtime)有两个版本:“Modern” 和 “Legacy”。我们目前使用的 Objective-C 2.0 采用的是现行(Modern)版本的 Runtime 系统,它只能运行于 iOS 和 macOS 10.5 之后的 64 位程序中,而 macOS 较老的 32 位程序仍采用 Objective-C 1.0 早期(Legacy)版本的 Runtime 系统。

在上一篇文章《Objective-C 1.0 中类与对象的定义》中,我们介绍了早期 Objective-C 1.0 中类与对象的定义,本文以最新的 Objective-C Runtime 源码 objc4-750.1.tar.gz 进行阅读和分析 Objective-C 2.0 中类与对象的定义。

需要说明的是,本文只是简单罗列出了 Objective-C 2.0 中类与对象的相关定义,仅作为参考,并没有做深入的分析,因为这方面的文章已经有很多了,详见文末的参考文档。

注:文中的相关示意图均来自霜神(halfrost)的这篇博文,在此不胜感激。

Class 和 id

objc-private.h 文件中:

  • Class 被定义为指向 struct objc_class 的指针;
  • id 被定义为指向 struct objc_object 的指针;
1
2
typedef struct objc_class *Class;
typedef struct objc_object *id;

其中,objc_object 结构体的定义如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
struct objc_object {
private:
isa_t isa;

public:

// ISA() assumes this is NOT a tagged pointer object
Class ISA();

// getIsa() allows this to be a tagged pointer object
Class getIsa();

// 其他方法声明...
}

objc_object 结构体用于初始化 Objective-C 中类的实例对象,它包含一个名为 isa 的成员变量,其类型为 union isa_t 共用体,isa_t 中有一个 Class 类型的指针 cls,用于指向实例对象所对应的类,关于 isa 下面会详细分析。

objc-runtime-new.h 文件中,objc_class 结构体的定义如下:

1
2
3
4
5
6
7
8
9
10
11
12
struct objc_class : objc_object {
// Class ISA;
Class superclass;
cache_t cache; // formerly cache pointer and vtable
class_data_bits_t bits; // class_rw_t * plus custom rr/alloc flags

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

// 其他方法声明...
}

我们可以发现,objc_class 继承于 objc_object,也就是说,在 Objective-C 中,类本身也是一个对象,其 “isa 指针” 指向对应的“元类”(Meta Class),superclass 指针指向父类,cache 为调用过的方法缓存,而 bits 保存着这个类的属性(成员变量)、实例方法链表、以及遵循的协议等信息,示意图如下:

cache_t

方法缓存的类型 cache_t 的定义如下:

1
2
3
4
5
6
7
struct cache_t {
struct bucket_t *_buckets;
mask_t _mask;
mask_t _occupied;

// 其他方法声明...
}

其中,bucket_t *_buckets 是一个散列表,用来存储 Method 方法列表,而 bucket_t 结构体定义如下,包含了一个 unsigned long 类型的 _key 和一个 IMP 类型的 _imp,存储了指针与 IMP 的键值对。IMP 是一个函数指针,指向了一个方法的具体实现。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
struct bucket_t {
private:
// IMP-first is better for arm64e ptrauth and no worse for arm64.
// SEL-first is better for armv7* and i386 and x86_64.
#if __arm64__
MethodCacheIMP _imp;
cache_key_t _key;
#else
cache_key_t _key;
MethodCacheIMP _imp;
#endif

// 其他声明...
}

cache_t 中另外两个变量 _mask_occupied,它们的类型为 mask_t,定义如下,其实是一个 unsigned int

1
2
3
4
5
#if __LP64__
typedef uint32_t mask_t; // x86_64 & arm64 asm are less efficient with 16-bits
#else
typedef uint16_t mask_t;
#endif

_mask_occupied 对应于 vtable

  • _mask:分配用来缓存 bucket 的总数。
  • _occupied:表明目前实际占用的缓存 bucket 的个数。

cache 的作用主要是对方法调用的性能进行优化。通俗地讲,每当实例对象接收到一个消息时,它不会直接在其 isa 指向的类(或类的 isa 指向的父类)的方法列表中遍历查找能够响应消息的方法实现,因为这样效率太低了,而是优先在 cache 中查找。Runtime 系统会把被调用过的方法存到该类对象的 cache 中(理论上讲一个方法如果被调用,那么它有可能今后还会被调用),下次查找的时候效率更高。– 摘自这里

class_data_bits_t

objc_class 结构体中最复杂的是 bits 成员变量,其类型为 class_data_bits_t,定义如下:

1
2
3
4
5
6
7
8
9
10
11
struct class_data_bits_t {
// Values are the FAST_ flags above.
uintptr_t bits;

public:
class_rw_t *data() {
return (class_rw_t *)(bits & FAST_DATA_MASK);
}

// 其他定义...
}

objc_class 的定义中我们可以发现,objc_classdata() 方法直接调用 class_data_bits_tdata() 方法,最终是返回 class_rw_t *

class_rw_t

Objective-C 的类的属性、方法、以及遵循的协议在 2.0 版本之后都放在 class_rw_t 结构体中,其定义如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
struct class_rw_t {
// Be warned that Symbolication knows the layout of this structure.
uint32_t flags;
uint32_t version;

const class_ro_t *ro;

method_array_t methods;
property_array_t properties;
protocol_array_t protocols;

Class firstSubclass;
Class nextSiblingClass;

char *demangledName;

// 其他定义...
}

class_rw_t 提供了运行时对类拓展的能力,而 class_ro_t 存储的大多是类在编译时就已经确定的信息。二者都存有类的方法、属性(成员变量)、协议等信息,不过存储它们的列表实现方式不同。

class_rw_t 中使用的 method_array_t, property_array_t, protocol_array_t 都继承自 list_array_tt<Element, List>, 它可以不断扩张,因为它可以存储 list 指针,内容有三种:(1)空;(2)一个 entsize_list_tt 指针;(3)entsize_list_tt 指针数组。

class_rw_t 的内容是可以在运行时被动态修改的,可以说运行时对类的拓展大都是存储在这里的。

class_ro_t

通过源码,我们可以知道,objc_class 结构体包含了 class_data_bits_tclass_data_bits_t 存储了 class_rw_t 的指针,而 class_rw_t 结构体又包含 class_ro_t 的指针。

class_rw_t 中,class_ro_t 是一个指向常量的指针,存储了编译器决定了的属性、方法和遵守协议。class_ro_t 的定义如下,

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
struct class_ro_t {
uint32_t flags;
uint32_t instanceStart;
uint32_t instanceSize;
#ifdef __LP64__
uint32_t reserved;
#endif

const uint8_t * ivarLayout;

const char * name;
method_list_t * baseMethodList;
protocol_list_t * baseProtocols;
const ivar_list_t * ivars;

const uint8_t * weakIvarLayout;
property_list_t *baseProperties;

method_list_t *baseMethods() const {
return baseMethodList;
}
};

其中,class_ro_t 里的 method_list_t, ivar_list_t, property_list_t 结构体都继承自 entsize_list_tt<Element, List, FlagMask>,而 protocol_list_t 与前三个不同,它存储的是 protocol_t * 指针列表,实现比较简单。

entsize_list_tt 实现了 non-fragile 特性的数据结构,定义如下:

1
2
3
4
5
6
7
8
template <typename Element, typename List, uint32_t FlagMask>
struct entsize_list_tt {
uint32_t entsizeAndFlags;
uint32_t count;
Element first;

// 其他方法声明
}

关于 entsize_list_tt 的分析和 Non Fragile ivars 特性,详见这篇文章

假如苹果在新版本的 SDK 中向 NSObject 类增加了一些内容,NSObject 的占据的内存区域会扩大,开发者以前编译出的二进制中的子类就会与新的 NSObject 内存有重叠部分。Non Fragile ivars 特性在编译期会给 instanceStartinstanceSize 赋值,确定好编译时每个类的所占内存区域起始偏移量和大小,这样只需将子类与基类的这两个变量作对比即可知道子类是否与基类有重叠,如果有,也可知道子类需要挪多少偏移量。

综上,objc_classclass_data_bits_tclass_rw_tclass_ro_t 的关系如下:

关于 Objective-C 2.0 中方法结构的深入解析,详见这篇文章

isa

在上一篇文章中,我们介绍了,对象是某一个类的实例,在 Objective-C 中,类也被设计成一个对象,它是其对应的元类(Meta Class)的实例,而元类其实也是一个对象,它的类型是根元类(Root Meta Class),根元类的类型是它自己,它们的继承关系和 isa 指向关系如下图所指示:

此外,类对象和元类对象是全局唯一的,而对象是可以在运行时创建无数个的。在 main 方法执行之前,从 dyld 到 Runtime 加载期间,类对象和元类对象在此时被创建。

在 Objective-C 1.0 中,isa 只是一个指向 Class 的指针,而在 2.0 中,objc_object 中的 isa 是一个 isa_t 类型的共用体(union),在 objc-private.h 文件中定义如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
#include "isa.h"

union isa_t {
isa_t() { }
isa_t(uintptr_t value) : bits(value) { }

Class cls;
uintptr_t bits;
#if defined(ISA_BITFIELD)
struct {
ISA_BITFIELD; // defined in isa.h
};
#endif
};

其中,ISA_BITFIELD 宏定义了 isa_t 在不同处理器架构(arm64x86_64)下的结构,每个字段的含义如下表:

变量名 含义
indexed 0 表示普通的 isa 指针,1 表示使用优化,存储引用计数
has_assoc 表示该对象是否包含 associated object,如果没有,则析构时会更快
has_cxx_dtor 表示该对象是否有 C++ 或 ARC 的析构函数,如果没有,则析构时更快
shiftcls 类的指针
magic 固定值为 0xd2,用于在调试时分辨对象是否未完成初始化。
weakly_referenced 表示该对象是否有过 weak 对象,如果没有,则析构时更快
deallocating 表示该对象是否正在析构
has_sidetable_rc 表示该对象的引用计数值是否过大无法存储在 isa 指针
extra_rc 存储引用计数值减一后的结果

示意图:

关于 isa 的更多分析和 Tagged Pointer 的概念,详见这篇文章这篇文章

SEL

objc.h 中,定义了 SEL 为指向 struct objc_selector 的指针:

1
2
/// An opaque type that represents a method selector.
typedef struct objc_selector *SEL;

在 objc4 源码中,我们找不到 objc_selector 结构体的具体定义,我们可以把 SEL 理解为就是个映射到方法名的 C 字符串。

此外,不同类中相同名字的方法所对应的方法选择器是相同的,即使方法名字相同而变量类型不同也会导致它们具有相同的方法选择器。

我们可以通过编译器命令 @selector() 或者 Runtime 系统提供的 sel_registerName 函数来获得一个 SEL 类型的方法选择器。

IMP

objc.h 中,IMP 的定义如下:

1
2
3
4
5
6
/// A pointer to the function of a method implementation. 
#if !OBJC_OLD_DISPATCH_PROTOTYPES
typedef void (*IMP)(void /* id, SEL, ... */ );
#else
typedef id _Nullable (*IMP)(id _Nonnull, SEL _Nonnull, ...);
#endif

根据宏判断,我们可以知道,在 2.0 中,IMP 为:

1
typedef void (*IMP)(void /* id, SEL, ... */ );

IMP 是一个函数指针,指向方法最终的函数实现,它的参数与 objc_msgSend 函数的参数类型相同,关于 Objective-C 的“方法调用”与“消息转发”,我们这里不再赘述。

Method

objc-private.h 中,Method 被定义为一个指向 method_t 结构体的指针:

1
2
#if __OBJC2__
typedef struct method_t *Method;

method_t 结构体在 objc-runtime-new.h 中定义:

1
2
3
4
5
6
7
struct method_t {
SEL name;
const char *types;
MethodListIMP imp;

// 其他定义...
};

其中 MethodListIMPobjc-ptrauth.h 中声明,可以把它理解为就是 IMP 函数指针:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
#if __has_feature(ptrauth_calls)

// Method lists use process-independent signature for compatibility.
// Method caches use process-dependent signature for extra protection.
// (fixme not yet __ptrauth(...) because of `stp` inline asm in objc-cache.mm)
using MethodListIMP = IMP __ptrauth_objc_method_list_imp;
using MethodCacheIMP = StorageSignedFunctionPointer<IMP, ptrauth_key_process_dependent_code>;

#else

using MethodListIMP = IMP;
using MethodCacheIMP = IMP;

#endif

所以,Method 其实存储了方法名,方法类型和方法实现:

  • 方法名类型为 SEL,相同名字的方法即使在不同类中定义,或者参数类型不同,但它们的方法选择器是相同的。
  • 方法类型 types 是个 char 指针,其实存储着方法的参数类型和返回值类型。
  • imp 指向了方法的函数实现,本质上是一个函数指针。

Ivar

objc-private.h 中,Ivar 被定义为一个指向 ivar_t 结构体的指针:

1
2
#if __OBJC2__
typedef struct ivar_t *Ivar;

ivar_t 结构体在 objc-runtime-new.h 中定义:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
struct ivar_t {
#if __x86_64__
// *offset was originally 64-bit on some x86_64 platforms.
// We read and write only 32 bits of it.
// Some metadata provides all 64 bits. This is harmless for unsigned
// little-endian values.
// Some code uses all 64 bits. class_addIvar() over-allocates the
// offset for their benefit.
#endif
int32_t *offset;
const char *name;
const char *type;
// alignment is sometimes -1; use alignment() instead
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;
}
};

Ivar 用于表示类中实例变量(成员变量)的类型,ivar_t 中定义了实例变量的名字和类型。

Property

我们知道,@property 用于声明类中的属性,在 Runtime 系统中,objc_property_t 是一个指向objc_property 结构体的指针,在 objc-private.h 中定义:

1
2
#if __OBJC2__
typedef struct property_t *objc_property_t;

property_t 结构体在 objc-runtime-new.h 中定义:

1
2
3
4
struct property_t {
const char *name;
const char *attributes;
};

entsize_list_tt

此外,在 objc-runtime-new.h 中也定义了 method_list_tivar_list_tproperty_list_t,它们都继承于 entsize_list_tt,如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
// Two bits of entsize are used for fixup markers.
struct method_list_t : entsize_list_tt<method_t, method_list_t, 0x3> {
bool isFixedUp() const;
void setFixedUp();

uint32_t indexOfMethod(const method_t *meth) const {
uint32_t i =
(uint32_t)(((uintptr_t)meth - (uintptr_t)this) / entsize());
assert(i < count);
return i;
}
};

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());
}
};

struct property_list_t : entsize_list_tt<property_t, property_list_t, 0> {
};

entsize_list_tt 的声明如下:

1
2
3
4
5
6
7
8
template <typename Element, typename List, uint32_t FlagMask>
struct entsize_list_tt {
uint32_t entsizeAndFlags;
uint32_t count;
Element first;

// 其他定义...
}

它们将在上述提到的 class_ro_t 的结构体中使用。

Category

objc-private.h 中,Category 被定义为一个指向 category_t 结构体的指针:

1
2
#if __OBJC2__
typedef struct category_t *Category;

category_t 结构体在 objc-runtime-new.h 中定义:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
struct category_t {
const char *name;
classref_t cls;
struct method_list_t *instanceMethods;
struct method_list_t *classMethods;
struct protocol_list_t *protocols;
struct property_list_t *instanceProperties;
// Fields below this point are not always present on disk.
struct property_list_t *_classProperties;

method_list_t *methodsForMeta(bool isMeta) {
if (isMeta) return classMethods;
else return instanceMethods;
}

property_list_t *propertiesForMeta(bool isMeta, struct header_info *hi);
};

category_t 存储了分类中可以拓展的实例方法、类方法、协议、实例属性和类属性。在 App 启动时,Runtime 加载完类后,会通过调用 attachCategories 函数进行向类中添加 Category 的工作。原理就是向 class_rw_t 中的 method_array_t, property_array_t, protocol_array_t 数组中分别添加 method_list_t, property_list_t, protocol_list_t 指针。

Protocol

objc-runtime-new.h 中定义了 protocol_t 结构体:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
struct protocol_t : objc_object {
const char *mangledName;
struct protocol_list_t *protocols;
method_list_t *instanceMethods;
method_list_t *classMethods;
method_list_t *optionalInstanceMethods;
method_list_t *optionalClassMethods;
property_list_t *instanceProperties;
uint32_t size; // sizeof(protocol_t)
uint32_t flags;
// Fields below this point are not always present on disk.
const char **_extendedMethodTypes;
const char *_demangledName;
property_list_t *_classProperties;

// 其它定义...
}

protocol_t 继承于 objc_object,其中定义了协议中声明的实例方法,类方法,可选实例方法,可选类方法,实例属性和类属性等。此外,由于 Swift 还支持 Protocol 多继承,所以需要 protocols 数组来做兼容。

protocol_list_t 定义如下:

1
2
3
4
5
6
7
struct protocol_list_t {
// count is 64-bit by accident.
uintptr_t count;
protocol_ref_t list[0]; // variable-size

// 其它定义...
}

总结

通过两篇文章的整理,我们可以用如下两张图(来源)来分别表示 Objective-C 1.0 和 2.0 版本中类和对象的定义,及相关数据结构的关系:


参考