OC底层原理03:alloc&init&new探索

张建 lol

前言

  • 在分析alloc&init&new源码之前,我们先来看看下面的代码:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
- (void)viewDidLoad {
[super viewDidLoad];

// 熟悉的入手 - 对象
// alloc 做了什么?
// init 做了什么?

ZJPerson *p1 = [ZJPerson alloc];
ZJPerson *p2 = [p1 init];
ZJPerson *p3 = [p1 init];
ZJNSLog(@"%@ - %p - %p",p1,p1,&p1);
ZJNSLog(@"%@ - %p - %p",p2,p2,&p2);
ZJNSLog(@"%@ - %p - %p",p3,p3,&p3);
}

查看打印结果,分别输出3个对象的内容、内存地址、指针地址

1
2
3
<ZJPerson: 0x600001316130> - 0x600001316130 - 0x7ffee10e6188
<ZJPerson: 0x600001316130> - 0x600001316130 - 0x7ffee10e6180
<ZJPerson: 0x600001316130> - 0x600001316130 - 0x7ffee10e6178

由上述打印结果,我们可以知道:

  • p1,p2,p3对象内存地址 是相同的

  • 返回的 指针变量p1,p2,p3 指向对象的 内存地址 也是相同的

  • 指针p的地址&p 是不同的

结论:

  • 说明 alloc开辟了内存空间,而 init没有开辟内存空间

  • p1,p2,p3指针变量指向了同一个内存空间ZJPerson

下面用一张图说明一下:

这就是本文需要探索的内容,alloc做了什么?init做了什么?,alloc&init&new到底干了什么?

准备工作

  1. 下载 objc4-781 源码

  2. 编译源码,可参考iOS-OC底层原理02:Objc4源码编译

alloc源码探索

  1. 创建一个ZJPerson自定义类,来分析一下底层调用流程

  1. 跟踪 alloc 调用流程
  • 通过断点调试 alloc 调用 _objc_rootAlloc
1
2
3
+ (id)alloc {
return _objc_rootAlloc(self);
}
  • 通过断点调试 _objc_rootAlloc 调用 callAlloc
1
2
3
4
_objc_rootAlloc(Class cls)
{
return callAlloc(cls, false/*checkNil*/, true/*allocWithZone*/);
}
  • 通过断点调试 callAlloc 调用 _objc_rootAllocWithZone
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
static ALWAYS_INLINE id
callAlloc(Class cls, bool checkNil, bool allocWithZone=false) // alloc 源码 第三步
{
#if __OBJC2__ // 有可用的编译器优化
// checkNil 为false,!cls 也为false ,所以slowpath 为 false,假值判断不会走到if里面,即不会返回nil
if (slowpath(checkNil && !cls)) return nil;

// hasCustomAWZ实际意义是 hasCustomAllocWithZone —— 这里表示有没有alloc / allocWithZone的实现
if (fastpath(!cls->ISA()->hasCustomAWZ())) {
// 继承自NSObject/NSProxy的类才能走到这里,在oc中基本都继承自这两个类
return _objc_rootAllocWithZone(cls, nil);
}
#endif

// No shortcuts available. // 没有可用的编译器优化
if (allocWithZone) {
return ((id(*)(id, SEL, struct _NSZone *))objc_msgSend)(cls, @selector(allocWithZone:), nil);
}
return ((id(*)(id, SEL))objc_msgSend)(cls, @selector(alloc));
}

这里补充一下 cls->ISA()->hasCustomAWZ() 是什么意思呢?

其中fastpath中的 cls->ISA()->hasCustomAWZ() 这里表示hasCustomAWZ实际意义是hasCustomAllocWithZone——这里表示有没有alloc / allocWithZone的实现(只有不是继承NSObject/NSProxy的类才为true),这里通过断点调试,是没有自定义的实现,所以会执行到 if 里面的代码,即走到 _objc_rootAllocWithZone

这里补充一下 slowpath & fastpath 是什么?

其中关于 slowpathfastpath 这里需要简要说明下,这两个都是 objc 源码中定义的宏,其定义如下:

1
2
3
4
// x很可能为真, fastpath 可以简称为 真值判断
#define fastpath(x) (__builtin_expect(bool(x), 1))
// x很可能为假,slowpath 可以简称为 假值判断
#define slowpath(x) (__builtin_expect(bool(x), 0))

其中的 __builtin_expect 指令是由 gcc 引入的,

