iOS底层探索之对象原理(二)
前言
iOS底层探索之对象原理(一)中了解到通过calloc
我们对象有了内存地址,通过initInstanceIsa
和我们对象有了关联,本文将继续探索如我们对象中不同属性,将如何影响开辟的内存大小,及对象结构里面的 isa 是怎么关联到我们的对象的内存地址。
内存对齐原理
内存对齐的原则
- 数据成员对⻬规则:结构(struct)(或联合(union))的数据成员,第⼀个数据成员放在offset为0的地⽅,以后每个数据成员存储的起始位置要从该成员⼤⼩或者成员的⼦成员⼤⼩(只要该成员有⼦成员,⽐如说是数组,结构体等)的整数倍开始(⽐如int为4字节,则要从4的整数倍地址开始存储
- 结构体作为成员:如果⼀个结构⾥有某些结构体成员,则结构体成员要从其内部最⼤元素⼤⼩的整数倍地址开始存储.(struct a⾥存有struct b,b⾥有char,int ,double等元素,那b应该从8的整数倍开始存储.)
- 结构体的总⼤⼩,也就是sizeof的结果,.必须是其内部最⼤成员的整数倍.不⾜的要补⻬。
结构体内存对齐
struct StructOne {
char a; // 1字节
double b; // 8字节
int c; // 4字节
short d; // 2字节
} MyStruct1;
struct StructTwo {
double b; // 8字节
int c; // 4字节
char a; // 1字节
short d; // 2字节
} MyStruct2;
struct StructOThree {
double b; // 8字节 0 - 7
char a; // 1字节 min(8, 1) 8
int c; // 4字节 min(9, 4) 9不是4整数倍,则9,10,11不能用,12,13,14,15就是当前位置
short d; // 2字节 min(16, 2) 16是2的整数倍,则排16,17 —— 对齐为24
} MyStruct3;
NSLog(@"%lu---%lu---%lu",sizeof(MyStruct1),sizeof(MyStruct2),sizeof(MyStruct3));
打印结果: 24---16---24
从内存对齐原则来看,上面三个结构体在内存中应该是这样的: 结构体内存对齐.png类属性内存对齐
- 通过测试,类属性不是按顺序进行内存分配,如果按照对象默认的属性顺序进行内存分配,在进行 属性的8字节对齐 环节时会浪费大量的内存空间,所以这里系统会帮我们把对象的属性重新排列来最大化利用我们的内存空间,这种操作被称为二进制重排
对象申请内存VS系统开辟内存
Person *p = [Person alloc];
p.name = @"Kaemi"; // NSString 8
p.age = 18; // int 4
p.height = 188; // long 8
p.hobby = @"game"; // NSString 8
NSLog(@"申请内存大小为:%lu——-系统开辟内存大小为:%lu",class_getInstanceSize([p class]),malloc_size((__bridge const void *)(p)));
打印结果: 对象申请内存大小为:40---系统开辟内存大小为:48
40 个字节不难理解,是因为当前对象有 4 个属性,有三个属性为 8 个字节,有一个属性为 4个字节,再加上 isa 的 8 个字节,就是 32 + 4 = 36 个字节,然后根据内存对齐原则,36 不能被 8 整除,36 往后移动刚好到了 40 就是 8 的倍数,所以内存大小为 40。
48 个字节的话需要我们探索 calloc 的底层原理。
这里还有一个注意点,就是class_getInstanceSize
和malloc_size
对同一个对象返回的结果不一样的,原因是malloc_size
是直接返回的calloc
之后的指针的大小,如
size_t instanceSize(size_t extraBytes) {
size_t size = alignedInstanceSize() + extraBytes;
// CF requires all objects be at least 16 bytes.
if (size < 16) size = 16;
return size;
}
而class_getInstanceSize
内部实现是:
size_t class_getInstanceSize(Class cls)
{
if (!cls) return 0;
return cls->alignedInstanceSize();
}
也就是说class_getInstanceSize
会输出 8 个字节,malloc_size
会输出 16 个字节,当然前提是该对象没有任何属性。
calloc原理探索
从calloc
函数出发,在 libobjc 源码中无法进入具体实现,通过Xcode观察,知道calloc
需通过 libmalloc 源码进行
这里有个小技巧,其实我们研究的是 calloc 的底层原理,而 libobjc 和 libmalloc 是相互独立的,所以在 libmalloc 源码里面,我们没必要去走 calloc 前面的流程了。我们通过断点调试 libobjc 源码可以知道第二个参数是 40: (这是因为当前发送 alloc 消息的对象有 4 个属性,每个属性 8 个字节,再加上 isa 的 8 个字节,所以就是 40 个字节)
接下来我们打开 libmalloc 的源码,在新建的 target 中直接手动声明如下的代码:
void *p = calloc(1, 40);
NSLog(@"%lu", malloc_size(p));
运行之后我们一直沿着源码断点下去,会来到malloc_zone_calloc
中这么一段代码
ptr = zone->calloc(zone, num_items, size);
这里我们可以直接在断点处使用 LLDB 命令打印这行代码来看具体实现是位于哪个文件中
p zone->calloc
输出: (void *(*)(_malloc_zone_t *, size_t, size_t)) $1 = 0x00000001003839c7 (.dylib`default_zone_calloc at malloc.c:249)
确定default_zone_calloc
,再搜索它的实现源码
static void *
default_zone_calloc(malloc_zone_t *zone, size_t num_items, size_t size)
{
zone = runtime_default_zone();
return zone->calloc(zone, num_items, size);
}
但是我们发现这里又是一次zone->calloc
,我们接着再次使用 LLDB 打印内存地址:
p zone->calloc
输出: (void *(*)(_malloc_zone_t *, size_t, size_t)) $0 = 0x0000000100384faa (.dylib`nano_calloc at nano_malloc.c:884)
我们再次来到nano_calloc
方法
static void *
nano_calloc(nanozone_t *nanozone, size_t num_items, size_t size)
{
size_t total_bytes;
if (calloc_get_size(num_items, size, 0, &total_bytes)) {
return NULL;
}
if (total_bytes <= NANO_MAX_SIZE) {
void *p = _nano_malloc_check_clear(nanozone, total_bytes, 1);
if (p) {
return p;
} else {
/* FALLTHROUGH to helper zone */
}
}
malloc_zone_t *zone = (malloc_zone_t *)(nanozone->helper_zone);
return zone->calloc(zone, 1, total_bytes);
}
我们简单分析一下,应该往_nano_malloc_check_clear
里面继续走,然后我们发现 _nano_malloc_check_clear
里面内容非常多,这个时候我们要明确一点,我们的目的是找出 48 是怎么算出来的,经过分析之后,我们来到segregated_size_to_fit
static MALLOC_INLINE size_t
segregated_size_to_fit(nanozone_t *nanozone, size_t size, size_t *pKey)
{
// size = 40
size_t k, slot_bytes;
if (0 == size) {
size = NANO_REGIME_QUANTA_SIZE; // Historical behavior
}
// 40 + 16-1 >> 4 << 4
// 40 - 16*3 = 48
// 16
k = (size + NANO_REGIME_QUANTA_SIZE - 1) >> SHIFT_NANO_QUANTUM; // round up and shift for number of quanta
slot_bytes = k << SHIFT_NANO_QUANTUM; // multiply by power of two quanta size
*pKey = k - 1; // Zero-based!
return slot_bytes;
}
这里可以看出进行的是 16 字节对齐,那么也就是说我们传入的 size 是 40,在经过 (40 + 16 - 1) >> 4 << 4 操作后,结果为48,也就是16的整数倍。
总结
- 对象的属性是进行的 8 字节对齐
-
对象自己进行的是 16 字节对齐
- 因为内存是连续的,通过 16 字节对齐规避风险和容错,防止溢出
- 同时,也提高了寻址访问效率,也就是空间换时间
编译器优化
在 release 模式下,OptimizationLevel 为 Fastest,Smallest,编译器会进行优化,把在汇编中进行的一些运算操作给优化了。编译器可以从下列 4 个纬度优化:
- 编译时间
- 链接时间
- 运行时间
- 空闲时间
联合体位域
我们探索 isa 的时候,会发现 isa 其实是一个联合体,而这其实是从内存管理层面来设计的,以为联合体是所有成员共享一个内存,联合体内存的大小取决于内部成员内存大小最大的那个元素,对于 isa 指针来说,就不用额外声明很多的属性,直接在内部的 ISA_BITFIELD 保存信息。
isa结构
isa
是存在对象中类型是isa_t的联合体,有一个结构体属性为 ISA_BITFIELD,其大小为 8 个字节,也就是 64 位
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
};
# define ISA_MASK 0x00007ffffffffff8ULL
# define ISA_MAGIC_MASK 0x001f800000000001ULL
# define ISA_MAGIC_VALUE 0x001d800000000001ULL
# define ISA_BITFIELD \
uintptr_t nonpointer : 1; \
uintptr_t has_assoc : 1; \
uintptr_t has_cxx_dtor : 1; \
uintptr_t shiftcls : 44; /*MACH_VM_MAX_ADDRESS 0x7fffffe00000*/ \
uintptr_t magic : 6; \
uintptr_t weakly_referenced : 1; \
uintptr_t deallocating : 1; \
uintptr_t has_sidetable_rc : 1; \
uintptr_t extra_rc : 8
# define RC_ONE (1ULL<<56)
# define RC_HALF (1ULL<<7)
-
nonpointer
:表示是否对 isa 指针开启指针优化- 0:纯isa指针
- 1:不⽌是类对象地址,isa 中包含了类信息、对象的引⽤计数等
-
has_assoc
:关联对象标志位,0没有,1存在 -
has_cxx_dtor
:该对象是否有 C++ 或者 Objc 的析构器,如果有析构函数,则需要做析构逻辑, 如果没有,则可以更快的释放对象 -
shiftcls
:存储类指针的值。开启指针优化的情况下,在 arm64 架构中有 33 位⽤来存储类指针 -
magic
:⽤于调试器判断当前对象是真的对象还是没有初始化的空间 -
weakly_referenced
:标志对象是否被指向或者曾经指向一个 ARC 的弱变量,没有弱引用的对象可以更快释放 -
deallocating
:标志对象是否正在释放内存 -
has_sidetable_rc
:当对象引用技术大于 10 时,则需要借用该变量存储进位 -
extra_rc
:当表示该对象的引用计数值,实际上是引用计数值减 1, 例如,如果对象的引用计数为 10,那么 extra_rc 为 9。如果引用计数大于 10, 则需要使用到下面的 has_sidetable_rc