0%

iOS底层:calloc和isa

calloc底层

calloc的源码在libMalloc,不是之前用的libObjc

1
2
3
4
5
void *
calloc(size_t num_items, size_t size)
{
return _malloc_zone_calloc(default_zone, num_items, size, MZ_POSIX);
}

可以看出calloc调用了_malloc_zone_calloc函数

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
MALLOC_NOINLINE
static void *
_malloc_zone_calloc(malloc_zone_t *zone, size_t num_items, size_t size,
malloc_zone_options_t mzo)
{
MALLOC_TRACE(TRACE_calloc | DBG_FUNC_START, (uintptr_t)zone, num_items, size, 0);

void *ptr;
if (malloc_check_start) {
internal_check();
}
// 这里调用了calloc函数
ptr = zone->calloc(zone, num_items, size);

if (os_unlikely(malloc_logger)) {
malloc_logger(MALLOC_LOG_TYPE_ALLOCATE | MALLOC_LOG_TYPE_HAS_ZONE | MALLOC_LOG_TYPE_CLEARED, (uintptr_t)zone,
(uintptr_t)(num_items * size), 0, (uintptr_t)ptr, 0);
}

MALLOC_TRACE(TRACE_calloc | DBG_FUNC_END, (uintptr_t)zone, num_items, size, (uintptr_t)ptr);
if (os_unlikely(ptr == NULL)) {
malloc_set_errno_fast(mzo, ENOMEM);
}
return ptr;
}

可以看到,如果继续调用就又回到了calloc函数,这个时候我们借助LLDB打印内存地址。

1
2
p zone->calloc
会输出:(void *(*)(_malloc_zone_t *, size_t, size_t)) $0 = 0x00000001002e1b87 (.dylib`default_zone_calloc at malloc.c:385)

可以看出这里调用了malloc.c文件的385行

1
2
3
4
5
6
7
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);
}

default_zone_calloc里依然会调用calloc,我们用相同的方法查看具体执行的代码。

1
2
p zone->calloc
(void *(*)(_malloc_zone_t *, size_t, size_t)) $1 = 0x00000001002e72cd (.dylib`nano_calloc at nano_malloc.c:878)

这次执行的代码在nano_malloc.c的878行。nano_calloc函数的实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
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函数,内存大小分配应该是调用segregated_size_to_fit函数。