1
2
3
4
5
6
7
8
9
10
11
目的:编译器可以对代码进行优化,以减少指令跳转带来的性能下降。即性能优化

作用:允许程序员将最有可能执行的分支告诉编译器。

指令的写法为:__builtin_expect(EXP, N)。表示 EXP==N 的概率很大。

fastpath定义中__builtin_expect((x),1)表示 x 的值为真的可能性更大;即 执行if 里面语句的机会更大

slowpath定义中的__builtin_expect((x),0)表示 x 的值为假的可能性更大。即执行 else 里面语句的机会更大

在日常的开发中,也可以通过设置来优化编译器,达到性能优化的目的,设置的路径为:`Build Setting --> Optimization Level --> Debug -->` 将None 改为 fastest 或者 smallest,即编译器优化
  • 通过断点调试 _objc_rootAllocWithZone 调用 _class_createInstanceFromZone
1
2
3
4
5
6
_objc_rootAllocWithZone(Class cls, malloc_zone_t *zone __unused)
{
// allocWithZone under __OBJC2__ ignores the zone parameter
return _class_createInstanceFromZone(cls, 0, nil,
OBJECT_CONSTRUCT_CALL_BADALLOC);
}
  • 通过断点调试 _class_createInstanceFromZone 内部调用 cls->instanceSize、calloc、obj->initInstanceIsa,最后返回对象
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
_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

// hasCxxCtor()是判断当前class或者superclass是否有.cxx_construct 构造方法的实现
bool hasCxxCtor = cxxConstruct && cls->hasCxxCtor();

// hasCxxDtor()是判断判断当前class或者superclass是否有.cxx_destruct 析构方法的实现
bool hasCxxDtor = cls->hasCxxDtor();

// canAllocNonpointer()是具体标记某个类是否支持优化的isa
bool fast = cls->canAllocNonpointer();
size_t size;

// 1.返回开辟内存大小
size = cls->instanceSize(extraBytes);
if (outAllocatedSize) *outAllocatedSize = size;

id obj;
if (zone) {
obj = (id)malloc_zone_calloc((malloc_zone_t *)zone, 1, size);
} else {
// 2.开辟内存
obj = (id)calloc(1, size);
}
if (slowpath(!obj)) {
if (construct_flags & OBJECT_CONSTRUCT_CALL_BADALLOC) {
return _objc_callBadAllocHandler(cls);
}
return nil;
}

// 3. 初始化指针关联到相应的类
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);
}

// 4.编译器优化返回对象
if (fastpath(!hasCxxCtor)) {
return obj;
}

construct_flags |= OBJECT_CONSTRUCT_FREE_ONFAILURE;
return object_cxxConstructFromClass(obj, cls, construct_flags);
}
  • 调用的流程图

  1. cls->instanceSize 实例对象返回开辟内存的大小?

点击 instanceSize 跟踪断点去看源码来分析:

1) 调用的是下面两个函数

1
2
3
4
size_t size = _flags & FAST_CACHE_ALLOC_MASK;
// remove the FAST_CACHE_ALLOC_DELTA16 that was added
// by setFastInstanceSize
return align16(size + extra - FAST_CACHE_ALLOC_DELTA16);

FAST_CACHE_ALLOC_MASK 下

1
2
3
#define FAST_CACHE_ALLOC_MASK         0x1ff8
#define FAST_CACHE_ALLOC_MASK16 0x1ff0
#define FAST_CACHE_ALLOC_DELTA16 0x0008
  • size 得到的值是 16
  • extra 得到的值是 0
  • FAST_CACHE_ALLOC_DELTA16 得到的值为 = 16 + 0 - 8 = 8
1
2
3
(lldb) po 0x0008
8

2)然后我们计算一下 16 字节对齐 align16 到底返回多少字节?

计算一下(x + size_t(15))) & ~size_t(15)的值

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
8 + 15 = 23
0000 0000 0001 0111

15
0000 0000 0000 1111
~1111 1111 1111 0000

23 & ~15
0000 0000 0001 0111
1111 1111 1111 0000
-----------------------
0000 0000 0001 0000

最后的值为16
0000 0000 0001 0000

结论:留下了16的倍数,系统返回了16字节的内存

3)为什么是16字节的对齐?

  • 方便快捷
  • 16字节更加安全
  • 看起来不像 8字节 那么紧凑,并满足以后发展
  1. calloc

1)调用下面的方法去开辟内存

1
2
// 开辟内存
obj = (id)calloc(1, size);

