背景
iOS开发,我们每天都在 alloc
对象,但是我们jump to Definition
到NSObject底层能看到的仅有个未实现的alloc
方法,而我们对alloc
底层的逻辑却一无所知。
alloc
底层有什么、
alloc
做了什么、
init
做了什么、
alloc
、init
和new
到底有什么关系和区别呢?
掘金地址同步:掘金-OC对象原理(上)
实例
简单的代码和打印(分别输出对象的内容,对象的地址,以及对象指针的地址)示例:
ZMPerson *p1 = [ZMPerson alloc];
ZMPerson *p2 = [p1 init];
ZMPerson *p3 = [p1 init];
ZMPerson *newP = [ZMPerson alloc];
NSLog(@"%@ - %p - %p",p1,p1,&p1);
NSLog(@"%@ - %p - %p",p2,p2,&p2);
NSLog(@"%@ - %p - %p",p3,p3,&p3);
NSLog(@"%@ - %p - %p",newP,newP,&newP);
<ZMPerson: 0x6000030f40e0> - 0x6000030f40e0 - 0x7ffeed054068
<ZMPerson: 0x6000030f40e0> - 0x6000030f40e0 - 0x7ffeed054060
<ZMPerson: 0x6000030f40e0> - 0x6000030f40e0 - 0x7ffeed054058
<ZMPerson: 0x6000030f40f0> - 0x6000030f40f0 - 0x7ffeed054050
从打印结果我们可以看到:
- p1、p2、p3的对象地址是一模一样,但指针地址不一样并且是从高位到低位连续的间隔8字节。
- newP和p1、p2、p3的对象地址,指针的地址是不一样的。
为什么呢:
alloc
具备在堆中开辟内存的功能init
不具备在堆中开辟内存的功能,但可以在栈中生成指向堆内存的指针地址
栈区
开辟的内存是高地址
到低地址
,堆区
则是低地址
到高地址
如图:
alloc探索
准备工作
- 下载objc4-818.2(版本号根据自己macOS版本变化)
- 编译源码
alloc流程图
四种探索方式
1. 断点模式 & control
+step into
需要调试的alloc
位置打上断点,编译走到断点的时候control
+step into
进入下一步的汇编里面,如图:
然后找到方法objc_alloc,并给这个方法添符号断点。如图:
然后继续编译,进到libobjc.A.dylib`objc_alloc
,在这里我们看到objc_alloc里面有_objc_rootAllocWithZone
和objc_msgSend
2. 汇编(比较给手的方法,👏)
同样,在需要调试的alloc
位置打上断点,然后Xcode->Debug->Debug Workflow -> Always Show Disassembly
勾选Always Show Disassembly
然后找到方法objc_alloc
直接添加符号断点调试,或者断住要跳转的地方,control+step into一步一步一直走
3. 符号断点,断位置
直接给要调试的方法添加符号断点,比如下图给我们探索的alloc添加符号断点,不过有个注意点,这个符号断点需要在 断住的方法 之后添加并激活
,因为alloc
项目运行中可能很多地方调用,那数据量,你说你不想知道结果,但那是考验机器性能和你识别度的。如图:
然后找到我们需要的alloc:
4. 编译源码LLDB调试
objc_alloc
是属于libobjc
库的方法,在alloc探索的准备模块里
,我们准备好了objc源码,然后源码编译成工程run起来,这种就比较beautiful的方法
根据编译源码工程,我们点击进入alloc
,更直观一目了然的看到alloc的执行过程,流程图如下:
进入alloc
:
然后到_objc_rootAlloc
:
然后到callAlloc
:
- 重磅提示 这里是核心方法
- slowpath(x):x很可能为假,为真的概率很小
- fastpath(x):x很可能为真
- 其实将fastpath和slowpath去掉完全不影响任何功能,写上是告诉编译器对代码进行优化
然后断点到_objc_rootAllocWithZone
:
问题来了(这两个问题将在下面的 alloc init & new
去解答):
- 问题1,我们通过上面汇编发现是进到
objc_alloc
,为什么通过源码LLDB却走了_objc_rootAlloc
? - 问题2,我们打印
alloc
时在走到_objc_rootAlloc
时,线程里面有了objc_alloc
->callAlloc
,然后走到NSObject的alloc -> _objc_rootAlloc -> callAlloc-> _objc_rootAllocWithZone及之后的流程
,为什么?
然后断点进入核心方法_class_createInstanceFromZone
:
cls->instanceSize
计算需要开辟的内存大小(id)calloc(1, size)
向内存申请开辟空间,并返回地址指针obj->initInstanceIsa
初始化的isa指针和cls关联
核心方法-instanceSize
进入cache.fastInstanceSize
断点进入align16
- 16字节对齐算法 公式:(x + 15)& ~15
比如我们以align(9)为例:
总结:align16
算法实际上就是取16的整数倍。对于变量x来说是向上取整,比如30,向上取整为32(16<30 但是 16*2>30),算法的角度有人认为是向下取整:(x + 15)
是16
的几倍
,超过的部分抹去。例如 (20 + 15) = 35 = 16 * 2 + 3
,结果是32
。这种算法和 >> 4 << 4
是一样的,得出的结果就是16
的倍数,不足16
的全部抹去。
为什么对象需要16
字节对齐
cpu
的性能和读取速度cpu
读取数据是一个用空间换取时间的做法,以固定字节块来读取的,如果频繁的读取字节未对齐的数据,降低了cpu
的性能和读取速度。- 更安全 由于在一个对象中
isa
指针是占8
个字节,如果不进行节对齐 ,对象之间就会紧挨着,容易造成访问混乱。16
字节对齐,会预留部分空间,访问更安全
核心方法-calloc
- 开辟内存,返回地址指针
由instanceSize
方法 计算出需要的内存大小,然后向内存申请size
大小的内存,返回给objc
,因此objc
是指向内存地址的指针
执行calloc
后打印的是一个16进制
的指针地址,说明已经开辟了内存
核心方法-initInstanceIsa
方法内部isa结构后续探索待补充
执行完initInstanceIsa后
图示打印结果说明:指针已经与类进行了关联
init探索
断点进入_objc_rootInit
可以看到init
返回的是对象本身,可以提供给开发者更多的自由去自定义重写init ,通过id
实现强转,返回我们需要的类型
new探索
new
的流程走了callAlloc
的方法流程,然后走了init
方法 ,所以 new
看做是alloc
+ init
alloc init & new
Calls [[cls alloc] init]
object_alloc问题及解答:
- 如果直接LLDB源码调试NSObject*obj = [[NSObject alloc] init];能走进底层
alloc
么?为什么?
如图:
经过源码一步一步探索我们发现并不能走到底层alloc
代码但走了obj_alloc
,但为什么呢?设置符号断点继续跟源码:
通过跟源码,我们发现子类进入这里经过了俩次,从alloc
中进入,又进了object_alloc
进行发送系统消息,然后又出来重新进入alloc
方法,而NSObject却不是,他没有经过给系统发送消息的一步,直接开辟内存,所以我们可以猜测,它是在系统编译之后就已经执行了,跟程序员无关,那么问题又来了,为什么子类却要经过俩次呢?
解答:通过_read_images找到fixupMessageRef函数:
答案就出来了,在read_images做了一次fixup的修复,😂
然后我们通过LLVM源码:搜索objc_alloc
通过对LLVM探索:
- alloc在系统被系统拦截 -> objc_alloc -> 标记receiver 判断进不去 这个时候objc_alloc-> objc_msgSend -> 然后重新走alloc方法这个时候receiver已经有了标记最后就正常走alloc流程(alloc-> _objc_rootAlloc -> …)
如图:
Calls [cls new]
iOS开发中, alloc init <=> new
终章
alloc
的核心作用就是开辟内存,伴随初始化了isa
,通过isa
指针与类进行关联,init
方法 工厂设计,子类可以自定义重写,提供开发者更多的自由,new
是对(alloc+init)
进行了封装,无法在初始化的时候添加其它的需求
进阶
instanceSize & mallocSize
对象占用8字节对齐,系统分配内存16字节对齐为什么?- 编译器优化设置路径
Xcode -> target -> build Setting -> optimization -> Fast smallest
- 编译器优化的作用:
编译时间,链接时间,运行时间,空闲时间
- isa怎么初始化的?怎么计算内存的?做了什么?对象的本质是什么?请看
iOS底层-OC对象原理(下)