0%

iOS底层:类

class的本质

类是我们面向对象开发中使用很频繁的概念,我们看一下类的结构是什么。

首先我们定义一个类Person

1
2
3
4
5
6
7
8
9
10
11
12
NS_ASSUME_NONNULL_BEGIN

@interface Person : NSObject

@end

NS_ASSUME_NONNULL_END

// Animal.m
@implementation Person

@end

我们在main函数里初始化Person对象

1
2
3
4
5
6
7
int main(int argc, const char * argv[]) {
@autoreleasepool {
Person *obj = [[Person alloc] init];
NSLog(@"%p",obj);
}
return 0;
}

在terminal里执行clang命令

clang -rewrite-objc main.m -o main.cpp

会生成main.cpp文件,在main.cpp文件里搜索Person找到下面这段代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
#ifndef _REWRITER_typedef_Person
#define _REWRITER_typedef_Person
//1
typedef struct objc_object Person;
typedef struct {} _objc_exc_Person;
#endif
//2
struct Person_IMPL {
struct NSObject_IMPL NSObject_IVARS;
};


/* @end */

#pragma clang assume_nonnull end



// @implementation Person

// @end

int main(int argc, const char * argv[]) {
/* @autoreleasepool */ { __AtAutoreleasePool __autoreleasepool;
//3 objc_getClass("Person")
Person *obj = ((Person *(*)(id, SEL))(void *)objc_msgSend)((id)((Person *(*)(id, SEL))(void *)objc_msgSend)((id)objc_getClass("Person"), sel_registerName("alloc")), sel_registerName("init"));
NSLog((NSString *)&__NSConstantStringImpl__var_folders__s_l_p47fxn36j8d2_n75spyb7c0000gn_T_main_971c67_mi_0,obj);
}
return 0;
}

我们主要看代码注释1、2、3的位置,全局搜索typedef struct objc_object,发现有845个结果,结果数有点多,我们再全局搜索*Class,这次比较幸运,只有7个结果,我们可以快速定位到其中的一行

typedef struct objc_class *Class;

看到这里我们就知道了Class 类型的实际是一个叫objc_class的结构体。我们去objc源码里搜索struct objc_class

1
2
3
4
5
6
7
8
9
10
11
struct objc_class : objc_object {
objc_class(const objc_class&) = delete;
objc_class(objc_class&&) = delete;
void operator=(const objc_class&) = delete;
void operator=(objc_class&&) = delete;
// Class ISA;
Class superclass;
cache_t cache; // formerly cache pointer and vtable
class_data_bits_t bits;
///省略后面的set get函数
}

看源码可以知道objc_class继承自objc_object,说明Class其实也是一个对象。

类的结构

看上面这段源码,我们知道类的本质也是对象,我们日常开发中常见的成员变量、属性、方法、协议等都在类里面存在的,我们接下来探索一下这些内容在雷利是怎么存储的。

从上一小节最后的源码我们看到,类中的四个属性:

  • isa指针
  • superclass指针
  • cache
  • bits

isa指针是隐藏属性(继承自objc_object)。

isa指针

我们之前已经探索过,在对象初始化的时候,通过isa可以让对象和类关联,类中isa的作用也类似,关联类和元类。

superclass指针

这个看名字就很好理解,指向了父类,一般来说,类的根父类都是NSObject类。根元类的父类也是NSObject类。

cache

cache_t里的东西比较多,我们暂时先了解这个是缓存,里面会有方法的缓存。具体内容后面再探究。

bits属性

bits 的数据结构类型是 class_data_bits_t,同时也是一个结构体类型。而我们阅读 objc_class 源码的时候,会发现很多地方都有 bits 的身影,比如:

1
2
3
4
5
6
7
8
9
10
11
12
class_rw_t *data() { 
return bits.data();
}

bool hasCustomRR() {
return ! bits.hasDefaultRR();
}

bool canAllocFast() {
assert(!isFuture());
return bits.canAllocFast();
}

这里值得我们注意的是,objc_classdata() 方法其实是返回的 bitsdata() 方法,而通过这个 data() 方法,我们发现诸如类的字节对齐、ARC、元类等特性都有 data() 的出现,这间接说明 bits 属性其实是个大容器,有关于内存管理、C++ 析构等内容在其中有定义。

这里我们会遇到一个十分重要的知识点: class_rw_tdata() 方法的返回值就是 class_rw_t 类型的指针对象。我们在本文后面会重点介绍。

探索 bits 属性

