这是我参与8月更文挑战的第1天,活动详情查看:8月更文挑战
作为一名iOSer,平时写的最多的就是alloc对象。那么alloc底层做了什么呢?我们不得而知,下面就开始探索下alloc的底层实现流程。
一、alloc对象的指针地址和内存
首先我们观察下面一段代码,分别打印出对象、对象地址和对象指针的地址。
LRPerson *p1 = [LRPerson alloc];
LRPerson *p2 = [p1 init];
LRPerson *p3 = [p1 init];
LRPerson *p4 = [LRPerson alloc];
NSLog(@"%@-%p-%p",p1,p1,&p1);
NSLog(@"%@-%p-%p",p2,p2,&p2);
NSLog(@"%@-%p-%p",p3,p3,&p3);
NSLog(@"%@-%p-%p",p4,p4,&p4);
// 打印结果
// <LRPerson: 0x600003320200>-0x600003320200-0x7ffee81e30f8
// <LRPerson: 0x600003320200>-0x600003320200-0x7ffee81e30f0
// <LRPerson: 0x600003320200>-0x600003320200-0x7ffee81e30e8
// <LRPerson: 0x600003320250>-0x600003320250-0x7ffee81e30e0
根据打印结果,我们发现
- p1、p2、p3打印的对象、对象地址相同,指针地址不同
- p4打印输出的对象、对象地址及指针地址都不相同
- p1到p4的对象地址是从低地址到高地址,指针地址是从高地址到低地址
结合上图分析:
alloc在堆中开辟一块内存空间创建对象。init初始化并不会开辟内存空间创建对象。
那么alloc是如何分配内存空间创建对象,init又具体做了什么呢?
二、底层探索的三种方法
在日常开发中我们一般会按住command+点击进入相应的类或方法,但是点击alloc没有发现方法的具体实现,只看到方法的声明。这时我们该如何继续探索底层呢?下面介绍三种探索底层的方法
1、断点调试
在alloc处添加断点,运行程序停在断点处时,按住control键点击step into,进入下一步,可以看到调用了objc_alloc方法。
将objc_alloc添加符号断点,继续运行程序
然后就会发现objc_alloc来自于libobjc.A.dylib动态库
2、查看汇编跟流程
查看汇编方式:选中菜单Debug -> Debug Workflow -> Always Show Disassembly。即可看到汇编代码,按住control键点击step into即可一步一步往下走,最终也会进入到objc_alloc。
其实我们可以在汇编中看到一些关键字,也可直接添加关键字的符号断点
3、添加已知方法的符号断点
例如我们正在探索alloc方法,在程序断住后直接添加alloc的符号断点
然后就会发现alloc也是来自libobjc.A.dylib动态库。这里为何走到了+[NSObject alloc],因为LRPerson继承自NSObject,且没有alloc方法,会调用父类的alloc方法
除了以上的三种方式,我们还可以通过反汇编、LLDB工具、堆栈等方式,进行底层原理的探究
通过上面列举的三种方法,我们定位到
alloc来自于libobjc动态库,接下来我们需要继续探索libobjc源码
三、结合源码调试分析
首先我们需要下载苹果官方提供的源码,
源码下载地址:objc4源码。这里我们使用的是 objc4-818.2.tar.gz源码进行调试分析。
附上kc老师的Github地址:github.com/LGCooci/obj…(源码可编译)
1、alloc流程分析
直接在源码工程中搜索alloc {可以发现alloc实现方法。
跟着源码一步步点进去alloc->_objc_rootAlloc->callAlloc,会来到callAlloc方法
此时我们下个callAlloc符号断点,并运行源码,断住后bt查看堆栈信息,这里发现callAlloc是由objc_alloc进来的,结合上面我们讨论探索底层方法时通过断点和汇编可以发现alloc->objc_alloc->callAlloc。为什么和这里源码一步步点进去分析的流程不一致呢?我们后续再展开讨论。
继续往下探索,callAlloc方法中到底是先走_objc_rootAllocWithZone还是objc_msgSend呢?此时我们需要汇编+符号断点进行调试。先将符号断点设置为Enable状态,在断住alloc方法后勾选查看汇编、启用符号断点继续往下走
这样就进入到了_objc_rootAlloc汇编代码中,此时我们可以看到先调用的_objc_rootAllocWithZone再调用objc_msgSend
跟着_objc_rootAllocWithZone往下走。我们最终来到_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());
// Read class's info bits all at once for performance
//判断当前class或者superclass是否有.cxx_construct构造方法的实现
bool hasCxxCtor = cxxConstruct && cls->hasCxxCtor();
//判断当前class或者superclass是否有.cxx_destruct析构方法的实现
bool hasCxxDtor = cls->hasCxxDtor();
//标记类是否支持优化的isa
bool fast = cls->canAllocNonpointer();
size_t size;
//通过内存对齐得到实例大小,extraBytes是由对象所拥有的实例变量决定的。
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;
}
//初始化实例isa指针,关联到相应的类
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);
}
由此分析_class_createInstanceFromZone核心方法是这三个
size = cls->instanceSize(extraBytes);先计算出需要的内存空间大小obj = (id)calloc(1, size);向系统申请开辟内存空间,返回地址指针obj->initInstanceIsa(cls, hasCxxDtor);初始化指针,关联到相应的类
接下来我们将对这三个核心方法进行探索
2、alloc核心方法
instanceSize:计算所需内存大小
断点跟进instanceSize的实现
没有缓存调用alignedInstanceSize方法,返回字节对齐的内存大小。最后如果大小不足16,设置最小为16
而
alignedInstanceSize是由unalignedInstanceSize方法决定的,可以发现源码中unalignedInstanceSize最终取决于instanceSize实例变量的大小。
获取到实例变量内存大小后,通过word_align()进行8字节对齐。
有缓存则走fastInstanceSize方法,最终内存大小通过align16()进行16字节对齐
为何是
16字节对齐
cpu读取数据是以固定字节块来读取的,如果频繁的读取字节未对齐的数据,会降低cpu的性能和读取速度,用空间换时间。- 由于对象中
isa指针是占8个字节,当无属性时,会预留8字节。如果不进行16字节对齐 ,对象之间就会紧挨着,容易造成访问混乱。16字节对齐后访问更安全
calloc:申请开辟内存,返回指针地址
上一步instanceSize计算出所需内存的大小,这里由calloc向系统申请所需内存并将内存地址返回给obj,obj初始化时会分配一个脏地址
initInstanceIsa:初始化指针,关联到相应的类
前面两部计算内存大小开辟内存空间后obj只是一个内存地址,还没和相应的类进行关联。initInstanceIsa方法将初始化指针,关联到相应的类。