前言
在oc对象alloc的过程中,使用calloc函数开辟内存空间后,需要把实例和类关联起来,这一步到底是如何实现的呢?
预备知识
联合体(union)
语法定义
联合体(union),与结构体(struct)有极为相似的语法结构,跟struct一样可以有多种数据类型和成员:
union {
int i;
double x;
char str[16];
} myData;
unoin的成员共享同一内存空间
不同于struct的成员在结构体中具有独立的内存位置,union的成员共享同一内存位置,也就是说,union中的所有成员都是从相同内存地址开始的。unoin的本质就是同一内存地址的数据,可以使用不同的方式来解读。
union {
int i;
double x;
char str[16];
} myUnion;
struct {
int i;
double x;
char str[16];
} myStruct;
以上union和struct,成员的存储布局,可以用下面两图说明:
验证代码
union {
int i;
double x;
char str[16];
} myUnion;
myUnion.i = 0x313233;
NSLog(@"i=%d, str=%@", myUnion.i, [NSString stringWithUTF8String: myUnion.str]);
控制台输出:
i=3224115, str=321
查看内存
(lldb) p 0x313233
(int) $1 = 3224115
(lldb) p &myUnion
((anonymous union) *) $2 = 0x00007ffeefbff448
(lldb) x $2
0x7ffeefbff448: 33 32 31 00 00 00 00 00 78 f4 bf ef fe 7f 00 00 321.....x.......
0x7ffeefbff458: 7b 00 c4 ce 76 e1 d1 a4 78 f4 bf ef fe 7f 00 00 {...v...x.......
- 0x31323316进制转10进制,即3224115
- str[0]-str[2], 即从首地址开始的三个字节,0x33,0x32,0x31, 正好是字符'3'、'2'、'1'的ascii码
union的size
unoin的size要符合以下条件
- 所有成员都是从offset为0的地方开始
- 大小要足够容纳最宽的成员
- 大小要是所有基础类型成员大小的整数倍
验证代码
union {
int i;
double x;
char str[11];
} myUnion;
NSLog(@"size=%ld", sizeof(myUnion));
控制台输出:
size=16
分析: 根据原则1,由于char数组,至少需要11位;根据原则2,需要是double的整数倍,即8的整数倍,最终是16
unoin与struct的异同
- 结构体(struct)中所有变量是“共存”的——优点是“有容乃大”,全面;缺点是struct内存空间的分配是粗放的,不管用不用,全分配。
- 联合体(union)中是各变量是“互斥”的——缺点就是不够“包容”,即任何两个成员不会同时有效;但优点是内存使用更为精细灵活,也节省了内存空间。 当多个数据需要共享内存或者多个数据每次只取其一时,可以考虑使用union
位域(Bit field)
在一个结构体中以位为bit单位来指定其成员所占内存长度,即为位域。这样的设计有以下优势:
- 节省存储空间,尤其是成千上万数据的时候;
- 可以访问数据的指定部分内容
验证代码
struct {
unsigned short a : 1;
unsigned int b : 2;
int c;
}myStruct;
myStruct.a = 0b11;
myStruct.b = 0b110;
myStruct.c = 0xffffffff;
NSLog(@"a=%d, b=%d, size=%d", myStruct.a, myStruct.b, sizeof(myStruct));
控制台输出:
a=1, b=2, size=8
查看内存:
(lldb) p &myStruct
((anonymous struct) *) $0 = 0x00007ffeefbff430
(lldb) x/t $0
0x7ffeefbff430: 0b00000000000000000000000000000101
(lldb) x/gx $0
0x7ffeefbff430: 0xffffffff00000005
(lldb)
实例和对象的关联
initInstanceIsa()
在实例对象alloc过程中,关键步骤在于函数_class_createInstanceFromZone:
static ALWAYS_INLINE id
_class_createInstanceFromZone(Class cls, size_t extraBytes, void *zone,
int construct_flags = OBJECT_CONSTRUCT_NONE,
bool cxxConstruct = true,
size_t *outAllocatedSize = nil)
{
ASSERT(cls->isRealized());
// Read class's info bits all at once for performance
bool hasCxxCtor = cxxConstruct && cls->hasCxxCtor();
bool hasCxxDtor = cls->hasCxxDtor();
bool fast = cls->canAllocNonpointer();
size_t size;
// 1. 计算需要的空间大小
size = cls->instanceSize(extraBytes);
if (outAllocatedSize) *outAllocatedSize = size;
id obj;
// 2. 开辟空间,返回地址,赋值为obj
if (zone) {
obj = (id)malloc_zone_calloc((malloc_zone_t *)zone, 1, size);
} else {
obj = (id)calloc(1, size);
}
if (slowpath(!obj)) {
if (construct_flags & OBJECT_CONSTRUCT_CALL_BADALLOC) {
return _objc_callBadAllocHandler(cls);
}
return nil;
}
// 3. 关联class
if (!zone && fast) {
obj->initInstanceIsa(cls, hasCxxDtor);
} else {
// Use raw pointer isa on the assumption that they might be
// doing something weird with the zone or RR.
obj->initIsa(cls);
}
if (fastpath(!hasCxxCtor)) {
return obj;
}
construct_flags |= OBJECT_CONSTRUCT_FREE_ONFAILURE;
return object_cxxConstructFromClass(obj, cls, construct_flags);
}
在第三步执行前,po obj,输出:
(lldb) po obj
0x0000000100684960
obj中并没有class信息,在第三步执行后,po obj,输出:
(lldb) po obj
<LGPerson: 0x100684960>
也就是说经过obj->initInstanceIsa(cls, hasCxxDtor);,之后对象与class关联起来。
initIsa
继续跟踪initInstanceIsa:
inline void
objc_object::initInstanceIsa(Class cls, bool hasCxxDtor)
{
ASSERT(!cls->instancesRequireRawIsa());
ASSERT(hasCxxDtor == cls->hasCxxDtor());
initIsa(cls, true, hasCxxDtor);
}
追踪到initIsa:
inline void
objc_object::initIsa(Class cls, bool nonpointer, bool hasCxxDtor)
{
ASSERT(!isTaggedPointer());
if (!nonpointer) {
isa = isa_t((uintptr_t)cls);
} else {
ASSERT(!DisableNonpointerIsa);
ASSERT(!cls->instancesRequireRawIsa());
isa_t newisa(0);
#if SUPPORT_INDEXED_ISA
ASSERT(cls->classArrayIndex() > 0);
newisa.bits = ISA_INDEX_MAGIC_VALUE;
// isa.magic is part of ISA_MAGIC_VALUE
// isa.nonpointer is part of ISA_MAGIC_VALUE
newisa.has_cxx_dtor = hasCxxDtor;
newisa.indexcls = (uintptr_t)cls->classArrayIndex();
#else
newisa.bits = ISA_MAGIC_VALUE;
// isa.magic is part of ISA_MAGIC_VALUE
// isa.nonpointer is part of ISA_MAGIC_VALUE
newisa.has_cxx_dtor = hasCxxDtor;
newisa.shiftcls = (uintptr_t)cls >> 3;
#endif
// This write must be performed in a single store in some cases
// (for example when realizing a class because other threads
// may simultaneously try to use the class).
// fixme use atomics here to guarantee single-store and to
// guarantee memory order w.r.t. the class index table
// ...but not too atomic because we don't want to hurt instantiation
isa = newisa;
}
}
分析源码可知,该函数主要实现了对变量isa的赋值。那么isa这个变量就应该是关键点,该变量中应该存储了类信息相关数据。
成员变量isa
jump to define, 可以看到:
struct objc_object {
private:
isa_t isa;
public:
isa变量的类型isa_t,是实例对象的私有成员变量。由此可知:实例对象通过私有成员变量isa关联到对应的Class
isa的结构以及初始化
isa_t 结构
- isa是一个联合体union,包含成员cls, bits, 还有一个struct
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
};
- cls是一个结构体指针
typedef struct objc_class *Class;
- bits是一个unsigned long, 64位
typedef unsigned long uintptr_t;
- struct里面使用了位域,每段都有不同的含义
# if __arm64__
# define ISA_MASK 0x0000000ffffffff8ULL
# define ISA_MAGIC_MASK 0x000003f000000001ULL
# define ISA_MAGIC_VALUE 0x000001a000000001ULL
# define ISA_BITFIELD \
uintptr_t nonpointer : 1; \
uintptr_t has_assoc : 1; \
uintptr_t has_cxx_dtor : 1; \
uintptr_t shiftcls : 33; /*MACH_VM_MAX_ADDRESS 0x1000000000*/ \
uintptr_t magic : 6; \
uintptr_t weakly_referenced : 1; \
uintptr_t deallocating : 1; \
uintptr_t has_sidetable_rc : 1; \
uintptr_t extra_rc : 19
# define RC_ONE (1ULL<<45)
# define RC_HALF (1ULL<<18)
- isa_t利用结构体、位域,在一个8字节有限空间内存储了多种信息
如果单纯的只是存储类对象指针,根本用不上64位(64位如果作为地址空间,远远超出了手机的物理空间大小,1T才40位呢,), 所以存储其他信息,空间高效利用
isa中具体字段含义
字段含义表
isa,8字节长度,从低字节到高字节,存储信息如下
| 名称 | 长度 | 意义 |
|---|---|---|
| nonpointer | 1 | 表示isa指针是否开启指针优化。0:纯指针 1:不止存储类对象地址,还包含了其他信息,具体见各行。也就是下面的几个字段必须在nonpointer为1时才有 |
| has_assoc | 1 | 关联对象标志位,0-没有,1-有 |
| has_cxx_dtor | 1 | 标记该对象是否有c++或oc的析构器,如果有,则在释放对象时需要做析构处理,如果没有,则可以更快释放掉 |
| shiftcls | 33 | 存储类对象指针 |
| magic | 6 | 用于调试器判断当前对象是真的对象还是没有初始化的空间 |
| weakly_referenced | 1 | 标记是否有ARC的weak变量指向或者曾经指向该对象,如果没有,则可以更快的释放 |
| deallocating | 1 | 标记改对象是否正在释放 |
| has_sidetable_rc | 1 | 标记该对象的是否使用了引用计数表存储引用计数 |
| extra_rc | 19 | 该对象的引用计数值,实际上是引用计数值减1 |
关于引用计数存储
根据上表的has_sidetable_rc、extra_rc可知:引用计数是分两部分存储的:
- 当引用计数值未超过extra_rc可以表示的范围时,存储在extra_rc中,最大范围为0x7FFFF(524287),已经是一个很大的值了!
- 当引用计数继续增长时,就需要使用sidetable的refcnts引用计数表,具体见下图源码分析
- 关于引用计数的处理,可以这样理解:**用一个桶和一个池子装水,桶满了后,倒一半进入池子,然后继续在第一桶中装水,满了以后再次到一半进入池子。**这样的好处就是减少对引用表的访问,减少地址跳转操作,提高性能。
- 关于RC_HALF:因为extra_rc是19位的宽度,满了以后,(extra+1)的一半,正好就是(1ULL<<18)。以3位宽度为例,extra_rc就是最大0b111(即7),再次增加后7+1=8,一半为4,即0b100,即1<<2.
- 关于RC_ONE: 因为extra_rc是从46位开始的,所以extra_rc加1的话,应该是在46位加1,即 (isa.bits + 1<<45)即可。
isa初始化
对象空间开辟完成,需要初始化isa,源码如下:
inline void
objc_object::initIsa(Class cls, bool nonpointer, bool hasCxxDtor)
{
ASSERT(!isTaggedPointer());
if (!nonpointer) {
isa = isa_t((uintptr_t)cls);
} else {
ASSERT(!DisableNonpointerIsa);
ASSERT(!cls->instancesRequireRawIsa());
isa_t newisa(0);
#if SUPPORT_INDEXED_ISA
ASSERT(cls->classArrayIndex() > 0);
newisa.bits = ISA_INDEX_MAGIC_VALUE;
// isa.magic is part of ISA_MAGIC_VALUE
// isa.nonpointer is part of ISA_MAGIC_VALUE
newisa.has_cxx_dtor = hasCxxDtor;
newisa.indexcls = (uintptr_t)cls->classArrayIndex();
#else
newisa.bits = ISA_MAGIC_VALUE;
// isa.magic is part of ISA_MAGIC_VALUE
// isa.nonpointer is part of ISA_MAGIC_VALUE
newisa.has_cxx_dtor = hasCxxDtor;
newisa.shiftcls = (uintptr_t)cls >> 3;
#endif
// This write must be performed in a single store in some cases
// (for example when realizing a class because other threads
// may simultaneously try to use the class).
// fixme use atomics here to guarantee single-store and to
// guarantee memory order w.r.t. the class index table
// ...but not too atomic because we don't want to hurt instantiation
isa = newisa;
}
}
- 纯指针isa的设置: 直接bits设置为cls即可
isa = isa_t((uintptr_t)cls);
- nonpointer的isa,设置shiftcls: 将cls左移三位
newisa.shiftcls = (uintptr_t)cls >> 3;
左移难道不怕丢失数据吗?毕竟左移后,前3位bit就丢失了。猜测这三位应该都是0才对。po一下,看看:
(lldb) po cls
0x00000001003f00f0
果然。另外,正好也保证了纯指针isa时的nonpointer位的值是0,设计真是巧妙
总结
- oc对象利用isa变量存储类对象指针
- isa是一个union类型,利用了结构体位域技术,定义了多个数据段,除了class指针还存储额外信息,提高了空间利用率
- nonpointer标记了是否存储额外信息
- 利用extra_rc和has_sidetable_rc以及sidetable共同通管理引用计数