内存地址 & 指针地址
搭建
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字节,并指向相同的堆空间
底层探索的三种方法
日常开发中,我们只能找到
alloc的方法定义,却找不到它的方法实现+ (instancetype)alloc OBJC_SWIFT_UNAVAILABLE("use object initializers instead");所以想了解底层原理,必须从源码中进行探索
底层探索的三种方法:
- 使用
Control + Step into单步调试- 查看汇编代码
- 对已知方法设置符号断点
【方式一】
使用
Control + Step into单步调试
底层调用的
objc_alloc函数
选择
Symbolic Breakpoint...,设置符号断点
对
objc_alloc设置符号断点
点击
Continue继续执行
进入
objc_alloc函数
- 来自于
libobjc.A.dylib动态库,想了解它的底层原理,必须探索objc源码
【方式二】
查看汇编代码
在菜单中,选择
Debug→Debug Workflow→Always Show Disassembly
来到汇编代码,当
bl指令一旦执行,就会进入objc_alloc函数
使用
Control + Step into单步调试,执行两步,进入objc_alloc函数
对
objc_alloc设置符号断点,即可跟踪到方法来源
【方式三】
对已知方法设置符号断点
对象初始化依赖于
alloc方法,对已知的alloc方法设置符号断点
一旦设置成功,整个项目中,针对
alloc方法设置的断点非常多
所以,先对
[LGPerson alloc]设置断点,暂时禁用alloc断点
项目运行后,先进入
[LGPerson alloc]断点,然后启用alloc断点,点击Continue继续执行
+[NSObject alloc]方法,同样来自libobjc.A.dylib。所以我们想更深入的了解底层,对于objc源码的探索是必不可少的
下载objc源码
【方式一】
选择系统版本,例如:
11.3
在列表中,搜索
objc
【方式二】
在列表中,搜索
objc
选择
objc的源码版本
汇编结合源码探索
下载
objc4-818.2源码,打开项目搜索
alloc {关键字,打开NSObject.mm文件,找到alloc方法实现
alloc方法的执行流程:alloc→_objc_rootAlloc→callAlloc在
callAlloc方法中,出现了复杂的代码逻辑
使用汇编结合源码,定位条件分支的触发
延用
allocDemo项目将
alloc流程中找到的几个的函数,全部设置符号断点
运行项目,查看汇编代码,进入
alloc方法
进入
_objc_rootAlloc函数,由于编译器优化,不会执行callAlloc函数,直接跳转_objc_rootAllocWithZone函数
进入
_objc_rootAllocWithZone函数
编译器优化
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的情况下减小代码大小
alloc源码解析
打开
objc源码进入
alloc方法+ (id)alloc { return _objc_rootAlloc(self); }
进入
_objc_rootAlloc函数id _objc_rootAlloc(Class cls) { return callAlloc(cls, false/*checkNil*/, true/*allocWithZone*/); }
进入
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:假值判断。入参较大可能为falsefastpath:真值判断。入参较大可能为truehasCustomAWZ:类或父类具有默认的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的概率较大
进入
_objc_rootAllocWithZone函数NEVER_INLINE id _objc_rootAllocWithZone(Class cls, malloc_zone_t *zone __unused) { return _class_createInstanceFromZone(cls, 0, nil, OBJECT_CONSTRUCT_CALL_BADALLOC); }
进入
_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进行关联
alloc流程图
llvm优化alloc
调用
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完成
打开
llvm源码,搜索objc_alloc关键字
- 在注释中找到线索,
alloc关联objc_alloc,allocWithZone:nil关联objc_allocWithZone- 代码进行了版本控制,哪些系统和版本有此关联
进入
GeneratePossiblySpecializedMessageSend函数
- 判断如果为特殊消息,调用
tryGenerateSpecializedMessageSend函数,否则调用GenerateMessageSend函数- 特殊消息,例如:
alloc方法
进入
tryGenerateSpecializedMessageSend函数
- 在
OMF_alloc条件中,如果方法编号为alloc,调用EmitObjCAlloc函数并返回
进入
EmitObjCAlloc函数
- 将
alloc方法编号,修改为objc_alloc的函数地址
苹果对特殊方法,自身会进行
HOOK。例如alloc方法,优先进入objc_alloc流程,执行完毕后,对当前对象发送alloc消息,然后进入alloc流程
打开
objc源码在对象的
alloc方法上设置断点
首先进入
objc_alloc函数id objc_alloc(Class cls) { return callAlloc(cls, true/*checkNil*/, false/*allocWithZone*/); }来到
callAlloc函数,发送alloc消息
进入
alloc流程:alloc→_objc_rootAlloc→callAlloc再次进入
callAlloc函数,调用_objc_rootAllocWithZone函数,继续alloc流程
[LGPerson alloc]流程图
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本质是构造方法,通过工厂设计模式,给用户提供入口以便重写和定制
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方法而言扩展性更好,使用更灵活。并且在原则上,显示调用比隐式调用更清晰
NSObject初始化流程
NSObject与自定义类的初始化流程有一些区别
alloc流程当
NSObject调用alloc方法,首先进入objc_alloc流程进入
callAlloc函数,不触发objc_msgSend,直接调用_objc_rootAllocWithZone函数
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_createInstanceFromZonealloc核心方法
◦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,直接进入各自初始化流程












































