文章目录
  1. 1. 定义
  2. 2. 存储结构
  3. 3. 寻址
    1. 3.1. 寻址方式
    2. 3.2. Non Fragile ivars
    3. 3.3. 禁止动态添加
  4. 4. 参考

当我们研究一个事物的时候我们会研究他的属性和行为,属性是事物的外在特征如身高、体重。行为是事物能做什么如打球、喝水。当我们定义一个类的时候也要考虑这两方面因素,上面说的属性就是成员变量,行为就会方法。当然这些内容已经被我们潜移默化。在这里通过阅读runtime源码来深入了解Objective-c中成员变量的实现方式。

定义

首先我们来看runtime中Ivar的定义

Ivar 在objc_object中的位置

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
// objc_class
struct objc_class : objc_object {
//...
class_data_bits_t bits; // class_rw_t * plus custom rr/alloc flags
class_rw_t *data() {
return bits.data();
}
//...
}

//class_rw_t
struct class_rw_t {
//...
const class_ro_t *ro;

method_array_t methods;
property_array_t properties;
protocol_array_t protocols;
//...
}

// class_ro_t
struct class_ro_t {
//...
const uint8_t * ivarLayout;

const char * name;
method_list_t * baseMethodList;
protocol_list_t * baseProtocols;
const ivar_list_t * ivars;
//...
};

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

可以用一个导图来直观观察

类和成员变量

Ivar的定义

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
#if __OBJC2__
typedef struct ivar_t *Ivar;
#else
typedef struct old_ivar *Ivar;
#endif

//...

struct ivar_t {
//...
int32_t *offset;
const char *name;
const char *type;
//...
};

存储结构

如果把类的实例看成一个 C 语言的结构体(struct),上面说的 isa 指针就是这个结构体的第一个成员变量,而类的其它成员变量依次排列在结构体中。排列顺序如下图所示,(图片来自《iOS 6 Programming Pushing the Limits》)

类的结构

也可以是用代码验证

(lldb) p *_konwledgeSub
(RTKnowlegdeSub) $0 = {
RTKnowledge = {
​ NSObject = {
​ isa = RTKnowlegdeSub
​ }
}
_varString = nil
_proInt = 0
_propertyString = nil
_strongObject = nil
_weakObject = nil
}

寻址

寻址方式

LLVM在编译时,首先生成一种中间语言(IR,intermediate representation);后续的一些优化、分析步骤都在IR上进行;最后再把IR转化成native可执行文件。由于IR比汇编可读性要好,我们利用IR来分析编译后的Objective-C程序是怎么执行的。创建测试文件IvarAddress.m

1
2
3
4
5
6
7
8
9
10
11
12
13
@interface MyClass : NSError {
@public
int myInt;
}
@end

@implementation MyClass
@end

int main() {
MyClass *obj = [[MyClass alloc] init];
obj->myInt = 42;
}

使用下面的命令转化为IR代码,得到IvarAddress.ll文件

clang -cc1 -S -emit-llvm -fblocks IvarAddress.m

下面是寻址相关的代码

1
2
3
4
5
6
7
8
9
@"OBJC_IVAR_$_MyClass.myInt" = global i64 40, section "__DATA, __objc_ivar", align 8

//..

%ivar = load i64, i64* @"OBJC_IVAR_$_MyClass.myInt", align 8
%9 = bitcast %0* %8 to i8*
%add.ptr = getelementptr inbounds i8, i8* %9, i64 %ivar
%10 = bitcast i8* %add.ptr to i32*
store i32 42, i32* %10, align 4

观察上面代码obj->myInt = 42可以转化为下面简单C代码

1
2
int32_t g_ivar_MyClass_myInt = 40;  // 全局变量
*(int32_t *)((uint8_t *)obj + g_ivar_MyClass_myInt) = 42;

第一条取g_ivar_MyClass_myInt的值,第二条寻址并赋值。根本不需要一长串的指针调用。LLVM为每个类的每个成员变量都分配了一个全局变量,用于存储该成员变量的偏移值。

这也就是为什么ivar_t.offset用int指针来存储偏移值,而不是直接放一个int的原因。在这个设计中,真正存放偏移值的地址是固定不变的,在编译时就确定了下来。

Non Fragile ivars

首先看苹果官方文档Objective-C Runtime Programming Guide关于non fragile的说明

The most notable new feature is that instance variables in the modern runtime are “non-fragile”:

  • In the legacy runtime, if you change the layout of instance variables in a class, you must recompile classes that inherit from it.
  • In the modern runtime, if you change the layout of instance variables in a class, you do not have to recompile classes that inherit from it.

也就是说在程序启动后,runtime加载类的时候,通过计算基类的大小,runtime动态调整了类成员变量布局。于是我们的程序无需编译,就能在新版本系统上运行。

变量地址 = 对象地址 + ivar.offset

ivar.offset = 基类地址(动态) +ivar在本类中的偏移量(编译时固定)

class_ro_t中有两个相关属性

1
2
3
4
5
6
struct class_ro_t {
uint32_t flags;
uint32_t instanceStart;
uint32_t instanceSize;
//...
}

当我们的程序启动后,runtime加载类定义的时候,发现基类的真实大小和类的instanceStart不相符,得知基类的大小发生了改变。于是runtime遍历类的所有成员变量定义,将offset指向的值改变,它的具体实现在具体的实现代码在runtime/objc-runtime-new.mmmoveIvars()函数中。如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
static void moveIvars(class_ro_t *ro, uint32_t superSize)
{
runtimeLock.assertWriting();

uint32_t diff;

assert(superSize > ro->instanceStart);
diff = superSize - ro->instanceStart;

if (ro->ivars) {
//移动每个变量的offset
//...
}
*(uint32_t *)&ro->instanceStart += diff;
*(uint32_t *)&ro->instanceSize += diff;
}

禁止动态添加

首先声明,一旦类定义完成就不能添加成员变了。关于runtime提供的函数class_addIvar()苹果也有下面的说明:

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

意思是class_addIvar()只能在『构建一个类的过程中调用』

关于不能添加成员变量可以从类的定义个具体操作两个方面解释:

  • 在计算机中类是有某种特定的元数据所组成的内聚的包,元数据变了类就变了。在实际生活中类是具有特定特征的对象的总称。他们的特征是不能变的,特征变了类就变。比如『一个有尾巴的人还是人吗』『白马非马』
  • 从上面可以知道类中成员变量的偏移量是由基类大小和本类中成员变量共同决定的,如果一个类添加了成员变量size发生了变化,会到导致子类无法工作。

参考

Objective-C Runtime Programming Guide

Objective-C类成员变量深度剖析)