001-OC对象原理探究
alloc探索
通过这篇文章可以知道什么:
- alloc方法是如何开辟内存的,开辟了多少内存?
- 在alloc过程中内存、指针有什么关系?
- alloc是如何开辟内存空间的?
- 如何探索底层源码?
- 底层源码怎么获取,例如(Objc4/)
- alloc源码的详细分析
- alloc加载流程图
- 不同模式下的编译器优化,在汇编层面上是怎样的?
- 什么是字节对齐?字节对齐的好处?
从启动流程开始搞起:
启动流程.jpg加载过程
绿色部分为程序启动部分,由_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.jpg
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)
图形详解
对象内存开辟与指向.png关键点:连续开辟,指向同一块空间
alloc是如何做到的?
init是真的什么都不做吗?
如何探索源码:
方式一:
- 真机模式
第一步:在工程的LGPerson *p1 = [LGPerson alloc];处设置断点
真机+源码探索00.jpg第二步:将工程运行,停在断点处之后,按住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
第三步:设置符号断点:
模拟器+源码探索02.jpg
第四步:再次按住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加载流程图
alloc加载流程图.png编译器优化
<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寄存器,结果如下:
编译器优化-None.jpg
-
Fastest,Smallest[-Os]
编译器优化-Fastest,Smallest.jpg
执行结果:优化掉了a、b两个变量,甚至连lgSum函数都被优化掉了,只剩下了一个结果0x1e存在w8寄存器中了。
结论:由于选择了Fastest,Smallest[-Os]优化方案,导致lgSum函数没有了,同理callAlloc
函数也是一样的。
alloc做了什么?
源码解析
源码_class_createInstanceFromZone详解.jpg
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
,这是为什么?->编译器优化部分