2)下断点到obj = (id)calloc(1, size);在这个地方打印po:

1
2
(lldb) po obj
0x000000010241e660

打印的是指针变量的地址,没有打印出对象,说明这个地方还没有返回对象

  1. obj->initInstanceIsa

1)断点查看下面的源码

1
2
3
4
5
6
7
8
// 3.初始化指针关联到相应的类
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);
}

2)下断点到 obj->initInstanceIsa(cls, hasCxxDtor); 到这行打印 po

1
2
3
4
(lldb) po obj
<ZJPerson: 0x10241e660>

(lldb)

说明在这个位置才是 将初始化的指针关联到对象

3)返回对象

1
2
3
4
// 4.编译器优化返回对象
if (fastpath(!hasCxxCtor)) {
return obj;
}

对象开辟内存的影响因素

  1. 给对象添加属性

ZJPerson.h文件

1
2
3
4
@interface ZJPerson : NSObject
@property (nonatomic, copy) NSString *name;
@property (nonatomic, assign) int age;
@end

main.h文件

1
2
3
4
5
6
7
8
9
10
11
12
int main(int argc, const char * argv[]) {
@autoreleasepool {
// insert code here...

ZJPerson * p = [ZJPerson alloc];
p.name = @"zj"; // 8
p.age = 30; // 4
NSLog(@"申请内存大小为:%lu——-系统开辟内存大小为:%lu",class_getInstanceSize([p class]),malloc_size((__bridge const void *)(p)));

}
return 0;
}
  1. 分析结果:
1
2
3
4
5
6
7
8
9
10
分析结果:
1. 成员变量应该分配内存:8字节 + 4字节 = 12字节
2. 再加上isa的8字节 = 20字节
3. 根据对象8字节对齐原则最大8字节,所以20往大走补充到24字节
4. 所以内存空间应该分配24字节
5. 注意:对象开辟空间的时候成员变量就会编译进来,所以成员变量未赋值也会分配内存
6. 系统由于是16字节对齐原则,所以系统开辟的内存大小为16的倍数 = 16 * 2 = 32,而24大于16小于32,所以为32字节

实际打印结果 :
2020-09-06 15:28:31.072150+0800 ZJObjc[15774:988136] 申请内存大小为:24——-系统开辟内存大小为:32
  1. 分析内存情况

1)打断点在 ZJPerson * p = [ZJPerson alloc]; 行,终端输入:

1
2
3
4
(lldb) x p
0x1006414b0: 35 22 00 00 01 80 1d 00 00 00 00 00 00 00 00 00 5"..............
0x1006414c0: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 ................
(lldb)

0x1006414b0 为首地址,在iOS中内存是 小端模式,由于没有赋值name和age所以 35 22 00 00 01 80 1d 00 后面的 00 00 00 00 00 00 00 00 是没值的

2)当我们打断点在NSLog(@”申请内存大小为…行时,终端输入:

1
2
3
4
(lldb) x/4gx p
0x1006414b0: 0x001d800100002235 0x000000000000001e
0x1006414c0: 0x0000000100001018 0x0000000000000000
(lldb)

此时,我们看到 0x000000000000001e0x0000000100001018 是有值的,分别代表age=30name=zj

1
2
3
4
5
(lldb) po 0x000000000000001e
30
(lldb) po 0x0000000100001018
zj

init探索

  1. 查看源码:
1
2
3
4
5
6
7
8
9
10
11
- (id)init {
return _objc_rootInit(self);
}

_objc_rootInit(id obj)
{
// In practice, it will be hard to rely on this function.
// Many classes do not properly chain -init calls.
return obj;
}

由源码可知:init什么都没做,只是调用了 _objc_rootInit 重写 init,即构造方法,给用户提供入口去实现工厂设计。

new

查看源码

1
2
3
+ (id)new {
return [callAlloc(self, false/*checkNil*/) init];
}

发现和 alloc 调用 callAlloc 是一个流程,即等同于 new = alloc + init,但是有一点区别 没有自定义的构造方法,所以一般自定义的类会用 alloc init,系统的类用new。

总结

本人小白,很多细节问题待补充,希望多多指点。

  • Post title:OC底层原理03:alloc&init&new探索
  • Post author:张建
  • Create time:2020-09-07 01:05:36
  • Post link:https://redefine.ohevan.com/2020/09/07/OC底层原理/OC底层原理03:alloc&init&new源码分析/
  • Copyright Notice:All articles in this blog are licensed under BY-NC-SA unless stating additionally.