1
size_t slot_bytes = segregated_size_to_fit(nanozone, size, &slot_key);
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
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 (先除以16余数舍去,再乘以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字节对齐,原因:1、内存是连续的,通过 16 字节对齐规避风险和容错,防止访问溢出。2、提高了寻址访问效率,也就是空间换时间

isa底层

isa是一个联合体,是从内存管理层面来设计的,因为联合体是所有成员共享一个内存,联合体内存的大小取决于内部成员内存大小最大的那个元素,对应isa指针来说,就不用额外声明很多属性,直接在内部ISA_BITFIELD保存信息。同时由于联合体属性间是互斥的,所以 clsbitsisa 初始化流程时是在两个分支中被赋值的。

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
union isa_t {
isa_t() { }
isa_t(uintptr_t value) : bits(value) { }

uintptr_t bits;

private:
// Accessing the class requires custom ptrauth operations, so
// force clients to go through setClass/getClass by making this
// private.
Class cls;

public:
#if defined(ISA_BITFIELD)
struct {
ISA_BITFIELD; // defined in isa.h
};

bool isDeallocating() {
return extra_rc == 0 && has_sidetable_rc == 0;
}
void setDeallocating() {
extra_rc = 0;
has_sidetable_rc = 0;
}
#endif

void setClass(Class cls, objc_object *obj);
Class getClass(bool authenticated);
Class getDecodedClass(bool authenticated);
};

下面是isa的初始化

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
inline void 
objc_object::initIsa(Class cls, bool nonpointer, UNUSED_WITHOUT_INDEXED_ISA_AND_DTOR_BIT bool hasCxxDtor)
{
ASSERT(!isTaggedPointer());

isa_t newisa(0);

if (!nonpointer) {
newisa.setClass(cls, this);
} else {
ASSERT(!DisableNonpointerIsa);
ASSERT(!cls->instancesRequireRawIsa());


#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
# if ISA_HAS_CXX_DTOR_BIT
newisa.has_cxx_dtor = hasCxxDtor;
# endif
newisa.setClass(cls, this);
#endif
newisa.extra_rc = 1;
}

// 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_BITFIELD,其大小为 8 个字节,也就是 64 位。

1
2
3
4
5
6
7
8
9
10
#   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 unused : 1; \
uintptr_t has_sidetable_rc : 1; \
uintptr_t extra_rc : 8
  • nonpointer: 表示是否对 isa 指针开启指针优化 0:纯 isa 指针 1:不止是类对象地址, isa 中包含了类信息、对象的引用计数等
  • has_assoc: 关联对象标志位,0 没有,1 存在
  • has_cxx_dtor: 该对象是否有 C++ 或者 Objc 的析构器,如果有析构函数,则需要做析构逻辑, 如果没有,则可以更快的释放对象
  • shiftcls: 存储类指针的值。开启指针优化的情况下,在 arm64 架构中有 33 位用来存储类指针。
  • magic: 用于调试器判断当前对象是真的对象还是没有初始化的空间
  • weakly_referenced: 标志对象是否被指向或者曾经指向一个 ARC 的弱变量,没有弱引用的对象可以更快释放。
  • has_sidetable_rc: 当对象引用技术大于 10 时,则需要借用该变量存储进位
  • extra_rc: 当表示该对象的引用计数值,实际上是引用计数值减 1, 例如,如果对象的引用计数为 10,那么 extra_rc 为 9。如果引用计数大于 10, 则需要使用到下面的 has_sidetable_rc。

isa关联对象和类

isa 是对象中的第一个属性,因为这一步是在继承的时候发生的,要早于对象的成员变量,属性列表,方法列表以及所遵循的协议列表。

我们在探索 alloc 底层原理的时候,有一个方法叫做 initIsa

这个方法的作用就是初始化 isa 联合体位域。其中有这么一行代码:

1
newisa.shiftcls = (uintptr_t)cls >> 3;

通过这行代码,我们知道 shiftcls 这个位域其实存储的是类的信息。这个类就是实例化对象所指向的那个类。

这里的左移右移操作其实很好理解,首先我们先观察 isaISA_BITFIELD 位域的结构:

1
2
3
4
5
6
7
8
9
10
#   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 unused : 1; \
uintptr_t has_sidetable_rc : 1; \
uintptr_t extra_rc : 8

我们可以看到,ISA_BITFIELD 的前 3 位是 nonpointerhas_assochas_cxx_dtor,中间 44 位是 shiftcls ,后面 17 位是剩余的内容,同时因为 iOS 是小端模式,那么我们就需要去掉右边的 3 位和左边的 17位,所以就会采用 >>3<<3 然后 <<17>>17 的操作了。

通过这个测试,我们就知道了 isa 实现了对象与类之间的关联。

我们还可以探索 object_getClass 底层,可以发现有这样一行代码:

1
return (Class)(isa.bits & ISA_MASK);

这行代码就是将 isa 中的联合体位域与上一个蒙版,这个蒙版定义是怎么样的呢?

1
#   define ISA_MASK        0x00007ffffffffff8ULL

x00007ffffffffff8ULL 这个值我们转成二进制表示:

1
2
0000 0000 0000 0000 0111 1111 1111 1111 
1111 1111 1111 1111 1111 1111 1111 1000

结果一目了然,这个蒙版就是帮我们去过滤掉除 shiftcls 之外的内容。

我们直接将对象的 isa 地址与上这个mask之后,就会得到 object.class 一样的内存地址。

isa走位分析

类与元类

我们都知道对象可以创建多个,但是类是否可以创建多个呢?
答案很简单,一个。那么如果来验证呢?

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
//MARK: - 分析类对象内存存在个数
void lgTestClassNum(){
Class class1 = [LGPerson class];
Class class2 = [LGPerson alloc].class;
Class class3 = object_getClass([LGPerson alloc]);
Class class4 = [LGPerson alloc].class;
NSLog(@"\n%p-\n%p-\n%p-\n%p",class1,class2,class3,class4);
}

// 打印输出如下:

0x100002108-
0x100002108-
0x100002108-
0x100002108

所以我们就知道了类在内存中只会存在一份。

1
2
3
4
5
6
7
8
9
10
(lldb) x/4gx LGTeacher.class
0x100001420: 0x001d8001000013f9 0x0000000100b38140
0x100001430: 0x00000001003db270 0x0000000000000000
(lldb) po 0x001d8001000013f9
17082823967917874

(lldb) p 0x001d8001000013f9
(long) $2 = 8303516107936761
(lldb) po 0x100001420
LGTeacher

我们通过上面的打印,就发现 类的内存结构里面的第一个结构打印出来还是 LGTeacher,那么是不是就意味着 对象->类->类 这样的死循环呢?这里的第二个类其实是 元类。是由系统帮我们创建的。这个元类也无法被我们实例化。

对象的本质

在我们认知里面,OC 对象的本质就是一个结构体,这个结论在 libObjc 源码的 objc-private.h 源文件中可以得到证实。

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-runtime-new.h 源文件中找到

1
2
3
4
5
6
7
8
9
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

...省略其他的内容...

}

也就是说 objc_class 内存中第一个位置是 isa,第二个位置是 superclass

不过我们本着求真的态度可以用 clang 来重写我们的 OC 源文件来查看是不是这么回事。

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

这行命令会把我们的 main.m 文件编译成 C++ 格式,输出为 main.cpp

我们可以看到 LGPerson 对象在底层其实是一个结构体 objc_object

而我们的 Class 在底层也是一个结构体 objc_class