bits属性的结构体是class_data_bits_t,看一下它的源码,class_data_bits_t声明了objc_class是它的友元类,可见关系不一般。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
struct class_data_bits_t {
friend objc_class;//友元类

// Values are the FAST_ flags above.
uintptr_t bits;
private:
bool getBit(uintptr_t bit) const
{
return bits & bit;
}

// Atomically set the bits in `set` and clear the bits in `clear`.
// set and clear must not overlap.
void setAndClearBits(uintptr_t set, uintptr_t clear)
{
ASSERT((set & clear) == 0);
uintptr_t newBits, oldBits = LoadExclusive(&bits);
do {
newBits = (oldBits | set) & ~clear;
} while (slowpath(!StoreReleaseExclusive(&bits, &oldBits, newBits)));
}

void setBits(uintptr_t set) {
__c11_atomic_fetch_or((_Atomic(uintptr_t) *)&bits, set, __ATOMIC_RELAXED);
}

void clearBits(uintptr_t clear) {
__c11_atomic_fetch_and((_Atomic(uintptr_t) *)&bits, ~clear, __ATOMIC_RELAXED);
}

public:

class_rw_t* data() const {
return (class_rw_t *)(bits & FAST_DATA_MASK);
}
void setData(class_rw_t *newData)
{
ASSERT(!data() || (newData->flags & (RW_REALIZING | RW_FUTURE)));
// Set during realization or construction only. No locking needed.
// Use a store-release fence because there may be concurrent
// readers of data and data's contents.
uintptr_t newBits = (bits & ~FAST_DATA_MASK) | (uintptr_t)newData;
atomic_thread_fence(memory_order_release);
bits = newBits;
}

// Get the class's ro data, even in the presence of concurrent realization.
// fixme this isn't really safe without a compiler barrier at least
// and probably a memory barrier when realizeClass changes the data field
const class_ro_t *safe_ro() const {
class_rw_t *maybe_rw = data();
if (maybe_rw->flags & RW_REALIZED) {
// maybe_rw is rw
return maybe_rw->ro();
} else {
// maybe_rw is actually ro
return (class_ro_t *)maybe_rw;
}
}

#if SUPPORT_INDEXED_ISA
void setClassArrayIndex(unsigned Idx) {
// 0 is unused as then we can rely on zero-initialisation from calloc.
ASSERT(Idx > 0);
data()->index = Idx;
}
#else
void setClassArrayIndex(__unused unsigned Idx) {
}
#endif

unsigned classArrayIndex() {
#if SUPPORT_INDEXED_ISA
return data()->index;
#else
return 0;
#endif
}

bool isAnySwift() {
return isSwiftStable() || isSwiftLegacy();
}

bool isSwiftStable() {
return getBit(FAST_IS_SWIFT_STABLE);
}
void setIsSwiftStable() {
setAndClearBits(FAST_IS_SWIFT_STABLE, FAST_IS_SWIFT_LEGACY);
}

bool isSwiftLegacy() {
return getBit(FAST_IS_SWIFT_LEGACY);
}
void setIsSwiftLegacy() {
setAndClearBits(FAST_IS_SWIFT_LEGACY, FAST_IS_SWIFT_STABLE);
}

// fixme remove this once the Swift runtime uses the stable bits
bool isSwiftStable_ButAllowLegacyForNow() {
return isAnySwift();
}

_objc_swiftMetadataInitializer swiftMetadataInitializer() {
// This function is called on un-realized classes without
// holding any locks.
// Beware of races with other realizers.
return safe_ro()->swiftMetadataInitializer();
}
};

我们主要看下面两块代码。

1
2
3
4
5
6
7
8
9
10
11
12
13
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) {
// maybe_rw is rw
return maybe_rw->ro();
} else {
// maybe_rw is actually ro
return (class_ro_t *)maybe_rw;
}
}

