Runtime之isa详解

isa 概念

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

共用体概念

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

寻找真理

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

代码图

这个时候你通过终端打印,可以看到结果输出是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值:

char 声明图

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

初始化

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

tall,rich,handsome对应图

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

二进制的赋值操作

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

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

tall赋值操作

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

handsome赋值为0

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

set方法

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

二进制的取值操作

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

取值操作

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

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)

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

测试源码
没有加上!!

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

get方法纠正

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

位域

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

位域形式大概如下:

位域

set get方法实现如下:

set get方法

测试代码和结果如下:

测试结果图

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

终端调试图

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

结构体
位域

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

修改测试图

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

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

位域!!

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

共用体

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

共用体设计如下:

共用体

get 方法 set方法如下图:

get set

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

runtime-isa_t 源码

isa_t 共用体源码

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

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

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:

0x0000000ffffffff8ULL 二进制

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

实例证明

运行环境:__arm64__位架构

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

代码原图
终端输出图
二进制


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

isa 初始化

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

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

代码图
二进制图

二进制:0000 0000 0000 0000

              0000 0101 1010 0001

              0000 0000 0000 0000 

              1000 1110 0110  1111

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

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

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

对象销毁

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

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 202,607评论 5 476
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,047评论 2 379
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 149,496评论 0 335
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,405评论 1 273
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,400评论 5 364
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,479评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,883评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,535评论 0 256
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,743评论 1 295
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,544评论 2 319
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,612评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,309评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,881评论 3 306
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,891评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,136评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 42,783评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,316评论 2 342

推荐阅读更多精彩内容