前语

前面在 iOS 目标底层原理之alloc剖析 中介绍了创立一个目标要害三步:

  1. 核算类占用的内存巨细
  2. 依据核算出来的类占用的内存巨细size,关于size进行16进制对齐,并在堆中拓荒空间
  3. 类(isa)和创立出来的目标进行相关

前面两节已经介绍了核算类的内存巨细和拓荒内存空间,今日来剖析一下isa 以及isa怎样和创立出来的目标进行绑定的

结构体和联合体

在探究isa之前,先来了解一下共同体和位域,由于isa就是经过共同体 + 位域完成的。

  1. 结构体 开发中结构体算是比较常用的了,运用如下:
struct DXJPersonStatus {
  BOOL sleep;
  BOOL eat;
  BOOL play;
  BOOL study;
}pStatus; // 人的状况
NSLog(@"sizeof(pStatus) = %lu",sizeof(pStatus));

终究打印: sizeof(pStatus) = 4,由于BOOL 占1个字节,4个变量,所以占用4个字节,那就是 4 * 8 = 32位。这儿其实存在两个问题,第一个:BOOL用 0 或者 1 就可以表达,但是这儿用4个字节(32位)表明;第二个:一个人不能又睡又吃又玩又学习的,睡 吃 玩 学习 只能有一种状况,从这个两个层面上考虑,这个规划是浪费了内存空间的。处理第一个问题,咱们运用:结构体 + 位域处理。处理第二个问题,咱们运用联合体 + 位域

  1. 结构体 + 位域
struct DXJPersonStatus {
  BOOL sleep : 1; // 1表明sleep只占用1位,eat play study同理
  BOOL eat : 1;
  BOOL play : 1;
  BOOL study : 1;
}pStatus; // 人的状况
NSLog(@"sizeof(pStatus) = %lu",sizeof(pStatus));

终究打印: sizeof(pStatus) = 1, 由于sleep eat play study分别只占用1位,一共4位,所以只占用了1个字节。相比上面的单纯的结构体节省了3倍的内存空间。

空间上的确节省了,但是睡 吃 玩 学习依旧是同时存在的,不符合常理,如下图所示,这儿运用:共同体(联合体) + 位域

iOS 底层原理之 isa & obj绑定isa

  1. 共同体 + 位域,这儿丰富了一下内部的数据类型,便于探究不同的数据类型在结构体和联合体中的不一致,其间DXJPersonStructDXJPersonUnion内部数据是一模一样,关于数据的赋值也是一模一样,接下来看下他们的表现。
struct DXJPersonStruct {
    char *name;
    int age;
    double height ;
}; 
union DXJPersonUnion {
    char *name;
    int age;
    double height ;
};

iOS 底层原理之 isa & obj绑定isa

剖析上图:

  1. sizeof(pStruct),其间pStruct是一个结构体,char * 占8个字节,int占4个字节,double占8个字节,一共占用了[0-7],[8-11],(12-15)[16-23] => 24字节

  2. sizeof(pUnion),其间pUnion是一个联合体,联合体的巨细取决于体内最大成员变量的巨细,所以是8字节

  3. 红框打印中发现,结构体pStruct内部数据都正常的显示了出来:name = "DXJ", age = 18, height = 163

  4. 联合体:

    4.1 标签1:pUnion 只是创立,没有赋值时,内部数据都是指向的脏内存

    4.2 标签2:pUnion,给name赋值,age和height依旧指向脏内存

    4.3 标签3:pUnion,给age赋值,发现name = ""height依旧指向了脏内存

    4.4 标签4:pUnion,给height赋值,发现name = "" age = 0

在探究过程中咱们发现,联合体给内部成员变量赋值时,只保留当时成员变量的赋值,之前关于其他成员变量的赋值都置为了默认值,这儿得出了联合体和结构体的特点:

联合体: 各个成员变量之间是互斥的,那缺陷就是不够容纳,长处是内存运用更为精细灵敏,也节省了内存空间。

结构体: 各个成员变量之间是共存的,缺陷就是无论变量是否运用都会拓荒内存空间,长处是各个成员变量之间是共存。

