在平时的开发中,若是说你没有对象,我是不信的。那么本着了解对象,呵护对象,关心对象的原则,咱们必定要清楚的知道什是事对象,以及对象是怎么来的和对象内部的构成。so,这篇文章就来剖析一下对象的前世此生c++
传送门☞iOS底层探索-准备工做算法
首先,咱们要知道对象OC的底层中,到底是以什么方式在存在的数组
1.首先建立对象后,咱们运行clang -rewrite-objc main.m -o test.c++
命令,将main文件转换为C++代码,看代码编译后的对象是什么样子的缓存
2.经过查看源码,咱们能够发现,一个对象在通过编译以后,在底层的是一个结构体的形式存在的,以下图bash
struct NSObject_IMPL NSObject_IVARS
结构体,这个结构体其实是全部继承自NSobject的类均有的一个属性,它是一个指向类对象的isa,如图
咱们给类添加了一个属性name
和一个成员变量name
,那么这二者有何区别呢,经过编译后的源码,咱们能够发现架构
1.成员变量和属性都是对象struct结构体中的一个变量,可是属性会自动变成带下划线的变量,如_name,而成员变量不会
2.属性会自动生成get和set方法,而成员变量不会
复制代码
底层get和set源码以下app
能够看到系统自动给方法增长了两个参数LGPerson * self, SEL _cmd
ide
name
为上层get方法名@16@0:8
为符号方法签名_I_LGPerson_name
为底层方法名关于方法签名:函数
第一个符号@为返回值类型源码分析
第二个16为方法所占用的偏移量,即为总长度
第三个@为系统方法生成的id类型参数
第四个0为@的偏移量,即从0位开始,id类型占有8个字节(0-7)
第五个:为SEL参数
第六个8为SEL方法的偏移量,即SEL参数从第8位开始(8-15) 下图为整理的符号表
对象的本质有了初步了解后,咱们须要知道对象是怎么开辟控件,怎么获取到的,经常使用的
[LGPerson alloc]init]
究竟是怎么工做的呢
根据准备工做,配置到objc的源码以后,咱们开始探索
经过断点逐步跟踪
发现alloc的底层调用了 _objc_rootAlloc(self)
objc_alloc(Class cls)
方法,后续才走
_objc_rootAlloc(Class cls)
1.hasDefaultAWZ
hasDefaultAWZ( )方法是用来判断当前class是否有默认的allocWithZone。
bool hasCustomAWZ() {
return ! bits.hasDefaultAWZ();
}
复制代码
bool hasDefaultAWZ() {
return data()->flags & RW_HAS_DEFAULT_AWZ;
}
复制代码
在对象的数据段data中,class_rw_t
中有一个flags
,RW_HAS_DEFAULT_AWZ
这个是用来标示当前的class或者是superclass是否有默认的alloc/allocWithZone:
。值得注意的是,这个值会存储在metaclass
中。
若是cls->ISA()->hasCustomAWZ()
返回YES,意味着有默认的allocWithZone
方法,那么就直接对class进行allocWithZone
,申请内存空间。
+ (id)allocWithZone:(struct _NSZone *)zone {
return _objc_rootAllocWithZone(self, (malloc_zone_t *)zone);
}
复制代码
2.canAllocFast
是否能够快速建立,若是能够,直接调用calloc函数,申请1块bits.fastInstanceSize()
大小的内存空间,若是建立失败,会调用callBadAllocHandler
函数,返回错误信息。新版本中canAllocFast默认返回false,由于宏定义FAST_ALLOC
没有进行定义
3.在新版本代码中,会直接走else的class_createInstance
方法
若是没有快速建立等方法,会走到class_createInstance
中, 这个方法是产生对象的关键步骤
hasCxxCtor
判断当前class或者superclass 是否有.cxx_construct构造方法的实现
2.instanceSize(extraBytes)
这个方法是计算对象中属性内存对齐的主要方法,其实属性以8字节内存对齐,对象以16字节内存对齐,相关更详细的分下会在内存对齐模块讲述
3.(id)calloc(1, size)
方法是对象进行申请内存的主要方法,以16字节对齐,后续malloc原理详细讲述
4.initInstanceIsa
和initIsa
两个方式是给闯将isa并关联到对象,后续isa模块详细讲述
经过上述流程,一个对象就已经申请内存,并建立了isa指向该类,完成了从无到有的孕育过程,那么init
和new
方法是用来干什么的呢。
经过源码可知,底层调用了_objc_rootInit
方法
_objc_rootInit
方法能够看到,init方法直接返回了
self,并无作其余任何的操做
经过源码能够看到,new方法只是调用了callAlloc
和init
方法
因此,init和new方法只是返回了alloc以后就返回了对象自己,没有作其余操做,是方便开发者重写本身的逻辑的一种工厂模式
上一节咱们对对象的孕育有了一个大致的了解,可是有些概念仍是不太熟悉,好比对象是如何开辟内存的,到底开辟多少的内存才合适?,对象的属性和对象自己是如何进行二进制对齐的?这一节主要解决这个问题,并探寻原理
经过对象的本质咱们知道,对象在底层是以结构体的形式存在的,那么要计算结构的大小,就须要知道结构体中所包含的全部属性的大小,可是进行计算并非单纯想加各元素所占字节大小,编译器会进行优化
struct LGStruct1 {
char a; // 1 [0]
double b; // 8 [8,15]
int c; // 4 [16,19]
short d; // 2 [20,21]
} MyStruct1;
struct LGStruct2 {
double b; // 8 [0,7]
int c; // 4 [8,11]
char a; // 1 [12]
short d; // 2 [14,15]
} MyStruct2;
NSLog(@"%lu---%lu---%lu",sizeof(MyStruct1),sizeof(MyStruct2));
复制代码
上述代码的打印结果为 24-16,其内部遵循如下原则 初始位置为0,以后每一个元素的位置遵循min(当前开始的位置m n)
LGStruct1:
char a [0]; // [1,7]为空,由于都不是double字节8的倍数
double b [8,15] // 8位double字节8,直接存放
int c [16,19] // 16为int字节4 的倍数,直接存放
short d [20,21] // 20 为short 2 的倍数,直接存放
复制代码
LGStruct1一共占有了22字节,可是总大小必定要为元素最大字节数的倍数,里面最大为8字节,因此总字节数应为8的倍数,因此共申请24字节,以此类推,能够获得LGStruct2共占有了16字节
经过以上的例子,咱们基本能够总结内存对齐的三原则为:
结构体(struct)或联合体(union)的数据成员,第一个数据成员放在offset为0的地方,之后每一个数据成员存储的起始位置要从该成员大小或者成员的子成员大小(只要该成员有子成员,好比数组、结构体等)的整数倍开始。 eg: int为4字节,则要从4的整数倍地址开始存储
结构体做为成员:若是一个结构体内部包含其余结构体成员,则结构体成员要从其内部最大元素大小的整数倍地址开始存储。 eg: struct a里包含struct b,b中包含其余char、int、double等元素,那么b应该从8(double的元素大小)的整数倍开始存储
收尾工做:结构体的总大小,也就是sizeof的结果,必须是其内部最大成员的整数倍,不足的须要补齐。
经过上一节instanceSize(extraBytes)
方法的源码咱们来首先进行分析
alignedInstanceSize
方法:word_align
函数是一个进行对齐的算法
该算法和上述例子对齐方式是一个道理,其中WORD_MASK
为7,经过二进制的& ~ 运算,即表明该算法为8字节对齐,即所计算出的内存为8的倍数,表明对象实际根据属性数来申请内存的话,实际上是以8的倍数来进行申请的
LGTeacher *p = [LGTeacher alloc];
p.name = @"LG_Cooci"; // 8
p.age = 18; // 4
p.height = 185; // 8
p.hobby = @"女"; // 8
NSLog(@"%lu - %lu",class_getInstanceSize([p class]),malloc_size((__bridge const void *)(p)));
复制代码
上述例子打印的结果为 40,48
根据内存原则,咱们能够知道,类实际有class_getInstanceSize
大小为8倍数40自家,可是为何malloc_size
有48呢,咱们经过追踪源码,发现instanceSize
和(id)calloc(1, size)
的size均为40,那么calloc函数到底作了什么操做
咱们能够经过libmalloc源码进行分析 首先建立一个下图所示代码
找到calloc的底层实现,因为返回retval
,因此调用malloc_zone_calloc
方法
ptr
,因此要寻找
zone-calloc
default_zone_calloc
(lldb) p zone->calloc
(void *(*)(_malloc_zone_t *, size_t, size_t)) $0 = 0x000000010031cd14 (.dylib`default_zone_calloc at malloc.c:249)
复制代码
依旧点击不了,LLDB走起,发现调用为nano_calloc
(lldb) p zone->calloc
(void *(*)(_malloc_zone_t *, size_t, size_t)) $0 = 0x000000010031e33f (.dylib`nano_calloc at nano_malloc.c:878)
复制代码
经过代码可知,若是在最大值只下,会return p,若是大于才会走向helper_zone
,并递归,因此跟踪_nano_malloc_check_clear
segregated_size_to_fit
是用来计算的大小的主要方法,而且返回了48,因此对齐方法在此
slot_bytes
,此时
NANO_REGIME_QUANTA_SIZE
为15,
SHIFT_NANO_QUANTUM
为4,因此slot_bytes为[目标值 > > 4 < < 4]的位运算,是16字节对齐的,因此申请为40,16字节对齐后为48
经过上述mallco的流程咱们能够知道,对象是以16字节对齐的,因此在属性对齐时,才会要求最小16字节
malloc的流程图以下 calloc流程.jpg
n字节对齐方式算法(x为初始值):
1.(x + (2^n - 1))& ~(2^n - 1)
2.(x + (2^n - 1) 位运算:>>n <<n
总结:根据内存原则,对象在申请内存空间时,首先会进行属性对齐,此时会已8字节进行对齐,最后会对象对齐,此时已16字节进行对齐,因此对象最小为16字节,并已16字节对齐
经过上面的小结,咱们已经知道对象是如何建立,内存时如何申请的了,那么咱们都知道,每一个集成字NSObject的类的默认属性isa,那么isa本质究竟是个什么,它是怎么绑定的类,以及做用究竟是啥?
经过alloc流程分析,咱们找到initIsa
源码中堆isa的定义
咱们能够看到isa实际是一个isa_t
结构,看源码可知,isa是一个联合体,联合体中各元素共享内存,并互斥,且isa总共占有8字节,64位,在类中以Class 对象存在,是用来指向类的地址
ISA_BITFIELD
中存有那些元素么,这个会根据系统架构的不一样,有不一样的元素,咱们以arm64结构来解读,根据各元素站的位数可得,一共为64位8字节
nonpointer
表示是否对 isa 指针开启指针优化 0:纯isa指针,1:不⽌是类对象地址,isa 中包含了类信息、对象的引⽤计数等has_assoc
关联对象标志位,0没有,1存在has_cxx_dtor
该对象是否有 C++ 或者 Objc 的析构器,若是有析构函数,则须要作析构逻辑, 若是没有,则能够更快的释放对象shiftcls
存储类指针的值。开启指针优化的状况下,在 arm64 架构中有 33 位⽤来存储类指针。magic
⽤于调试器判断当前对象是真的对象仍是没有初始化的空间weakly_referenced
指对象是否被指向或者曾经指向⼀个 ARC 的弱变量, 没有弱引⽤的对象能够更快释放。deallocating
标志对象是否正在释放内存has_sidetable_rc
当对象引⽤技术⼤于 10 时,则须要借⽤该变量存储进位extra_rc
当表示该对象的引⽤计数值,其实是引⽤计数值减 1, 例如,若是对象的引⽤计数为 10,那么 extra_rc
为 9。若是引⽤计数⼤于 10, 则须要使⽤到下⾯的 has_sidetable_rc
若是为1,由于是结构体,且前面占有3位,则须要对类指针进行 >>3的位运算,来存储类的信息,对cls的地址右移动3位的目的是为了减小内存的消耗,由于类的指针须要按照8字节对齐,也就是说类的指针的大小一定是8的倍数,其二进制后三位为0,右移三位抹除后面的3位0并不会产生影响。
经过上面咱们知道了isa是绑定类的,那么咱们能够经过object_getClass
方法来经过对象是怎么获类的。源码以下
Class object_getClass(id obj)
{
if (obj) return obj->getIsa();
else return Nil;
}
objc_object::getIsa()
{
// 通常都不是TaggedPointer,这是特殊指针
if (!isTaggedPointer()) return ISA();
}
objc_object::ISA()
{
assert(!isTaggedPointer());
#if SUPPORT_INDEXED_ISA
if (isa.nonpointer) {
uintptr_t slot = isa.indexcls;
return classForIndex((unsigned)slot);
}
return (Class)isa.bits;
#else
// 通常状况下走这里,获取到类
return (Class)(isa.bits & ISA_MASK);
#endif
}
// 64位架构下 ISA_MASK的值为
# define ISA_MASK 0x00007ffffffffff8ULL
复制代码
经过以上源码能够发现获取对象的类就是获取对象的isa,而isa经过位域&上一个mask(isa.bits & ISA_MASK
),就能够获取类。
那么让我经过打印LGPerson *p = [LGPerson alloc];
对象的地址来看,首先对象的第一个属性为isa,即0x10200c480第一个值为isa的值,而后获取到LGPerson.class
类的地址。
1.经过验证咱们能够获得,对象的isa是指向对象的类
那么类对象的isa又指向什么呢,咱们能够经过上述命令继续验证
那么元类又指向什么呢,继续验证
那么根元类又指向什么呢,走起
总结一下:
经过上述验证,能够获得经典流程图
至此一个对象的申请内存并建立,且与类之间的关系已经所有探索完毕,一个章节将要进行类的探究