大拿之路,任重道远,永不言弃!!!
一.学习线索
我们平时的开发中都会认为main函数
是应用程序的入口,是否是这样的?我们在main函数
的入口添加一个断点。bt
查看运行的堆栈信息,发现一些端倪!
发现在main函数
之前还有一个start
,并且这个start
来自动态库libdyld.dylid
。说明main函数
并不是应用程序的入口,在此之前还有一些应用程序启动过程。
很陌生,我们先从简单的开始,在这里添加一个符号断点alloc
,过掉main函数
入口的断点,bt查看堆栈信息
,可以发现更多程序加载内容。
比如UIKitCore
、CoreFoundation
、libdispach.dylib
等等,我们开发过程中最常用的比如UI
、NSObject
、GCD
等内容,均是来自这些库。
先从最熟悉的创建对象开始!!!
二.alloc原理初探
开发过程中创建对象是我们最常写的代码。GFPerson * gf = [[GFPerson alloc] init];
留下一个问题,这里alloc
做了什么,init
又做了什么呢?
1.探索案例
引入一个案例,使用alloc方法
初始化一个对象gf1
,通过gf1
调用init方法
创建了gf2
和gf3
,然后分别输出对象、对象地址、指针地址。
根据运行结果,发现三者的对象输出是一样的,对象地址也是一样的,指针地址不一样。
案例总结:
- 三个指针指向了同一个对象,这个对象是
<GFPerson: 0x600000fbc1f0>
; - 三个指针的地址是
连续的存储在栈区
中,并且从高位向低位开辟内存空间
;
指针
占用空间是8个字节
;lg1
通过alloc方法
开辟了内存空间,创建了对象;通过init方法
初始化的lg2
和lg3
并没有开辟内存空间,也就是没有创建对象。
那么,alloc是如何开辟内存空间创建对象的呢?init又起到什么作用呢?
2.底层探究方法
在我们的开发工程中,Jump to Definition
只能看到alloc方法
的声明,不能看到真正的源码实现流程。
+ (instancetype)alloc OBJC_SWIFT_UNAVAILABLE("use object initializers instead");
下面提供三种底层探究的方法。
1. 添加符号断点的形式直接跟流程
我们要研究alloc方法
,那就添加个符号断点alloc
。符号断点添加方式见下图:
因为我们暂时只研究GFPerson类
的初始化流程,所以避免其他类调用alloc
导致的干扰,运行时先将alloc断点设置为Disable Breakpoint
。在程序运行到GFPerson * gf1 = [GFPerson alloc];
时,再将alloc断点
设置为Enable Breakpoint
。运行结果见下图:
根据上图的内容发现,[GFPerson alloc]
最终走到了+[NSObject alloc]
;为什么是NSObject
呢?因为LGPerson
继承自NSObject
,子类中没有alloc方法
,最终会调用父类的alloc方法
。
并且我们还发现了一个重要线索,[NSObject alloc]来自于libobjc.A.dylib动态库
。
2. 通过按住control - step into
在GFPerson * gf1 = [GFPerson alloc];
这行代码添加断点,运行程序,运行到断点后,点击control - step into
。操作流程见下图:
进入后发现调用了objc_alloc
,接着采用方式1添加符号断点objc_alloc
,运行程序。
同样我们也发现了一个重要线索,objc_alloc也来自于libobjc.A.dylib动态库
。
3. 汇编查看跟流程
设置方式:Debug -> Debug Workflow -> Always Show Disassembly
。
在GFPerson * gf1 = [GFPerson alloc];
这行代码添加断点,运行程序。进入汇编流程,见下图:
看不懂汇编,但是根据关键字眼也是可以发现一些端倪的,比如同样会走到objc_alloc方法
,继续采用符号断点
方式查找出处。
4.探索总结
根据上面的探索方式我们找到了alloc方法
的出处,也就是动态库:libobjc.A.dylib
。
同时也有个疑问,初始化调用了alloc方法,但在上面的汇编跟踪发现会调用objc_alloc方法,这两个方法又有什么关系呢?
如果要深入学习alloc
的实现流程,下载源码objc4源码
。源码下载地址:objc4源码下载地址
三.结合源码分析alloc
源码下载完成,采用关键字alloc {
全局搜索,在NSObject.mm文件(Objective-C++汇编)
中找到alloc
的方法实现。
1.alloc的流程分析
根据上面搜索的alloc方法
实现,跟踪关键源码如下:
// 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
// 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_rootAllocWithZone
id
_objc_rootAllocWithZone(Class cls, malloc_zone_t *zone __unused)
{
// allocWithZone under __OBJC2__ ignores the zone parameter
return _class_createInstanceFromZone(cls, 0, nil,
OBJECT_CONSTRUCT_CALL_BADALLOC);
}
从源码可以得到如下流程:
如何验证callAlloc
走的是_objc_rootAllocWithZone
还是objc_msgSend
呢?没错上面已经学习了探索方式,这里可以使用汇编+添加符合断点
!
下面设置符号断点,在程序运行到GFPerson * gf1 = [GFPerson alloc];
之后再将符号断点设置为Enable Breakpoint
,确保跟踪的是GFPerson
的初始化流程。断点调试,发现走的是_objc_rootAllocWithZone
的流程。
继续跟踪源码,_objc_rootAllocWithZone ->class_createInstanceFromZone
,那么alloc的流程
见下图:
2.流程梳理
alloc
的流程跟踪完成,貌似没啥问题,但是上一章节中的疑问还没有解开!初始化调用了alloc方法,但在上面的汇编跟踪发现会调用objc_alloc方法,这两个方法又有什么关系呢?
- 在
control - step into
的方式中,跟踪代码,走的是objc_alloc方法
。 - 汇编查看跟踪方式中,也是调用了
objc_alloc方法
。
问题出在哪?既然有源码了,设置断点,跟踪一下!分别在alloc方法
和objc_alloc方法
中设置断点。看看它到底会走到哪里!设置方式见下图:
同样的处理方式,当程序走到GFPerson * gf1 = [GFPerson alloc];
之后再将alloc方法
和objc_alloc方法
两处断点设置为Enable Breakpoint
,确保跟踪的是GFPerson
的初始化流程。运行代码:
神奇的事情发生了,调用的alloc方法
,结果运行进入了objc_alloc方法
中。断点调试,跟踪发现,会调用callAlloc
的objc_msgSend
方法,发送一个alloc消息
。
这个流程有些长,还需要再梳理一下,关键点在callAlloc方法
,calloAlloc
到底会走哪个分支需要深入研究一下!关键代码是:
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
// 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));
}
bool hasCustomAWZ() const
{
return !cache.getBit(FAST_CACHE_HAS_DEFAULT_AWZ);
}
代码解读:
- 判断缓存中是否存在自定义的
alloc/allocWithZone
地方实现,显然第一次运行类中是没有该方法缓存的。 - cls是什么?类嘛?不是!看看源码的定义:
typedef struct objc_class *Class;
。所以cls是一个指针,指向一个结构体,这个结构体也就是Core Foundation层的类
! - 类的初始化在
read_images
方法执行时,而实例对象
的初始化在alloc
的时候。 - 第一次执行
((id(*)(id, SEL))objc_msgSend)(cls, @selector(alloc));
,会进行慢速方法查找
,找到NSObject类的alloc方法
,并将方法放入方法缓存
。 - 所以除了第一调用
alloc方法
外,之后在进行对象初始化会直接走_objc_rootAllocWithZone方法
。可进行debug跟踪验证!已验证!
综上,得出以下流程图:
至此基本摸清了alloc
的调用流程,但是问题是调用alloc方法为什么会走到objc_alloc中呢?
—— llvm!后面再解密!
四.Alloc核⼼⽅法
_class_createInstanceFromZone方法
核心流程见下图:
1.cls->instanceSize
此流程会计算出需要的内存空间⼤⼩。跟踪cls->instanceSize
源码是实现:
进行了编译器优化
,更容易执行缓存中fastInstanceSize方法
,进行快速计算所需的内存空间大小
。
最终会进入align16方法
,这个方法是16字节对齐
算法。
流程解析:
- 初始化的原始内存
8 + 15 = 23
,23二进制位:0001 0111
; - 15二进制位:
0000 1111
,取反操作得到:1111 0000
; - 然后将1、2中得到的数据进行&运算,同时为1才得1,结果为:
0001 0000 = 16
;
为什么需要16字节对齐
?有以下几点:
- cpu在存取数据时,并不是以字节为单位,而是以块为单位存取。频繁存取字节未对齐的数据,会极大降低cpu的性能,所以通过减少存取次数来降低cpu的开销;
16字节对齐
,是由于在一个对象中,第一个属性isa占8字节(继承自父类),当然一个对象肯定还有其他属性,当无属性时,会预留8字节,即16字节对齐
,如果不预留,相当于这个对象的isa和其他对象的isa紧挨着,容易造成访问混乱;16字节对齐
后,可以加快CPU读取速度,同时使访问更安全,不会产生访问混乱的情况。
2.calloc
向系统申请开辟内存,返回地址指针。此流程会临时分配一个脏内存,调用calloc
后分配的内存空间才是创建对象的内存地址。
3.obj->initInstanceIsa
关联到相应的类,即将开辟的内存空间指向所要关联的类!
通过运行结果发现,在调用obj->initInstanceIsa
之前,obj
只有一个内存地址,而调用之后明确了对象类型为LGPerson
。
五.补充内容
1.编译器优化
#define fastpath(x) (__builtin_expect(bool(x), 1))
#define slowpath(x) (__builtin_expect(bool(x), 0))
fastpath
:可以理解为快速流程,对更有可能执行的流程进行优化,调高运行速度;
slowpath
:基本流程,不被优化的。
一般针对发布版本设置为fastest, Smallest
,让发布版本更快更小。对开发版本设置为None
,便于调试!
2.内存优化
如果自定义类没有定义属性,仅仅只是继承自NSObject
,则这个实例对象实际占用的大小为8字节
,使用的为8字节对齐算法
。系统实际分配的内存大小,使用的为16字节对齐
的算法。
内存对齐可以理解为结构体中的成员申请内存大小时,系统最小分配的内存为8字节,是按每次8字节来申请的,不够8字节的也会申请8字节,然后按结构体中成员变量顺序再次申请,直到所有成员变量都能放下为止。
- 内存优化(属性重排)
当结构体成员内存小的在前面时会因为内存对齐的原因比较浪费内存,为了解决这个问题苹果中采用空间换时间,将类中的属性进行重排,以此来达到内存优化的目的。
3.instanceSize疑问
这里到底是走缓存还是下面的分支呢?
会走到缓存里!
在类的实现过程中,类是否为非懒加载,如果是非懒加载就会在main函数之前;如果懒加载,就会在第一次发送消息的时候,会对类进行初始化,也就是实现类!过程中会将fastInstanceSize设置到缓存中。
所以系统实际分配的内存大小,使用的为16字节对齐
的算法。
但是不太理解下面返回的8字节对齐是什么意思!
4.NSObject alloc
补充一下[NSObject alloc]
流程,因为在调用NSObject
的alloc方法
时,alloc已经放入缓存
,(系统初始化时,已经被其他的类调用,放入了缓存!)
。所以NSObjec alloc流程
会直接调用_objc_rootAllocWithZone
。