isa

isa是做什么?在哪里有表现呢? 那首先查看下p这个目标在内存中分布:

iOS 底层原理之 isa & obj绑定isa

iOS 底层原理之 isa & obj绑定isa

目标的isa就是打印目标时,放在首位位置的内存地址,在libobjc.A.dylibobjc-private中找到了isa_t源码(截取首要部分):

union isa_t {
    // 联合体的两种析构方法
  isa_t() { }
  isa_t(uintptr_t value) : bits(value) { }
    /**
    由于isa_t是联合体,所以bits 和 cls 以及 下方的结构体 是互斥的
    cls 是private类型的,所以外部只能操作bits和下方的结构体
    */
  uintptr_t bits;
private:
  Class cls;
public:
// __x86_64__ 装备
// 改结构体内部是共存的
struct {
   uintptr_t nonpointer    : 1;
   uintptr_t has_assoc     : 1;
   uintptr_t has_cxx_dtor   : 1;
   uintptr_t shiftcls     : 44; /*MACH_VM_MAX_ADDRESS 0x7fffffe00000*/
   uintptr_t magic       : 6;
   uintptr_t weakly_referenced : 1;
   uintptr_t unused      : 1;
   uintptr_t has_sidetable_rc : 1;
   uintptr_t extra_rc     : 8
  };
    // obj 和 cls 绑定
  void setClass(Class cls, objc_object *obj);
    // 获取类
  Class getClass(bool authenticated);
    // 获取编码后的类
  Class getDecodedClass(bool authenticated);
};

剖析一下上方代码,首要考虑成员变量:由于isa_t是一个联合体,所以成员变量bits 和 cls 以及结构体(结构体内部是共存的) 他们三个是互斥的,也就是

  • bits被赋值时,cls 假如曾经有赋值则被bits的值掩盖,假如未曾赋值则为空
  • cls被赋值时,bits假如曾经有赋值则被cls的值掩盖,假如未曾赋值则为空

剖析结构体内部数据之前,先看下这些成员变量表达的意义,这是结构体+位域组成的,占8个字节 1 + 1 + 1 + 44 + 6 + 1 + 1+ 1 + 8 = 64 => 8字节,这儿也表现了位域使内存愈加精细灵敏。以x86_64(模拟器)做剖析:

  • nonpointer : 1: 占用1bit,表明是否对isa做了指针优化,1:不止类的地址,还包括了其他的信息,例如 目标的引证计数,是否有相关目标,析构函数,是否指向ARC的弱引证变量…..
  • has_assoc : 1:占用1bit,相关目标标志位,1:有,0:没有
  • has_cxx_dtor : 1:占用1bit,该目标是否有C++或Objc的析构函数,1:有,0:没有
  • shiftcls : 44:占用44bit,类的指针的值
  • magic : 6:占用6bit,用于调试器判断当时目标是真的目标仍是么有初始化的空间
  • weakly_referenced : 1:占用1bit,目标是否被指向或曾经指向一个ARC的弱变量,没有弱引证的目标可以更快开释
  • extra_rc : 8:占用8bit,该目标的引证计数值,实际上是引证计数值减1

isa底层完成总结

  • isa有两种类型nonpointer 和 非nonpointer类型,nonpointer包括的类其他信息,是否有相关目标,是否有析构函数,目标引证计数等;非nonpointer是一个纯类的指针
  • 在开发中,会频繁的创立目标,每个目标有包括isa指针,这样会占用很多的内存,所以isa采用了联合体 +位域的算法来完成, 联合体 中成员变量互斥的特性,节省了部分内存空间,位域的运用,优化了内存的存储,使得内存的运用愈加灵敏。这样isa占用的内存空间得到了很大的优化。

obj绑定isa

iOS 底层原理之 isa & obj绑定isa
剖析:

  1. isa_t newisa(0) 创立一个isa_t类型的实例,内部数据为空,如下图

