alloc做了什么?
我们在实例化对象的时候,通常会调用alloc方法和init方法
Person *p1 = [Person alloc];
Person *p2 = [p1 init];
Person *p3 = [p1 init];
NSLog(@"p1: %@--%p--%p",p1, p1, &p1);
NSLog(@"p2: %@--%p--%p",p2, p2, &p2);
NSLog(@"p3: %@--%p--%p",p3, p3, &p3);
打印结果为:
p1: <Person: 0x600002f5c020>--0x600002f5c020--0x16b09bfc8
p2: <Person: 0x600002f5c020>--0x600002f5c020--0x16b09bfc0
p3: <Person: 0x600002f5c020>--0x600002f5c020--0x16b09bfb8
由此结果可以看出,p1,p2,p3指向了同一块内存区域,如图所示:
而且我们可以发现,三个指针的地址是连续的,因为在栈上,且相差八个字节,可以得出一个对象的指针大小为8字节。
如何找到对应的源码?
上面的探索初步可以得出,alloc会在堆上申请空间,但是具体怎么做的,我们就需要对底层进行探究。但是我们发现苹果并没有对alloc的实现在文档有详细的说明。有三种方式可以继续探究:
1、通过符号断点去定位
- 添加符号断点:
- 运行后发现,调用alloc的地方很多,无法区分是我们需要的。
- 可以先把该断点放行,在我们需要的地方打上断点,当断点到了我们需要的地方再将符号断点恢复,继续放行,这样就可以查看指定位置的符号断点了
从最后一张图片可以得出两个信息: 1、实际上调用的是NSObject的alloc方法,因为Person类继承自NSObject,父类实现了该方法,Person类并未实现。 2、alloc方法所在的动态库名为libobjc.A.dylib
step into:
2、通过control+step into和符号断点
-
先在需要探究的地方打上断点,运行程序
-
断点进行到对应位置,按住control键,点击如图位置
- 接着下符号断点,会显示该符号所在动态库的名字(我用的12.3版本的Xcode没有显示,不知道为什么)
3、通过汇编跟流程
- 同样先下断点
- 如图处选择,运行程序
- 找到符号后,如法炮制。
源码的探索
接着我们就去找苹果开源代码: opensource.apple.com/tarballs/
也可以去github上的苹果的仓库里面去找: github.com/apple-oss-d…
目前我们用838版本,打开源码后,我们全局搜索alloc {,
点进去,
发现调用了_objc_rootAlloc方法,jump to Definition不起作用,因为还没编译,就先通过全局搜索的方式搜索该方法
如法炮制,找callAlloc方法
但是我们发现这个方法中出现了条件分支,这样就不知道该走哪条分支。怎么办呢
从上面的探索中我们得到了几个符号:_objc_rootAlloc, callAlloc, _objc_rootAllocWithZone, 我们可以通过在测试项目中打符号断点的方法进行流程跟随,从而确定它的调用流程。
这里注意,在运行程序前先把符号断点关掉,为了避免无谓的断点干扰,因为程序中不止一个地方调用了alloc,当程序运行起来,断点断到目标位置后再打开符号断点,接着放行断点,会发现断点分别会在_objc_rootAlloc,_objc_rootAllocWithZone处停住,也就说明了该调用流程为alloc --> _objc_rootAlloc --> _objc_rootAllocWithZone,可是我们在源码中却看到明明调用了callAlloc方法,这里就牵扯到了编译器优化。
编译器优化
如图代码,打开汇编选项,运行程序
汇编如下:
其中w开头的指的是32位寄存器,x开头的指的是64位寄存器,可是我们运行的是64位系统,为啥会有32位寄存器,这是因为int类型的4个字节,32位足够,这样可以避免无谓的浪费。
0x100031e60 <+56>: mov w8, #0xa表示将10赋值给w8寄存器。
0x100031e64 <+60>: str w8, [sp, #0xc]表示将w8寄存器的值,传送到地址值为[sp, #0xc](sp表示堆栈指针,后面的表示偏移量)的(存储器)内存中。
0x100031e70 <+72>: ldr w8, [sp, #0xc]表示读取该地址值的值到w8中。
我们可以通过register read xxxlldb命令读取寄存器的值;
我们在Build Settings中的Optimization Level选项中,可以看到编译器在Release下是Fastest,Smallest,我们把Debug也改成一样的选项。
运行程序,会发现汇编变少了,这就是所谓的编译器优化。
可以联想到上面我们在探索alloc的源码调用流程中的callAlloc方法,也是被编译器优化掉了,这样可以节省性能。
我们可以自己在代码中运用slowpath以及fastpath来优化代码的调用,详细介绍参考:www.jianshu.com/p/536824702…
字节对齐
我们通过对可编译源码跟流程发现alloc的调用流程大致为:
static inline size_t align16(size_t x) {
return (x + size_t(15)) & ~size_t(15);
}
以上为字节对齐的代码,当传进来的值为8时(size_t(15)为15,可通过po size_t(15)得到。~表示取反):也就是(8+15) 与 15的取反,换算成二进制为:
0000 0000 0001 0111 23
&
1111 1111 1111 0000 15取反
=
0000 0000 0001 0000 16
0000 0000 0000 1111 15
可以看出这个算法最后得到的结果都会是16的倍数。 那为什么要16字节对齐呢?一个对象至少有一个isa指针,而一个isa指针的大小为8个字节,也就是说一个什么都没有的对象的大小为八字节,也就是对象最小为8字节,而我们研究最多的就是对象,如果规定其大小的时候则对其最小研究对象大小进行翻倍。而且系统在读取的时候,每次读取的大小是固定的,每次8字节,这样更快速便捷,所以8的倍数会更合理,设置为16则是为最小研究对象预留出空间,避免读取的时候读取错误。这也是一种空间换时间的策略。
由上面可以得出,alloc做了什么事情:开辟内存空间,关联类。
alloc,init和new的区分
+ (id)init {
return (id)self;
}
这是init的源码,返回了id类型的自身。那为什么前面进行了类关联,这里为什么又要强转为id类型呢,这是因为这个方法是给程序员提供的构造方法,子类可以重写该方法,也就是工厂方法。
+ (id)new {
return [callAlloc(self, false/*checkNil*/) init];
}
callAlloc是alloc的内部方法,这里说明new == [[Class alloc] init]。
不建议直接new,因为我们有可能会写带有自己参数的初始化方法。
对象开辟内存的影响因素
我们创建一个Person类,不添加任何东西,我们对源码打断点,这里注意先让断点走到我们需要调试的地方,再打开获取申请空间的断点,原理和前面一样。
可以看出其大小为16字节,因为只有一个isa指针,16字节对齐的规则,所以是16字节。
接下来我们为其添加两个字符串类型的属性。
然后我们走断点,会发现其size是32
isa占用8个字节,两个字符串类型2*8=16字节。实际大小为8+2*8=24字节,16字节对齐的原则,所以为32字节.
我们重新运行程序,打上断点
输入lldb命令x p:输出p对象的内存分布:
但是这样看着并不清晰,我们使用x/4gx p(x/5gx):x表示16进制输出,4表示4个内存单元,g表示8字节,x表示16进制:
iOS是小端模式,从右往左读:高字节在高地址,低字节在低地址。按照数字的读法,左边的数字会是高位,高字节就指的是左边的字节,低字节指的是右边的,一般来说,低地址在左边,高地址在右边,具体参考该篇文章:blog.csdn.net/ALakers/art…
第一个地址肯定是isa指针,但是目前打印出来的却不是对象的指针地址,这是因为有一个Mask,下篇文章解释。
我们对第二个地址和第三个地址进行打印:
OC对象的本质为结构体objc_object,对象中一般会存储isa指针以及成员变量的值。
关于汇编的一些知识:
b,bl 跳转,相当于函数调用
ret 返回
; 注释
寄存器 运算器 控制器
register read x0 打印返回值
register read x1 打印调用方法,需要强转为char*