一.了解对象与指针
先看一张图:
这张图我们可知:
- imgv是指针,指针指向的是对象;
- [JPeople alloc]创建了一个对象;
- p1写在等号前面,等于把P1指向了那个对象的内存地址,所以p1是指针;
- 同理可知p2,p3也是指针,指向了P1指向了那个对象的同一块内存地址;
- 所以我们都是通过通过这个指针找到内存中的对象(通过指针来找到对象而不是表示对象)
二.底层探索的三种方式
2.1 下断点方式
按着control + in 进入真机调试(模拟器是x86架构,真机是arm64架构):
in这里表示是上图所示的红框的按钮;
此时往下走找到libobjc.A.dylib动态库下的objc_alloc如图所示:
2.2 下符号断点
选择Symbolic Breakpoint(objc_alloc),
过去断点,我们得到了 alloc 实现位于 libObjc 这个动态库;
2.3 汇编方式
> Debug -> Debug Workflow -> Always show Disassembly
具体操作打开Debug菜单下的 Debug Workflow 下的 Always Show Disassembly如图:
然后跳转到objec_alloc 继续按住control + in 下一步,我们可以一步一步的找到libobjc.A.dylib库下的objc_alloc;
三.alloc初始化源码跟踪流程
1. alloc源码
1.alloc
+ (id)alloc {
return _objc_rootAlloc(self);
}
2._objc_rootAlloc
_objc_rootAlloc(Class cls)
{
return callAlloc(cls, false/*checkNil*/, true/*allocWithZone*/);
}
3.callAlloc
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));
}
4._objc_rootAllocWithZone
注意:allocWithZone under OBJC2 ignores the zone parameter allocWithZone 在 OBJC2 下可以忽略zone的此参数,可能做了优化
_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);
}
<font color=red>alloc源码流程图</font>
5._class_createInstanceFromZone
_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());
// Read class's info bits all at once for performance
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 {
// Use raw pointer isa on the assumption that they might be
// doing something weird with the zone or RR.
obj->initIsa(cls);
}
if (fastpath(!hasCxxCtor)) {
return obj;
}
construct_flags |= OBJECT_CONSTRUCT_FREE_ONFAILURE;
return object_cxxConstructFromClass(obj, cls, construct_flags);
}
2. 跟着源码查看堆栈信息分析:
我们看一下每一个堆栈对应的实现函数:
- main
People *p = [Peple alloc];
- objc_alloc
- callAlloc ---> objc_msgSend
走的第二个流程
- [NSObject alloc]的alloc
alloc
- _objc_rootAlloc
注意callAlloc第三个参数传true
- callAlloc
注意:走的第一个流程
- _objc_rootAllocWithZone
8. _class_createInstanceFromZone
代码解读
- 判断缓存中是否存在自定义的alloc/allocWithZone地方实现,显然第一次运行类中是没有该方法缓存的;
- 类的初始化在read_images方法执行时,而 实例对象的初始化在alloc 的时候。
- 第一次执行 ((id(*)(id, SEL))objc_msgSend)(cls, @selector(alloc));会进行慢速方法查找,找到 NSObject类的alloc方法,并将方法放入方法缓存。
- 所以除了第一调用 alloc方法外,之后在进行对象初始化会直接走_objc_rootAllocWithZone方法
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));
}
我们发现无论我们运行多少次都是这样流程:
3. alloc源码流程图
4.补充[NSObject alloc]流程
以上我分析了对象p的alloc流程,我们知道People继承于NSObject,那对于NSObject对象的底层流程到底是什么流程呢?
objc_alloc -> callAlloc -> _objc_rootAllocWithZone ->_class_createInstanceFromZone
走这样的流程是因为缓存已经有了,所以不会发送alloc消息
四.重要知识点的分析:
以上alloc流程图中我们三个方法如下图:
1. cls->instanceSize
此流程会计算出需要内存大小,跟踪源码如下:
- 通过缓存进行快速计算
快速途径
最终来到了align16方法,系统内存16字节对齐算法
为什么系统内存16字节对齐
- cpu在存取数据时,并不是以字节为单位,而是以块为单位存取。频繁存取字节未对齐的数据,会极大降低cpu的性能,所以通过减少存取次数来降低cpu的开销;
- 16字节对齐,是由于在一个对象中,第一个属性isa占8字节(继承自父类),当然一个对象肯定还有其他属性,当无属性时,会预留8字节,即16字节对齐,如果不预留,相当于这个对象的isa和其他对象的isa紧挨着,容易造成访问混乱;
- 16字节对齐后,可以加快CPU读取速度,同时使访问更安全,不会产生访问混乱的情况;
- 苹果对于内存做的一些容错考虑
- 第一进来没有在缓存中查找
alignedInstanceSize 方法走到 word_align方法,word_align方法中调用了unalignedInstanceSize
May be unaligned depending on class's ivars.
依赖于对象的属性
uint32_t unalignedInstanceSize() const {
ASSERT(isRealized());
return data()->ro()->instanceSize;
}
此方法可以看到从内存中ro取出干净的内存,大小为8字节;为什么呢? 1.其继承于NSObject ,中有isa,占用8个字节; 疑问点: 1.我们是在对象内存大小,而此时对象没有任何属性、方法成员变量、协议等,那到底影响对象内存大小因素到底是什么呢? 2.系统的16字节对齐为什么呢?从哪分析呢?上面也有部分分析到了,真正底层在哪呢?
2.calloc
向系统申请开辟内存,返回地址指针。此流程会临时分配一个脏内存,调用calloc后分配的内存空间才是创建对象的内存地址。
3.obj->initInstanceIsa
关联到相应的类,即将开辟的内存空间指向所要关联的类!通过运行结果发现,在调用obj->initInstanceIsa之前,obj只有一个内存地址且是id类型,而调用之后明确了对象类型为LGPerson
4.影响对象内存大小因素分析
对象占用内存内存大小,由其成员变量确定
验证过程如下:
打印可知对象占用了8个字节,(内存中数据以16字节对齐)
添加几个属性
结论:成员变量越多,对象内存占用空间越大
x/5gx p:以5个16进制的排版打印对象,也可以说成:5个8字节的排版打印对象p
5. init和new
1.init(汇编和符号断点方式)
在LGPerson * person = [[LGPerson alloc] init];这行代码中添加断点,并设置:Debug -> Debug Workflow -> Always Show Disassembly。运行程序,结果如下图:
发现该过程调用objc_alloc_init ,加入符号断点查看objc_alloc_init汇编
汇编流程上图所示:[[LGPerson alloc] init];过程是调用了callAlloc方法创建对象之后,向该对象发送init消息。这里再添加init符号断点,继续运行程序
最后找到[NSObject init]函数,LGPerson没有实现init方法,寻到是其父类的,在源码中最终会调用到_objc_rootInit方法
结合源码,设置断点进行调试跟踪,流程和我们在汇编+符号断点的流程是一致的,如下图所示!最终返回的内容是callAlloc创建的对象自身
总结:init只是一个构造方法,没有参与对象初始化的创建;工厂模式设计
init的流程
- [[LGPerson alloc] init];
- objc_alloc_init;
- 先进行对象内存开辟alloc流程;
- objc发送init消息;
- [NSObject init];
- _objc_rootInit;
- return self 返回当前对象;
2.new
探索方式和 -init() 一样, LGPerson * newlg = [LGPerson new];并不会直接调用+new(),而是执行了 objc_opt_new 方法,见下图:
源码上:
这里有类似callAlloc的判断流程,如果初次初始化,hasCustomCore()会返回true,见下图,这样会进入发送消息流程,即发送new消息。
因为LGPerson没有实现new类方法,所以在进行方法查找过程中会找到NSObject中,最终执行+[NSObject new];方法。见下图所示:
继续执行程序,会走到_objc_rootAllocWithZone中进行对象初始化
总结:new方法是个类方法,其是对[[cls alloc] init];流程的封装的过程
new的流程
- 1.[LGPerson new];
- 2.objc_opt_new;
- 3.先fastpath(cls && !cls->ISA()->hasCustomCore()有没有缓存第一次没有缓存;
- 4.没有缓存[NSObject new]发送new的消息;
- 5.调用[callAlloc(self, false/checkNil/) init]方法
- 6.有缓存时候直接[callAlloc(cls, false/checkNil/) init];
- 7.objc发送init消息;
- 8.[NSObject init];
- 9._objc_rootInit;
- 10.return self 返回当前对象;
虽然+new()是对[[cls alloc] init];的封装,但是依然建议使用[[cls alloc] init];进行对象的初始化!因为init作为构造器,可以自定义提供自己所需要的初始化方法;
更多解释
x/nuf <addr>
n 表示要显示的内存单元的个数
------------------------
u 表示一个地址单元的长度
b 表示单字节
h 表示双字节
w 表示4字节
g 表示8字节
------------------------
f 表示显示方式,可取以下值:
x 按十六进制格式显示变量
d 按十进制格式显示变量
u 按十进制格式显示无符号整型
o 按八进制格式显示变量
t 按二进制格式显示变量
a 按十六进制格式显示变量
i 按指令地址格式显示变量
c 按字符格式显示变量
f 按浮点数格式显示变量