Runtime之isa详解

isa 概念

isa是相当于是OC对象的一个标识指针,只要是OC对象就一定会有isa指针,arm64之前isa就是一个指向对象或者类的指针而已,在arm64之后发生了一些改进,isa在arm64之后变成了一个共用体(union)结构,同时使用位域的思想来实现,达到节省内存的作用;从源码中,我们可以看到一个OC对象的isa指针并不是直接指向类对象或者元类对象的,而是要通过一个&ISA_MASK才能获取到真正的类对象或者元类对象,这个是为什么呢?在分析之前,我们有必要先弄懂一下共用体的相关概念和用法;

共用体概念

在进行某些算法的C语言编程的时候,需要使几种不同类型的变量存放到同一段内存单元中。也就是使用覆盖技术,几个变量互相覆盖。这种几个不同的变量共同占用一段内存的结构,在C语言中,被称作“共用体”类型结构,简称共用体。

寻找真理

现在假设一个Person对象有3个布尔类型变量tall,rich,handsome,那么如果我们平时定义的时候,肯定都是通过属性变量来定义,例如下图:

Runtime之isa详解_第1张图片
代码图

这个时候你通过终端打印,可以看到结果输出是16而不是(isa指针 = 8) + (BOOL tall = 1) + (BOOL rich = 1) + (BOOL handsome = 1) = 13,这个为什么是16,前面应该也讲过了,是内存对齐原则的原因,分配的内存小于16的都会直接返回16;这个时候共同体就发挥作用了,因为共用体中变量可以相互覆盖,可以使几个不同的变量存放到同一段内存单元中,这样可以很大程度上节省内存空间,众所周知BOOL值只有两种情况0或者1,但是却占据了一个字节的内存空间,而一个内存空间有8个二进制位(0,1组成的),于是可以尝试用一个二进制方式来代替bool变量,我们可以定义占用一个字节的char类型来存储3个bool值:

Runtime之isa详解_第2张图片
char 声明图

我们可以在Person对象中直接初始化这个_tallRichHandsome 的值

Runtime之isa详解_第3张图片
初始化

我们可以_tallRichHandsome的后三位分别为其赋值0或者1来代表tall、rich、handsome

Runtime之isa详解_第4张图片
tall,rich,handsome对应图

这里只需要搞懂是如何赋值和取值的就可以了,这里我们先看看赋值操作先

二进制的赋值操作

前面说过BOOL变量只有0和1,那么赋值操作就是只要将对应的位置设置为0或者1就可以了,赋值为1呢,我们使用 | (按位或)操作,因为按位或的做法是只要有一个为1,结果为1,所以这里我们如果想将某一位赋值为1的话,就将原来的值和相应的掩码进行按位或的操作就可以了,例如现在我想将tall赋值为1

根据前面的tall,rich,handsome的图,我们这里需要将第三位赋值为1,如下图所示

Runtime之isa详解_第5张图片
tall赋值操作

那如果想将某一位赋值为0的话,需要将对应的掩码按位取反(~:按位取反符号),之后再与原本的_tallRichHandsome进行按位与操作,例如下面所示:

Runtime之isa详解_第6张图片
handsome赋值为0

那么Person类中的set方法就可以按照下面这样设置:

Runtime之isa详解_第7张图片
set方法

这个时候我们再来看看二进制的取值;

二进制的取值操作

取值操作我们使用按位& 操作来实现,相同为1不同为0

Runtime之isa详解_第8张图片
取值操作

从图中的展示结果可以看到这样的取值操作是没有问题的,所以我们的get方法实现应该如下:

Runtime之isa详解_第9张图片
get方法实现

#define SLThinMask (1<<3)

#define SLTallMask (1<<2)

#define SLRichMask (1<<1)

#define SLHandsomeMask (1<<0)

上面中用到的源码分别如下,<< 代表左移符号,1 << 0,代表1左移0 也就是000000001,1<<1(00000010),1<<2(00000100)

接下来我们来验证一下测试结果是否正确

Runtime之isa详解_第10张图片
测试源码
没有加上!!

