前言
通过上一篇文章《iOS对象的本质》可以知道对象在底层被编译成结构体,其中第一个属性就是对象的isa。那么什么是isa呢?isa的包含哪些信息呢?isa的结构怎么的呢?它跟对象又是怎么联系在一起的?带着种种的问题我们往下走吧!(#.#)
准备工作
联合体(union)
在开发过程中常用到一种结构体类型 struct,有一种和结构体比较相似的结构,叫共用体,也称联合体。代码如下:
// 联合体 : 互斥
union myPersion {
short money; //2
int age; //4
double height ; //8
};
int main(int argc, const char * argv[]) {
@autoreleasepool {
union myPersion persion;
persion.money = 1;
NSLog(@"money=%d---age=%d---height=%f",persion.money,persion.age,persion.height);
persion.age = 18;
NSLog(@"money=%d---age=%d---height=%f",persion.money,persion.age,persion.height);
persion.height = 170.0;
NSLog(@"money=%d---age=%d---height=%f",persion.money,persion.age,persion.height);
NSLog(@"persion联合体的大小=%ld",sizeof(persion));
}
运行结果:
2021-06-16 15:39:03.030731+0800 001-联合体位域[66996:1423574] money=1---age=1---height=0.000000
2021-06-16 15:39:50.484786+0800 001-联合体位域[66996:1423574] money=18---age=18---height=0.000000
2021-06-16 15:40:14.492142+0800 001-联合体位域[66996:1423574] money=0---age=0---height=170.000000
2021-06-16 15:40:54.421131+0800 001-联合体位域[66996:1423574] persion联合体的大小=8
得出结论:
- 联合体可以定义多个不同类型的成员,联合体的内存大小由其中
最大的成员的大小决定。 - 联合体中
修改其中的某个变量会覆盖其他变量的值或者取代其他的值。(union内存块大小够之前变量存放的时候会覆盖,不够就取代) - 联合体所有的变量
公用一块内存,变量之间互斥。
与结构体(struct)相比;联合体(union)的优缺点: 结构体(struct)中所有变量是“共存”的——优点是“有容乃大”, 全面;缺点是struct内存空间的分配是粗放的,不管用不用,全分配。联合体(union)中是各变量是“互斥”的——缺点就是不够“包容”; 但优点是内存使用更为精细灵活,也节省了内存空间.
位域(Bit field)
在开发过程中为了更加的合理利用内存,有些信息在存储时,并不需要占用一个完整的字节,而只需占几个或一个二进制位。例如BOOL值在存放只有一个只有0和1两种状态成员, 用一位二进位即可。分析代码如下:
// 4 位
// 1 字节 3倍浪费
struct LGCar1 {
BOOL front; // 0 1
BOOL back;
BOOL left;
BOOL right;
};
// 位域
// 互斥
struct LGCar2 {
BOOL front: 1;
BOOL back : 1;
BOOL left : 1;
BOOL right: 1;
};
int main(int argc, char * argv[]) {
@autoreleasepool {
struct OldCar oldCar;
struct NewCar newCar;
NSLog(@"----%lu----%lu",sizeof(oldCar),sizeof(newCar));
}
return 0;
}
运行结果:
2021-06-16 16:00:20.379723+0800 001-联合体位域[67528:1433973] ----4----1
分析结果:
好明显oldCar结构体的大小为4字节,newCard经过位运算大小为1字节。在newCard结构体1个字节的内存中,front、back、left和right都是占用1位,表示形式为:0000 1111。
isa与类之间的关联流程
在《alloc底层原理探索》一文中已经结合alloc源码详细分析了一波isa指针与类是怎么样关联的。这里我就粗略的分析一下。
alloc一个对象最核心的三个方法cls->instanceSize计算内存大小 ,(id)calloc(1, size)开辟内存返回地址指针,obj->initInstanceIsa初始化isa关联类。具体的流程总结为:alloc--> _objc_rootAlloc--> callAlloc--> _objc_rootAllocWithZone-->_class_createInstanceFromZone,断点在 obj->initInstanceIsa。
bj->initInstanceIsa源码流程分析:




分析结果:
isa_t其实就是一个联合体(union),sa_t有两个变量 一个是bits,一个是cls。通过上面分析联合体是互斥的,那就意味着初始化isa有两种方式:
bits被赋值,cls 没有值或者被覆盖-
cls 被赋值,bits没有值或者被覆盖
那么互斥一定存在一个变量嘛?变量都会被覆盖或者没有值嘛?联合体有没有具体的操作打破这种规律?哈哈哈,见下图:

我们尊敬的kc老师想都没想就给出他非常专业的答案:这个union进行了description表达
isa的结构
首先我们进去ISA_BITFIELD字段,看到的定义如下图:


isa各个字段在其64位的分布

各变量的含义
-
nonpointer:表示是否对isa指针进行优化,0表示纯指针,1表示不止是类对象的地址,isa中包含了类信息、对象、引用计数等 -
has_assoc:关联对象标志位,0表示未关联,1表示关联 -
has_cxx_dtor:该对象是否C ++或者Objc的析构器,如果有析构函数,则需要做析构逻辑,没有,则释放对象 -
shiftcls:储存类指针的值,开启指针优化的情况下,在arm64架构中有33位用来存储类指针,x86_64架构中占44位 -
magic:用于调试器判断当前对象是真的对象还是没有初始化的空间
-weakly_referenced:指对象是否被指向或者曾经指向一个ARC的弱变量,没有弱引用的对象可以更快释放 -
deallocating:标志对象是否正在释放 -
has_sidetable_rc:当对象引用计数大于10时,则需要借用该变量存储进位 -
hextra_rc:表示该对象的引用计数值,实际上引用计数值减1,例如,如果对象的引用计数为10,那么extra_rc为9,如果大于10,就需要用到上面的has_sidetable_rc
isa结构分析的总结: - 首先isa指针分为纯isa指针与非纯isa之整,
nonpointer进行标记区分。 -
isa是联合体+位域的方式存储信息的。采用这种方式的有点就是节省大量内存。发挥了联合体的优势节省了比较多的内存空间。
initIsa深入分析
首先定义一个XXPersion,当newisa(0)调用完时候发现bits=0、cls=nil且其他成员信息都为0,如下图所示:




分析:
-
shiftcls = 537307166与上面的算法shiftcls=(uintptr_t)newCls >> 3得到的结果是一样的。 -
XXPerson的类地址>>3进行10进制转换赋值给shiftcls。此时isa已经关联XXPerson类 ,cls变量被覆盖 ,cls = XXPerson。
补充:进行右移3位的目的主要是保持8字节的对齐,也就是说指针的地址只能是8的倍数,那么指针地址的后3位只能是0。同时虚拟内存中arm64架构的中间33位是类的地址,加上后面补0的三位,类的地址为36位,同理x86架构下的类地址为44+3=47
isa位运算

位运算过程
p/x 0x011d800100004561 >> 3
(long) $38 = 0x0023b000200008ac
(lldb) p/x 0x0023b000200008ac << 20
(long) $39 = 0x000200008ac00000
(lldb) p/x 0x000200008ac00000 >> 17
(long) $40 = 0x0000000100004560
位运算过程图像分析

isa与ISA_MASK(掩码)进行&运算

分析总结:
p/x 0x011d800100004561 & 0x00007ffffffffff8ULL的结果是XXPersion,说明了isa已经关联了类。由图可知掩码(ISA_MASK)的二进制也是保留了中间的44位,其他位置为0,跟类在isa中的位置关系(shiftcls)是对应的。