alloc探索
通过这篇文章可以知道什么:
- alloc方法是如何开辟内存的,开辟了多少内存?
- 在alloc过程中内存、指针有什么关系?
- alloc是如何开辟内存空间的?
- 如何探索底层源码?
- 底层源码怎么获取,例如(Objc4/)
- alloc源码的详细分析
- alloc加载流程图
- 不同模式下的编译器优化,在汇编层面上是怎样的?
- 什么是字节对齐?字节对齐的好处?
从启动流程开始搞起:

加载过程
绿色部分为程序启动部分,由_dyld_start(dyld开始加载)开始到dyld::main再到dyld_initialzeMainExecutable、ImageLoader::*等等,代表着主程序由_dyld_start开始,到main等为启动做准备,包括加载动态库,共享内存,全局C++函数的析构,还有一系列的初始化,注册回调函数都在此步骤内完成。这里并不是此篇文章的详细说明,只做引入功能。
红色部分为对象加载过程的开始,通过App启动一系列函数之后会进入到libSystem_initializer -> libdispatch_init -> GCD环境的准备 -> _objc_init
OC对象的初始化
1、oc对象是如何开辟的?
2、alloc、init、new是如何操作的?
3、在此过程中内存、指针有什么关系?

p1与p2的打印结果为什么一样?
LGPerson *p1 = [LGPerson alloc];
得出结论:
- p1此刻拥有了内存
- p1拥有了指针的指向
LGPerson *p2 = [p1 init];
LGPerson *p3 = [p1 init];
由于打印对象p2=p3,得出结论:
- p2、p3所指向的内存地址是一样的
- init未对指针进行任何操作

通过alloc之后开辟了一块内存空间,*p1 *p2 *p3代表3个指针地址,并且同时指向了同一块内存空间,由上图内存地址0x7ffeede340a8 0x7ffeede340a0 0x7ffeede34098得出结论:
-
*p1、*p2、*p3属于栈上内存地址 -
*p1、*p2、*p3是连续的地址空间,每个相隔8字节(解释:0x98+0x8=0xa0、0xa0+0x8=0xa8)
图形详解
关键点:连续开辟,指向同一块空间

alloc是如何做到的?
init是真的什么都不做吗?
如何探索源码:
方式一:
- 真机模式
第一步:在工程的LGPerson *p1 = [LGPerson alloc];处设置断点

第二步:将工程运行,停在断点处之后,按住control + Step into进入到汇编代码


这里发现了objc_alloc方法,看到了熟悉的代码,变得很兴奋,再次按住control + Step into

结果是无法再看到有效的信息了,原因是真机模式下Apple做了限制
- 模拟器模式
第一步:在工程的LGPerson *p1 = [LGPerson alloc];处设置断点,
第二步:将工程运行,停在断点处之后,按住control + Step into进入到汇编代码


第三步:将看到的objc_alloc添加符号断点,具体步骤如下:

第四步:继续按住control + Step into向下走

这里看到了libobjc.A.dylib objc_alloc,看到了接下来会调用的方法_objc_rootAllocWithZone,objc_msgSend,这里豁然开朗,终于找到了objc_alloc底层的源码,来自于哪个动态库,为向下探索提供了更多的线索!
方式二:
通过汇编流程的方式去查看:
第一步,设置工程的模式,选择菜单栏Debug->Debug wrokflow->Always Show Disassembly,将工程运行


第二步:此时断点断在了LGPerson处,按住control + Step into,去找到objc_alloc

第三步:设置符号断点:

第四步:再次按住control + Step into调试objc_alloc

这里看到了libobjc.A.dylib objc_alloc,看到了接下来会调用的方法_objc_rootAllocWithZone,objc_msgSend,这里豁然开朗,终于找到了objc_alloc底层的源码,来自于哪个动态库,为向下探索提供了更多的线索!
第三种:
直接通过已知符号断点设定,直接进入,通常配合第二种使用
底层源码在哪里?
Apple开元源码汇总:https://opensource.apple.com/