先看class_rw_t

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
struct class_rw_t {
// Be warned that Symbolication knows the layout of this structure.
uint32_t flags;
uint16_t witness;
#if SUPPORT_INDEXED_ISA
uint16_t index;
#endif

explicit_atomic<uintptr_t> ro_or_rw_ext;

Class firstSubclass;
Class nextSiblingClass;
//省略代码 ...

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

这个结构体里有一些我们熟悉的字样methodspropertiesprotocols。这里的properties是不是就是存储属性的地方呢。让我们验证一下。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
#import <Foundation/Foundation.h>
#import <objc/runtime.h>

NS_ASSUME_NONNULL_BEGIN

@interface Person : NSObject
{
NSString *hobby;
}
@property (nonatomic, copy) NSString *nickName;
@end

NS_ASSUME_NONNULL_END

@implementation Person

@end

int main(int argc, const char * argv[]) {
@autoreleasepool {
// insert code here...
Person *p = [[Person alloc] init];
Class pClass = object_getClass(p);
NSLog(@"%s", p);
}
return 0;
}

我们定义一个Person类,在里面定义一个成员变量和一个属性。在main函数里打断点,我们用LLDB打印看一下pClass

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
(lldb) x/4gx pClass
0x1000081d0: 0x00000001000081a8 0x0000000100357140
0x1000081e0: 0x000000010072fdc0 0x0001802400000003
(lldb) p (class_data_bits_t *)0x1000081f0//0x1000081d0+0x20 bits地址偏移量
(class_data_bits_t *) $16 = 0x00000001000081f0
(lldb) p $16->data()
(class_rw_t *) $17 = 0x0000000100731e10
(lldb) p *$17
(class_rw_t) $18 = {
flags = 2148007936
witness = 1
ro_or_rw_ext = {
std::__1::atomic<unsigned long> = {
Value = 4295000208
}
}
firstSubclass = nil
nextSiblingClass = NSUUID
}

可以看最后$18里面并没有打印出属性列表。我们接着探索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
23
24
struct class_ro_t {
uint32_t flags;
uint32_t instanceStart;
uint32_t instanceSize;
#ifdef __LP64__
uint32_t reserved;
#endif

union {
const uint8_t * ivarLayout;
Class nonMetaclass;
};

explicit_atomic<const char *> name;
// With ptrauth, this is signed if it points to a small list, but
// may be unsigned if it points to a big list.
void *baseMethodList;
protocol_list_t * baseProtocols;
const ivar_list_t * ivars;

const uint8_t * weakIvarLayout;
property_list_t *baseProperties;
//隐藏代码
}

发现class_ro_t接口中包含了baseMethodListbaseProtocolsbaseProperties等属性。验证一下属性是不是存储在这里了。和上面方式类似,我们利用LLDB查看

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
 p (class_data_bits_t *)0x1000081f0
(class_data_bits_t *) $19 = 0x00000001000081f0
(lldb) p $19->safe_ro()
(const class_ro_t *) $20 = 0x0000000100008090
(lldb) p $20
(const class_ro_t) $21 = {
flags = 388
instanceStart = 8
instanceSize = 24
reserved = 0
= {
ivarLayout = 0x0000000100003f1d "\x02"
nonMetaclass = 0x0000000100003f1d
}
name = {
std::__1::atomic<const char *> = "Person" {
Value = 0x0000000100003f16 "Person"
}
}
baseMethodList = 0x00000001000080d8
baseProtocols = 0x0000000000000000
ivars = 0x0000000100008128
weakIvarLayout = 0x0000000000000000
baseProperties = 0x0000000100008170
_swiftMetadataInitializer_NEVER_USE = {}
}
}
(lldb) p $20.baseProperties
(property_list_t *const) $22 = 0x0000000100008170
Fix-it applied, fixed expression was:
$20->baseProperties
(lldb) p *$22
(property_list_t) $23 = {
entsize_list_tt<property_t, property_list_t, 0, PointerModifierNop> = (entsizeAndFlags = 16, count = 1)
}
(lldb) p $23.get(0)
(property_t) $28 = (name = "nickName", attributes = "T@\"NSString\",C,N,V_nickName")

可以看到了nickName属性,这里的count是1,说明只有这一个属性。我们接下来看一下ivars属性,继续使用LLDB

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
(lldb) p $20.ivars
(const ivar_list_t *const) $24 = 0x0000000100008128
Fix-it applied, fixed expression was:
$20->ivars
(lldb) p *$24
(const ivar_list_t) $25 = {
entsize_list_tt<ivar_t, ivar_list_t, 0, PointerModifierNop> = (entsizeAndFlags = 32, count = 2)
}
(lldb) p $25.get(1)
(ivar_t) $26 = {
offset = 0x00000001000081a0
name = 0x0000000100003f5b "_nickName"
type = 0x0000000100003f89 "@\"NSString\""
alignment_raw = 3
size = 8
}
(lldb) p $25.get(0)
(ivar_t) $27 = {
offset = 0x0000000100008198
name = 0x0000000100003f55 "hobby"
type = 0x0000000100003f89 "@\"NSString\""
alignment_raw = 3
size = 8
}

可以看到ivars属性里一共有两个值,_nickName和hobby。这一结果证实了编译器会帮助我们给属性 nickName 生成一个带下划线前缀的实例变量 _nickName

至此,我们可以得出以下结论:

class_ro_t 是在编译时就已经确定了的,存储的是类的成员变量、属性、方法和协议等内容。
class_rw_t 是可以在运行时来拓展类的一些属性、方法和协议等内容。

类的方法存储在哪

我们用相同的方法验证一下,先在Person类中增加一个sayHello 的实例方法和一个 sayHappy 的类方法。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
NS_ASSUME_NONNULL_BEGIN

