1. 内存地址 & 指针地址
搭建allocDemo
项目
打印对象的内存地址和指针地址
- (void)viewDidLoad {
[super viewDidLoad];
LGPerson *p1 = [LGPerson alloc];
LGPerson *p2 = [p1 init];
LGPerson *p3 = [p1 init];
NSLog(@"对象:%@,内存:%p,指针:%p",p1,p1,&p1);
NSLog(@"对象:%@,内存:%p,指针:%p",p2,p2,&p2);
NSLog(@"对象:%@,内存:%p,指针:%p",p3,p3,&p3);
}
-------------------------
//输出结果:
对象:<LGPerson: 0x282a28700>,内存:0x282a28700,指针:0x16b375b38
对象:<LGPerson: 0x282a28700>,内存:0x282a28700,指针:0x16b375b30
对象:<LGPerson: 0x282a28700>,内存:0x282a28700,指针:0x16b375b28
上述案例中,三个对象的内存地址相同,但指针地址不同
在alloc
方法中,为对象在堆区开辟内存空间,并返回指针地址。而init
方法中,并没有对内存做任何处理,所以三个对象的内存地址相同
但它们的指针地址不同,因为指针地址在栈区,以连续的内存地址存储,相隔8字节
,并指向相同的堆空间
2. 底层探索的三种方法
日常开发中,我们只能找到alloc
的方法定义,却找不到它的方法实现
+ (instancetype)alloc OBJC_SWIFT_UNAVAILABLE("use object initializers instead");
所以想了解底层原理,必须从源码中进行探索
底层探索的三种方法:
- 使用
Control + Step into
单步调试- 查看汇编代码
- 对已知方法设置符号断点
2.1 使用Control + Step into
单步调试
进入断点,使用Control + Step into
进行单步调试
来到汇编代码,底层调用的objc_alloc
函数
选择Symbolic Breakpoint...
,设置符号断点
对objc_alloc
设置符号断点
点击Continue
继续执行
进入objc_alloc
函数
objc_alloc
函数,来自于libobjc.A.dylib
动态库,想了解它的底层原理,必须探索objc
源码
2.2 查看汇编代码
在菜单中,选择Debug
→Debug Workflow
→Always Show Disassembly
来到汇编代码,当bl
指令一旦执行,就会进入objc_alloc
函数
使用Control + Step into
单步调试,执行两步,进入objc_alloc
函数
对objc_alloc
设置符号断点,即可跟踪到方法来源
2.3 对已知方法设置符号断点
对象初始化依赖于alloc
方法,对已知的alloc
方法设置符号断点
一旦设置成功,整个项目中,针对alloc
方法设置的断点非常多
所以,先对[LGPerson alloc]
设置断点,暂时禁用alloc
断点
项目运行后,先进入[LGPerson alloc]
断点,然后启用alloc
断点,点击Continue
继续执行
+[NSObject alloc]
方法,同样来自libobjc.A.dylib
。所以我们想更深入的了解底层,对于objc
源码的探索是必不可少的
3. 下载objc源码
3.1 Apple Open Source
选择系统版本,例如:11.3
在列表中,搜索objc
3.2 Source Browser
在列表中,搜索objc
选择objc
的源码版本
4. 汇编结合源码探索
4.1 源码探索
下载objc4-818.2
源码,打开项目
搜索alloc {
关键字,打开NSObject.mm
文件,找到alloc
方法实现
alloc
方法的执行流程:alloc
→_objc_rootAlloc
→callAlloc
在callAlloc
方法中,出现了复杂的代码逻辑
使用汇编结合源码,定位条件分支的触发
4.2 汇编结合
延用allocDemo
项目
将alloc
流程中找到的几个的函数,全部设置符号断点
运行项目,查看汇编代码,进入alloc
方法
进入_objc_rootAlloc
函数,由于编译器优化,不会执行callAlloc
函数,直接跳转_objc_rootAllocWithZone
函数
进入_objc_rootAllocWithZone
函数
5. 编译器优化
Code Generation Options
在Build Setting
中,设置Optimization Level
(编译器的优化程度)
None [-O0]
:不优化Fast [-O1]
:大函数所需的编译时间和内存消耗都会稍微增加Faster [-O2]
:编译器执行所有不涉及时间空间交换的所有的支持的优化选项Fastest [-O3]
:在开启Fast [-O1]
项支持的所有优化项的同时,开启函数内联和寄存器重命名选项Fastest, Smallest [-Os]
:在不显着增加代码大小的情况下尽量提供高性能Fastest, Aggressive Optimizations [-Ofast]
:与Fastest, Smallest [-Os]
相比该级别还执行其他更激进的优化Smallest, Aggressive Size Optimizations [-Oz]
:不使用LTO
的情况下减小代码大小
6. alloc源码解析
6.1 探索objc
源码
6.1.1 alloc
方法
+ (id)alloc {
return _objc_rootAlloc(self);
}
6.1.2 _objc_rootAlloc
函数
id
_objc_rootAlloc(Class cls)
{
return callAlloc(cls, false/*checkNil*/, true/*allocWithZone*/);
}
6.1.3 callAlloc
函数
static ALWAYS_INLINE id
callAlloc(Class cls, bool checkNil, bool allocWithZone=false)
{
#if __OBJC2__
if (slowpath(checkNil && !cls)) return nil;
if (fastpath(!cls->ISA()->hasCustomAWZ())) {
return _objc_rootAllocWithZone(cls, nil);
}
#endif
if (allocWithZone) {
return ((id(*)(id, SEL, struct _NSZone *))objc_msgSend)(cls, @selector(allocWithZone:), nil);
}
return ((id(*)(id, SEL))objc_msgSend)(cls, @selector(alloc));
}
#if __OBJC2__
:编译器优化。如果给定条件为真,则编译下面代码slowpath
:假值判断。入参较大可能为false
fastpath
:真值判断。入参较大可能为true
hasCustomAWZ
:类或父类具有默认的alloc/allocWithZone:
实现
fastpath
和slowpath
的定义
#define fastpath(x) (__builtin_expect(bool(x), 1))
#define slowpath(x) (__builtin_expect(bool(x), 0))
__builtin_expect
,由GCC
引入。向编译器提供分支预测信息,从而帮助编译器进行代码优化__builtin_expect(EXP, N)
,表示EXP
等于N
的概率较大
6.1.4 _objc_rootAllocWithZone
函数
NEVER_INLINE
id
_objc_rootAllocWithZone(Class cls, malloc_zone_t *zone __unused)
{
return _class_createInstanceFromZone(cls, 0, nil,
OBJECT_CONSTRUCT_CALL_BADALLOC);
}
6.1.5 _class_createInstanceFromZone
函数
static ALWAYS_INLINE id
_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());
bool hasCxxCtor = cxxConstruct && cls->hasCxxCtor();
bool hasCxxDtor = cls->hasCxxDtor();
bool fast = cls->canAllocNonpointer();
size_t size;
size = cls->instanceSize(extraBytes);
if (outAllocatedSize) *outAllocatedSize = size;
id obj;
if (zone) {
obj = (id)malloc_zone_calloc((malloc_zone_t *)zone, 1, size);
} else {
obj = (id)calloc(1, size);
}
if (slowpath(!obj)) {
if (construct_flags & OBJECT_CONSTRUCT_CALL_BADALLOC) {
return _objc_callBadAllocHandler(cls);
}
return nil;
}
if (!zone && fast) {
obj->initInstanceIsa(cls, hasCxxDtor);
} else {
obj->initIsa(cls);
}
if (fastpath(!hasCxxCtor)) {
return obj;
}
construct_flags |= OBJECT_CONSTRUCT_FREE_ONFAILURE;
return object_cxxConstructFromClass(obj, cls, construct_flags);
}
instanceSize
:计算内存大小calloc
:开辟内存空间initInstanceIsa
:将class
与isa
进行关联
6.2 alloc
流程图
7. llvm优化alloc
调用alloc
方法,入口函数却是objc_alloc
,它们是在什么时候关联起来的?
7.1 alloc
和objc_alloc
关联时机
在objc
源码中,_read_images
函数在dyld
之后被调用
在_read_images
函数中,调用fixupMessageRef
函数
进入fixupMessageRef
函数
关键代码:如果方法编号为alloc
,修改为objc_alloc
的函数地址
fixupMessageRef
函数的作用,修复旧版本的方法调度表。难道在新版本中,alloc
方法理应关联objc_alloc
的函数地址吗?
fixupMessageRef
函数被_read_images
调用,而_read_images
在objc
源码中,执行时机已经非常优先
所以,可关联
alloc
和objc_alloc
,只剩下两个时机:
- 在
dyld
中进行关联- 在编译时期已经关联
查看MachO
文件,在符号表中搜索alloc
结论:在编译时期,MachO
中已经生成_objc_alloc
符号。可以确定alloc
和objc_alloc
的关联,是在编译时期由llvm
完成
7.2 探索llvm
源码
7.2.1 搜索objc_alloc
关键字
- 在注释中找到线索,
alloc
关联objc_alloc
,allocWithZone:nil
关联objc_allocWithZone
- 代码进行了版本控制,哪些系统和版本有此关联
7.2.2 GeneratePossiblySpecializedMessageSend
函数
- 判断如果为特殊消息,调用
tryGenerateSpecializedMessageSend
函数,否则调用GenerateMessageSend
函数 - 特殊消息,例如:
alloc
方法
7.2.3 tryGenerateSpecializedMessageSend
函数
- 在
OMF_alloc
条件中,如果方法编号为alloc
,调用EmitObjCAlloc
函数并返回
7.2.4 EmitObjCAlloc
函数
- 将
alloc
方法编号,修改为objc_alloc
的函数地址
结论:苹果对特殊方法,自身会进行HOOK
。例如:alloc
方法,优先进入objc_alloc
流程,执行完毕后,对当前对象发送alloc
消息,然后进入alloc
流程
7.3 探索objc
源码
在对象的alloc
方法上设置断点
7.3.1 objc_alloc
函数
id
objc_alloc(Class cls)
{
return callAlloc(cls, true/*checkNil*/, false/*allocWithZone*/);
}
7.3.2 callAlloc
函数(首次进入)
发送alloc
消息
- 进入
alloc
流程:alloc
→_objc_rootAlloc
→callAlloc
7.3.3 callAlloc
函数(再次进入)
调用_objc_rootAllocWithZone
函数,继续alloc
流程
7.4 [LGPerson alloc]
流程图
8. init源码解析
在对象的alloc + init
方法上设置断点
调用alloc + init
方法,入口函数为objc_alloc_init
打开objc
源码
进入objc_alloc_init
函数
id
objc_alloc_init(Class cls) {
return [callAlloc(cls, true/*checkNil*/, false/*allocWithZone*/) init];
}
- 在
callAlloc
函数执行完毕后,调用对象的init
方法
进入init
方法
- (id)init {
return _objc_rootInit(self);
}
进入_objc_rootInit
函数
id
_objc_rootInit(id obj) {
return obj;
}
结论:init
方法只做了一件事,将传入的self
对象返回。init
本质是构造方法,通过工厂设计模式,给用户提供入口以便重写和定制
9. new源码解析
对象初始化的另一种方式,new
方法
调用new
方法,入口函数为objc_opt_new
打开objc
源码
进入objc_opt_new
函数
id
objc_opt_new(Class cls)
{
#if __OBJC2__
if (fastpath(cls && !cls->ISA()->hasCustomCore())) {
return [callAlloc(cls, false/*checkNil*/) init];
}
#endif
return ((id(*)(id, SEL))objc_msgSend)(cls, @selector(new));
}
hasCustomCore
:类或父类具有默认的new/self/class/respondsToSelector/isKindOfClass
- 符合条件,直接调用
alloc + init
方法。否则进行消息发送
触发消息发送流程,调用new
方法,最终调用的还是alloc + init
方法
+ (id)new {
return [callAlloc(self, false/*checkNil*/) init];
}
结论:new
方法等价于alloc + init
方法。但我更推荐alloc + init
方式,因为在开发中,我们会定义initWithXXX
等方法,new
方法将初始化固定为init
,所以alloc + init
相比new
方法而言扩展性更好,使用更灵活。并且在原则上,显示调用比隐式调用更清晰
10. NSObject初始化流程
NSObject
与自定义类,它们的初始化流程会有一些区别。
10.1 alloc
当NSObject
调用alloc
方法,首先进入objc_alloc
流程
进入callAlloc
函数,不触发objc_msgSend
,直接调用_objc_rootAllocWithZone
函数
10.2 new
NSObject
调用new
方法,进入objc_opt_new
流程,不触发objc_msgSend
,直接调用alloc + init
方法
总结
内存地址 & 指针地址
- 不同指针地址指向相同堆空间
alloc
方法,为对象开辟内存空间,并返回指针地址init
方法,并没有对内存做任何处理
底层探索的三种方法
- 使用
Control + Step into
单步调试 - 查看汇编代码
- 对已知方法设置符号断点
下载objc
源码
汇编结合源码探索
alloc
方法的执行流程:alloc
→_objc_rootAlloc
→callAlloc
- 由于编译器优化,没有触发
callAlloc
方法的断点
编译器优化
- 在
Build Setting
中,设置Optimization Level
,编译器的优化程度 - 七个不同程度的优化等级供开发者选择,
Debug
模式默认为None [-O0]
不优化
alloc
源码解析
alloc
→_objc_rootAlloc
→callAlloc
→_objc_rootAllocWithZone
→_class_createInstanceFromZone
alloc
核心方法
◦ instanceSize
:计算内存大小
◦ calloc
:开辟内存空间
◦ initInstanceIsa
:将class
与isa
进行关联
llvm
优化alloc
alloc
方法,优先进入objc_alloc
流程,执行完毕后,对当前对象发送alloc
消息,然后进入alloc
流程- 自定义对象,
callAlloc
函数会执行两遍
◦ 对当前对象发送alloc
消息
◦ 调用_objc_rootAllocWithZone
函数
- 通过源码分析和
MachO
中的_objc_alloc
符号,可以确定alloc
和objc_alloc
的关联,是在编译时期由llvm
完成 llvm
源码中,如果是特殊消息,例如:alloc
方法,调用tryGenerateSpecializedMessageSend
函数,否则调用GenerateMessageSend
函数- 在
OMF_alloc
条件中,如果方法编号为alloc
,修改为objc_alloc
的函数地址
init
源码解析
init
方法本质是构造方法- 用于将传入的
self
对象返回 - 通过工厂设计模式,给用户提供入口以便重写和定制
new
源码解析
new
方法等价于alloc + init
方法- 更推荐
alloc + init
方式,扩展性更好,使用更灵活,显示调用比隐式调用更清晰
NSObject
初始化流程
NSObject
与自定义类的初始化流程有一些区别NSObject
调用alloc
和new
方法,都不触发objc_msgSend
,直接进入各自初始化流程