[Source Browser:https://opensource.apple.com/tarballs/]

我这里查看的源码是objc4-818.2.tar.gz,来自于LGCocci老师,那个最靓的男人:https://github.com/LGCooci/objc4_debug,有需求的伙伴可以自行获取,素质三连

alloc源码分析:
首先打开源码项目objc4-818.2,搜索alloc,查看一下alloc源码执行的详细流程:

1、进入_objc_rootAlloc方法

2、进入callAlloc方法

3、这里有#if __OBJC2__判断,如何验证走哪个方法进入_objc_rootAllocWithZone

4、进入_class_createInstanceFromZone方法

alloc加载流程图

编译器优化
<span id="callalloc">进入到BuildSetting下,找到Optimization level(GCC_OPTIMIZATION_LEVEL),意思是指定生成的代码针对速度和二进制大小进行优化的程度</span>
| 设置 | 参数 |
|---|---|
| None[-O0] | 编译器不会优化代码。编译器的目标是蒋迪编译成本并使调试产生预期的结果,通常在Debug模式下使用。 |
| Fast[-O,O1] | 快速,优化编译器需要编译的时间更久,对大型函数需要更多的内存。编译器会尝试减少代码大小和执行时间,而不执行任何需要大量编译时间的优化。 |
| Faster[-O2] | 更快速,编译器执行几乎所有不涉及空间速度权衡的受支持优化。使用此设置,编译器不会执行循环展开或函数内联或寄存器命名,次设置会增加编译时间和生成代码的性能。 |
| Fastest[-O3] | 设置指定的所有优化,并打开函数内联和寄存器重命名选项,此设置可能会产更大的二进制文件 |
| Fastest,Smallest[-Os] | 最快、最小,此设置启用所有通常不会增加代码大小的更快的优化,它还会做减少代码大小的进一步优化 |
尝试写一个小例子,设置不同的优化方案,用来验证编译器优化情况:
#import <UIKit/UIKit.h>
#import "AppDelegate.h"
//MARK: - 测试函数
int lgSum(int a, int b){
return a+b;
}
int main(int argc, char * argv[]) {
int a = 10;
int b = 20;
int c = lgSum(a, b);
NSLog(@"查看编译器优化情况:%d",c);
return 0;
}
-
None[-O0]
编译器优化.jpg
执行结果:不优化的情况下所有信息在寄存器中显示完整,我分别打印了a、b、计算钱与计算后的x0寄存器,结果如下:

-
Fastest,Smallest[-Os]
编译器优化-Fastest,Smallest.jpg
执行结果:优化掉了a、b两个变量,甚至连lgSum函数都被优化掉了,只剩下了一个结果0x1e存在w8寄存器中了。
结论:由于选择了Fastest,Smallest[-Os]优化方案,导致lgSum函数没有了,同理callAlloc函数也是一样的。
alloc做了什么?
源码解析

alloc内存是如何开辟的,开辟了多少内存
开辟内存是由instanceSize这个函数决定的,进入到这个函数,首先判断是否有缓存,如果有执行cache.fastInstanceSize函数直接返回,内存开辟结束,获得该对象内存大小。如果没有缓存,会执行alignedInstanceSize函数,执行word_align函数,此函数的参数是函数unalignedInstanceSize,而这个函数通过data()->ro()->instanceSize获取到对象的实例大小,也就是说,最终开辟内存空间的大小是根据对象的成员变量大小决定的。
默认情况下,不创建任何成员变量,类开辟的内存空间是8字节,因为继承NSObject造成的,NSObject内有成员变量isa,由于isa的类型是结构体指针,所以isa是8字节,所以创建一个新的对象,没有任何成员变量,默认内存大小是8字节
//对象
struct class_ro_t {
uint32_t flags;
uint32_t instanceStart;
uint32_t instanceSize;
#ifdef __LP64__
uint32_t reserved;
#endif
//_class_createInstanceFromZone内开辟内存的大小
size = cls->instanceSize(extraBytes);
// May be unaligned depending on class's ivars.
uint32_t unalignedInstanceStart() const {
ASSERT(isRealized());
return data()->ro()->instanceStart;
}
// Class's instance start rounded up to a pointer-size boundary.
// This is used for ARC layout bitmaps.
uint32_t alignedInstanceStart() const {
return word_align(unalignedInstanceStart());
}
// 可能是不对齐的,取决于类的成员变量(ivars)
uint32_t unalignedInstanceSize() const {
ASSERT(isRealized());
return data()->ro()->instanceSize;
}
// 类的 ivar 大小向上舍入到指针大小边界。
uint32_t alignedInstanceSize() const {
return word_align(unalignedInstanceSize());
}
inline size_t instanceSize(size_t extraBytes) const {
if (fastpath(cache.hasFastInstanceSize(extraBytes))) {
return cache.fastInstanceSize(extraBytes);
}
size_t size = alignedInstanceSize() + extraBytes;
// CF requires all objects be at least 16 bytes.
if (size < 16) size = 16;
return size;
}
字节对齐
字节对齐的优势:以空间换取时间
- 8字节来自于NSObject对象的isa结构体指针
- 不满16等于16
- 如果大于16会根据对象在内存分布中的特性来决定(根据传入的x,取x的整数倍),如果传入8,最后得到的是8的倍数
#ifdef __LP64__
# define WORD_SHIFT 3UL
# define WORD_MASK 7UL
# define WORD_BITS 64
#else
# define WORD_SHIFT 2UL
# define WORD_MASK 3UL
# define WORD_BITS 32
#endif
//字节对齐算法
//define WORD_MASK = 7
static inline uint32_t word_align(uint32_t x) {
return (x + WORD_MASK) & ~WORD_MASK;
}
工程调试
1、验证代码是否执行#if __OBJC2__判断内函数
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
// 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));
}
方案:
将_objc_rootAlloc、callAlloc、_objc_rootAllocWithZone等方法添加符号断点,并且将项目运行起来

按照想象如期的停在了_objc_rootAlloc方法处,通过register read读取寄存器,但是问题是并没有发现LGPerson这个class,原因是LGPerson还没有初始化,解决方法先将断点放过去,让系统的方法执行完,等执行到LGPerson时候再调试

执行的结果是_objc_rootAllocWithZone先会被执行,然后再执行objc_msgSend,这也就证明了#if __OBJC2__判断为true,执行了内部的代码。
但是细心你会发现,当前正在被执行的这个函数是_objc_rootAlloc,并不是源码中的callAlloc,这是为什么?
问题:
当前简书页面内跳转失效了上文中两个对应关系如下:
- 如何验证走哪个方法 -> 工程调试部分
- 并不是源码中的
callAlloc,这是为什么?->编译器优化部分

