iOS底层学习 - OC对象前世此生

在平时的开发中,若是说你没有对象,我是不信的。那么本着了解对象,呵护对象,关心对象的原则,咱们必定要清楚的知道什是事对象,以及对象是怎么来的和对象内部的构成。so,这篇文章就来剖析一下对象的前世此生c++

准备工做

传送门☞iOS底层探索-准备工做算法

对象的本质

首先,咱们要知道对象OC的底层中,到底是以什么方式在存在的数组

1.首先建立对象后,咱们运行clang -rewrite-objc main.m -o test.c++命令,将main文件转换为C++代码,看代码编译后的对象是什么样子的缓存

2.经过查看源码,咱们能够发现,一个对象在通过编译以后,在底层的是一个结构体的形式存在的,以下图bash

3.其中咱们能够发现有一个 struct NSObject_IMPL NSObject_IVARS结构体,这个结构体其实是全部继承自NSobject的类均有的一个属性,它是一个指向类对象的isa,如图

属性和成员变量

咱们给类添加了一个属性name和一个成员变量name,那么这二者有何区别呢,经过编译后的源码,咱们能够发现架构

1.成员变量和属性都是对象struct结构体中的一个变量,可是属性会自动变成带下划线的变量,如_name,而成员变量不会
2.属性会自动生成get和set方法,而成员变量不会
复制代码
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) 下图为整理的符号表

alloc原理

对象的本质有了初步了解后,咱们须要知道对象是怎么开辟控件,怎么获取到的,经常使用的[LGPerson alloc]init]究竟是怎么工做的呢

根据准备工做,配置到objc的源码以后,咱们开始探索

alloc流程

经过断点逐步跟踪

_objc_rootAlloc(self)

发现alloc的底层调用了 _objc_rootAlloc(self)

可是一个须要注意的点:系统动态库真是函数地址在共享缓存去,dyld加载的时候绑定一次符号,之后就直接找真实的函数地址,因此,第一次alloc的时候,会调用 objc_alloc(Class cls)方法,后续才走 _objc_rootAlloc(Class cls)

callAlloc(cls, false/checkNil/, true/allocWithZone/)

callAlloc分析

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

若是没有快速建立等方法,会走到class_createInstance中, 这个方法是产生对象的关键步骤

1. hasCxxCtor 判断当前class或者superclass 是否有.cxx_construct构造方法的实现

2.instanceSize(extraBytes)这个方法是计算对象中属性内存对齐的主要方法,其实属性以8字节内存对齐,对象以16字节内存对齐,相关更详细的分下会在内存对齐模块讲述

3.(id)calloc(1, size)方法是对象进行申请内存的主要方法,以16字节对齐,后续malloc原理详细讲述

4.initInstanceIsainitIsa两个方式是给闯将isa并关联到对象,后续isa模块详细讲述

流程图分析

init和new的原理

经过上述流程,一个对象就已经申请内存,并建立了isa指向该类,完成了从无到有的孕育过程,那么initnew方法是用来干什么的呢。

init源码

经过源码可知,底层调用了_objc_rootInit方法

经过 _objc_rootInit方法能够看到,init方法直接返回了 self,并无作其余任何的操做

new源码

经过源码能够看到,new方法只是调用了callAllocinit方法

因此,init和new方法只是返回了alloc以后就返回了对象自己,没有作其余操做,是方便开发者重写本身的逻辑的一种工厂模式

malloc和内存对齐

上一节咱们对对象的孕育有了一个大致的了解,可是有些概念仍是不太熟悉,好比对象是如何开辟内存的,到底开辟多少的内存才合适?,对象的属性和对象自己是如何进行二进制对齐的?这一节主要解决这个问题,并探寻原理

例子解读

经过对象的本质咱们知道,对象在底层是以结构体的形式存在的,那么要计算结构的大小,就须要知道结构体中所包含的全部属性的大小,可是进行计算并非单纯想加各元素所占字节大小,编译器会进行优化

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的结果,必须是其内部最大成员的整数倍,不足的须要补齐。

1. 属性对齐

经过上一节instanceSize(extraBytes)方法的源码咱们来首先进行分析

  • 经过判断咱们可知,一个对象所申请的空间,最低为16字节
  • alignedInstanceSize方法:

  • word_align函数是一个进行对齐的算法

该算法和上述例子对齐方式是一个道理,其中WORD_MASK为7,经过二进制的& ~ 运算,即表明该算法为8字节对齐,即所计算出的内存为8的倍数,表明对象实际根据属性数来申请内存的话,实际上是以8的倍数来进行申请的

2.对象对齐和malloc流程

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

直接点击的话,会找不到方法,因此要使用LLDB命令来寻找,发现对象的调用为 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字节对齐

isa的原理和走向

经过上面的小结,咱们已经知道对象是如何建立,内存时如何申请的了,那么咱们都知道,每一个集成字NSObject的类的默认属性isa,那么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

isa类绑定

经过源码发现isa有cls和shiftcls属性是与类相关的 若是时候纯isa指针,那么直接绑定cls便可

若是为1,由于是结构体,且前面占有3位,则须要对类指针进行 >>3的位运算,来存储类的信息,对cls的地址右移动3位的目的是为了减小内存的消耗,由于类的指针须要按照8字节对齐,也就是说类的指针的大小一定是8的倍数,其二进制后三位为0,右移三位抹除后面的3位0并不会产生影响。

isa的指向

经过上面咱们知道了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又指向什么呢,咱们能够经过上述命令继续验证

2.经过验证可得,LGPerson类的isa指向了一个地址彻底不一样,可是也名为LGPerson的类,咱们通常叫这个类为 元类,这是由系统建立的,没法操做

那么元类又指向什么呢,继续验证

3.经过验证可得LGPerson元类的isa指向了NSObject类,咱们通常叫此为 根元类

那么根元类又指向什么呢,走起

4.经过验证可得,根源类的isa就指向了本身,一个流程就此结束

总结一下:

  • 实例对象的isa指向的是类;
  • 类的isa指向的元类;
  • 元类指向根元类;
  • 根元类指向本身;
  • NSObject的父类是nil,根元类的父类是NSObject。

流程图

经过上述验证,能够获得经典流程图

总结

至此一个对象的申请内存并建立,且与类之间的关系已经所有探索完毕,一个章节将要进行类的探究

相关文章
相关标签/搜索