从结果中可以看到,这个结果不是我们想要的,我们不能这样返回一个BOOL变量值的,因为这个值返回去有可能是一个大于1的整数,所以这里我们采取来两个!!符号来实现,如果&的结果之后是0,那!0 代表1,!1代表0,正确,如果&结果返回1,!1是0,!0是1,返回结果也争取,所以上面的get方法更改成如下:

Runtime之isa详解_第11张图片
get方法纠正

虽然测试结果是正确的,但是代码具有一定的局限性,就是可读性差,如果需要多添加几个元素,就得重复上述的操作,这个时候我们可以考虑用位域来操作

位域

位域声明:位域:位域长度

位域形式大概如下:

Runtime之isa详解_第12张图片
位域

set get方法实现如下:

Runtime之isa详解_第13张图片
set get方法

测试代码和结果如下:

Runtime之isa详解_第14张图片
测试结果图

发现结果居然惊现-1,但是log打断点显示赋值已经是正确的了

Runtime之isa详解_第15张图片
终端调试图

上面计算机那里的07 可以看到后面3为都是111,说明已经是赋值成功的了,为什么直接拿07,不拿多其他多个字节呢,是因为_tallRichHandsome只占据一个内存空间,也就是一个字节,可能会有人疑问为什么是1个字节而不是三个字节,这个应该就是位域的性质了,看下图吧:

Runtime之isa详解_第16张图片
结构体
Runtime之isa详解_第17张图片
位域

上面的结果都能显示我们的赋值操作是正确的,那么出错的可能就应该是取值的时候操作有无导致的,将get方法稍微修改一下先:

Runtime之isa详解_第18张图片
修改测试图

可以发现的确是get方法错来,到那时错在哪里呢?因为BOOL变量占据一个内存空间,也就是8为,这里我们是将一个只占一个内存空间的一位0b1赋值到8位,前面的7个都是直接用0b前面的值补充,也就是1,这个时候就相当于符号为用1来填充,就全部变成来11111111,为什么明明显示是255,答案是-1呢,这里牵涉到有符号位和无符号位的关系,如果有符号位呢,这里11111111 就是-1 因为这里11111111是补码,需要返回原码,返回原码的操作就是减1取反,符号位置不变(原码--》补码 取反 再加1),无符号位就是255;

出现这种问题有两种解决方法,第一种就是将位域的长度扩大到2,也就是占用两个二进制树,那么就会变成ob01,这个时候赋值到8位的BOOL值时候就会用0取填充,最终就会变成来00000001;第二种方法就是和之前的一样,使用!!双非符号来解决问题,最终实现效果如下:

Runtime之isa详解_第19张图片
位域!!

相对来说,结构体的位域则不需要使用掩码,代码可读性增强,但是效力相比直接使用位运算的方式差,因为最终都是要转化成位运算的操作(汇编环节),所以我们可以采取用共用体,学习共用体之后,我们就可以看源码里面的一些设计思路来

共用体

为了提高高效率的同时又能有较强的可读性,可以使用共用体来增强代码可读性,同时使用位运算来提高效率

共用体设计如下:

Runtime之isa详解_第20张图片
共用体

get 方法 set方法如下图:

Runtime之isa详解_第21张图片
get set

上面的共用体中_tallRichHandsome1 占用1个字节,tall1,rich1,handsome1,thin1 只占一位二进制空间,所以结构体占用一个字节,前面打印结果有说到,而char类型的bits也只占一个字节,所以可以共用一个字节的内存;从上面的方法中可以看到,get,set方法并没有使用到结构体,而结构体的目的是为了增强代码的可读性,指明共用体中存储来哪些值,以及这些值各占多少位空间,同时存储取值还使用位运算来增加效率;好啦,这个时候可以进入查看isa_t 的源码了。

runtime-isa_t 源码

Runtime之isa详解_第22张图片
isa_t 共用体源码

有了之前的铺垫,现在再来看这份源代码是否有一些似曾相识的感觉呢?源码中通过共用体的形式存储来64位的值,这些值在结构体中被展示出来,通过对bits进行位运算而取出相应位置的值;

还记得我们之前在OC对象本质中提到过一件事,就是对象的isa指针需要同ISA_MASK经过一次& 运算才能得出真正的Class对象地址