@interface Person : NSObject
{
NSString *hobby;
}
@property (nonatomic, copy) NSString *nickName;
- (void)sayHello;
+ (void)sayHappy;
@end

NS_ASSUME_NONNULL_END

@implementation Person

- (void)sayHello
{
NSLog(@"%s", __func__);
}

+ (void)sayHappy
{
NSLog(@"%s", __func__);
}

@end

按照上面的思路,我们直接读取 class_ro_t 中的 baseMethods() 的内容:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
(lldb) p $3.baseMethods()
(method_list_t *) $7 = 0x00000001000080f0
(lldb) p *$7
(method_list_t) $8 = {
entsize_list_tt<method_t, method_list_t, 4294901763, method_t::pointer_modifier> = (entsizeAndFlags = 27, count = 4)
}
(lldb) p $8.get(0)
(method_t) $9 = {}
(lldb) p $8.get(1)
(method_t) $10 = {}
(lldb) p $8.get(2)
(method_t) $11 = {}
(lldb) p $8.get(3)
(method_t) $12 = {}

可以看到baseMethods有四个方法。

类的类方法存储在哪

我们用Runtime的API来实际测试一下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
// main.m
void testInstanceMethod_classToMetaclass(Class pClass){

const char *className = class_getName(pClass);
Class metaClass = objc_getMetaClass(className);

Method method1 = class_getInstanceMethod(pClass, @selector(sayHello));
Method method2 = class_getInstanceMethod(metaClass, @selector(sayHello));

Method method3 = class_getInstanceMethod(pClass, @selector(sayHappy));
Method method4 = class_getInstanceMethod(metaClass, @selector(sayHappy));

NSLog(@"%p-%p-%p-%p",method1,method2,method3,method4);
NSLog(@"%s",__func__);
}

int main(int argc, const char * argv[]) {
@autoreleasepool {
// insert code here...
Person *p = [[Person alloc] init];
Class pClass = object_getClass(p);

testInstanceMethod_classToMetaclass(pClass);
}
return 0;
}

运行后打印结果:

1
2
0x100008110-0x0-0x0-0x1000080a8
testInstanceMethod_classToMetaclass

首先 testInstanceMethod_classToMetaclass 方法测试的是分别从类和元类去获取实例方法、类方法的结果。由打印结果我们可以知道:

  • 对于类对象来说,sayHello 是实例方法,存储于类对象的内存中,不存在于元类对象中。而 sayHappy 是类方法,存储于元类对象的内存中,不存在于类对象中。
  • 对于元类对象来说,sayHello 是类对象的实例方法,跟元类没关系;sayHappy 是元类对象的实例方法,所以存在元类中。

类和元类的创建时机

先看结论:

类和元类是在编译期创建的,即在进行alloc操作之前,类和元类就已经被编译器床架你出来了。

我们通过LLDB打印类和元类的指针来验证一下,断点打在main的第一行,这时候Person对象还没有初始化。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
(lldb) p/x Person.class
(Class) $1 = 0x0000000100008220 Person
(lldb) x/4gx 0x0000000100008220//Person 类对象
0x100008220: 0x00000001000081f8 0x0000000100357140
0x100008230: 0x000000010034f360 0x0000802400000000
(lldb) p/x 0x00000001000081f8 & 0x00007ffffffffff8
(long) $2 = 0x00000001000081f8//Person 元类对象
(lldb) po $2
Person

(lldb) x/4gx 0x00000001000081f8
0x1000081f8: 0x00000001003570f0 0x00000001003570f0
0x100008208: 0x0000000100615130 0x0001e03500000003
(lldb) p/x 0x00000001003570f0 & 0x00007ffffffffff8
(long) $3 = 0x00000001003570f0//NSObject 根元类对象
(lldb) po $3
NSObject

(lldb) x/4gx 0x00000001003570f0
0x1003570f0: 0x00000001003570f0 0x0000000100357140
0x100357100: 0x0000000101104ac0 0x0004e03100000007
(lldb) p/x 0x00000001003570f0 & 0x00007ffffffffff8
(long) $4 = 0x00000001003570f0//NSObject 根元类对象
(lldb) po $4
NSObject

还有一种方式是通过MachoView应用打开编译的二进制mach-o文件,MachoView的使用就不介绍了。

总结

  • 类和元类创建于编译时,可以通过 LLDB 来打印类和元类的指针,或者 MachOView 查看二进制可执行文件
  • 万物皆对象:类的本质就是对象
  • 类在 class_ro_t 结构中存储了编译时确定的属性、成员变量、方法和协议等内容。
  • 实例方法存放在类中
  • 类方法存放在元类中