iOS 底层原理之 isa & obj绑定isa

  1. 假如对错nonpointer,也就是没有敞开指针优化,是一个纯的isa,直接将clsthis绑定newisa(cls, this)(这儿的this就是调用者,也就是obj)

  2. 假如是nonpointer类型,说明敞开了指针优化,包括了其他的信息

    3.1 newisa.bits = ISA_MAGIC_VALUE ,此刻除了bits会赋值,结构体内部的magic 和 no npointer也会赋值,magic 是占用了[48 - 53]位,nonpointer占用了第1位,依据ISA_MAGIC_VALUE掩码,核算得出magic = 59, nonpointer = 1

    iOS 底层原理之 isa & obj绑定isa

    3.2 ISA_HAS_CXX_DTOR_BITx86_64下为1,newisa.has_cx x_dtor = hasCxxDtor;是否有C++析构函数

    3.3 newisa.setClass(cls, this);然后再将clsthis绑定(这儿的this就是调用者,也就是obj)

    iOS 底层原理之 isa & obj绑定isa

扩展 ISA_MASK

isa_t中发现一个获取类内存地址的方法Class getClass(bool authenticated);其间有一个要害代码

uintptr_t clsbits = bits;
clsbits = clsbits & ISA_MASK;
// 也就是 bits = bits & ISA_MASK

ISA_MASKisa的掩码isa分别两种,其间nonpointer类型会包括一些其他的信息,有时候咱们指向获取到shiftcls的信息,这儿提供了两种方法_x86_64举例

0x011d800104179e6d,这是一个nonpointer类型的isa,怎样获取到shiftcls

  1. 经过位移的方法获取
// 0. x86_64的isa数据分布:这儿要获取的就是shiftcls
(magic +  weakly_referenced + unused + has_sidetable_rc + extra_rc)+ shiftcls + (nonpointer + has_assoc + has_cxx_dtor) = 17 + 44(shiftcls) + 3
// 1. 进制转化
十六进制:
0     1   1    d    8    0    0    1    0    4    1    7    9    e    6    d
转二进制:
0000 0001 0001 1101 1000 0000 0000 0001 0000 0100 0001 0111 1001 1110 0110 1101
//2. 右移三位 最有右端的101移出去了,最左端补了三个零
000 0000 0001 0001 1101 1000 0000 0000 0001 0000 0100 0001 0111 1001 1110 0110 1
//3. 左移20位 17 + 3 
000 0000 0000 0001 0000 0100 0001 0111 1001 1110 0110 1000 0000 0000 0000 0000 0
//4. 右移17位,回归44原来的位置
0000 0000 0000 0000 0 000 0000 0000 0001 0000 0100 0001 0111 1001 1110 0110 1000 
//5. 转化成16进制
0b 0000 0000 0000 0000 0 000 0000 0000 0001 0000 0100 0001 0111 1001 1110 0110 1000 
0x 0    0    0    0    0     0    0    1    0    4    1    7    9    e    6    8
0x0000000104179e68 就是终究的shiftcls
  1. 经过ISA_MASK掩码,按位与
// 1. 进制转化
#  define ISA_MASK    0x00007ffffffffff8ULL // ULL 无符号长整型
十六进制:
0    0    0    0    7    f    f    f    f    f     f    f    f    f    f    8
转二进制:
0000 0000 0000 0000 0111 1111 1111 1111 1111 1111  1111 1111 1111 1111 1111 1000
// 2. 
十六进制:
0     1   1    d    8    0    0    1    0    4    1    7    9    e    6    d
转二进制:
0000 0001 0001 1101 1000 0000 0000 0001 0000 0100 0001 0111 1001 1110 0110 1101
// 3. 按位与
   0000 0000 0000 0000 0111 1111 1111 1111 1111 1111 1111 1111 1111 1111 1111 1000
&  0000 0001 0001 1101 1000 0000 0000 0001 0000 0100 0001 0111 1001 1110 0110 1101
   0000 0000 0000 0000 0000 0000 0000 0001 0000 0100 0001 0111 1001 1110 0110 1000 = 0x0000000104179e68
// 0x0000000104179e68 就是终究的shiftcls。
// 其实这儿有更简单的核算方法,这儿就不描述了(依照按位与的特性,同为真时才为真)