Runtime之isa详解_第23张图片
isa&ISA_MASK

从源码中如果是arm64位的话,可以知道这个ISA_MASK的值是define ISA_MASK  0x0000000ffffffff8ULL,转为二进制表现形式是:0000 0000 0000 0000 0000 0000 1111 1111 1111 1111 1111 1111 1111 1111 1111 1000,可以看到有33位为1:

Runtime之isa详解_第24张图片
0x0000000ffffffff8ULL 二进制

所以共用体里面的shifcls中存储的Class,Meta_Class 对象的内存地址信息,其他的对应字段相应对应源码字段,这里有一个特殊点就是,因为这个ISA_MASK最后三位的值都为0,所以可以得出一个结论就是任何类对象或者猿类对象的内存地址最后三位必定为0,那也就是说明转为16进制之后,类对象或者元类对象的内存地址最后一位要不就是8要不就是0;

实例证明

运行环境:__arm64__位架构

实例代码图和调试图如下:

Runtime之isa详解_第25张图片
代码原图
Runtime之isa详解_第26张图片
终端输出图
Runtime之isa详解_第27张图片
二进制


Runtime之isa详解_第28张图片
isa&ISA_MASK

isa 二进制:                0000 0000 0000 0000 

                                    0000 0001 1010  0001 

                                    0000 0000 0000 1010 

                                    1000 1110 0010 0001

isa&ISA_MASK二进制:0000 0000 0000 0000

                                     0000 0000 0000 0001

                                     0000 0000 0000 1010 

                                     1000 1110 0010 0000

上述二进制的加粗部分说明的是取出shiftcls33 位数据,可以发现对象isa的33 为和isa&ISA_MASK的加粗33位相同的,代表这个shiftcls存储的就是类对象地址或者元类对象地址                                     

相信上面讲了这么多,大家对isa指针应该有了全新的认识,在__arm64__架构之后,isa指针不单单只存储了Class或者Meta-Class的地址,而是使用了共用体的方式存储了更多的信息,

其中shiftcls存储了Class或Meta-Class的地址,需要和ISA_MASK进行按位&操作才可以取出其内存地址值,通过上面的演示结果可以发现shiftcls和类对象地址存储的33位二进制完全相同,

extra_rc中的19位存储引用计数减1,实例当中person的引用计数位1,因此此时extra_rc的19位二进制存储的是0

magic中的6位在于判断对象是否未完成初始化,因为magic是(isa.magic is part of ISA_MAGIC_VALUE)的一部分   #define ISA_MAGIC_VALUE 0x000001a000000001ULL(二进制:0000 0000 0000 0000 0000 00001 1010 0000 0000 0000 0000 0000 0000 0000 0000 0001)粗色标注的6位就是magic的值:而例子中的person已经初始化了,所以magic存储的就是里面的6位011010

Runtime之isa详解_第29张图片
isa 初始化

nonpointer :0 代表普通指针,存储这Class,Meta-Class对象的内存地址,1代表表示优化后使用位域存储这更多的信息,这里肯定是使用优化后的isa,因此nonpointer的值肯定是1

这个时候可以看到另外一些字段has_assoc 和 weakly_referenced 值都为0,接着我们继续测试这两个字段,添加弱引用和关联对象,来观察一下has_assoc 和 weakly_referenced 变化

Runtime之isa详解_第30张图片
代码图
Runtime之isa详解_第31张图片
二进制图

二进制:0000 0000 0000 0000

              0000 0101 1010 0001

              0000 0000 0000 0000 

              1000 1110 0110  1111

可以看到后面3个都是1,说明测试结果正确

注意:只要设置过关联对象或者弱引用引用过的对象has_assoc和weakly_referenced 的值就会变成1,不论之后是否将关联对象置为nil或者断开弱引用

如果没有设置过关联对象,对象释放时候会更快,这是因为对象在销毁时会判断是否有关联对象进而对对象释放,上对象释放的源码图:

Runtime之isa详解_第32张图片
对象销毁

可以添加微信一起交流学习:fslskz

你可能感兴趣的:(Runtime之isa详解)