IOS的isa指针优化

我们知道,判定是不是OC对象的本质就是看是否含有isa指针,在ARM64架构之前,objc_object的isa指针就是一个class类型,存储的就是一个指针,而ARM64系统之后,对isa进行了优化,变成了一个共用体(union),还是用位域来存储更多信息.


共用体

今天我们就来研究一下共同体(union)和位域.

我们先通过一个小场景来开始今天的内容,我们先创建一个Person类,类中有三个BOOL类型的属性:tall , rich ,handsome,分别表示高,富,帅.

然后通过class_getInstanceSize查看这个类对象占用多少字节,发现打印输出是:16.为什么是16个字节呢?因为3个BOOL类型的属性占用3个字节,isa指针占用8个字节,一共占用11个字节,再内存对齐以后,就是16个字节.

这样我们就会发现一个问题,三个BOOL类型的属性占用了3个字节,其实BOOL类型属性本质就是0 或 1,只占一位,也就是说3个BOOL属性放在一个字节就可以搞定.比如说有一个字节的内存0x0000 0000我们用最后3位分别存放高,富,帅,如图所示:

❓思考一下:怎样才能做到只用一位去存放三个属性呢❓

只能通过位运算做到了.我们先把属性相关的代码删掉,(因为如果添加了属性就会自动生成setter,getter方法) 再手动添加setter,getter方法.然后再在.m中声明一个成员变量_tallRichHandsome存储这三个值:

@interfacePerson:NSObject//@property (nonatomic,assign)BOOL tall;//@property (nonatomic,assign)BOOL rich;//@property (nonatomic,assign)BOOL handSome;- (void)setTall:(BOOL)tall;- (void)setRich:(BOOL)rich;- (void)setHandSome:(BOOL)handsome;- (BOOL)isTall;- (BOOL)isRich;- (BOOL)isHandSome;@end@interfacePerson(){char_tallRichHandsome;//0b00000000}@end

取值用&运算符.按位与是两个为 1 ,结果才为1.如果我们想要获取某一位的值,只需要把那一位设置成1,其他位设置为0,就可以取出特定位的值.

所以我们只需要在getter方法中按位与一个特定的值即可,比如我们想要获取tall,只需按位与0b 0000 0001;获取rich,就按位与0b 0000 0010,但是这样运算得出的结果并不是一个布尔值,而我们是想要BOOL类型,所以我们可以使用return (BOOL)(_tallRichHandsome & 0b00000001)转换类型,也可以这样return !!(_tallRichHandsome & 0b00000001),使用两个!!获取真实布尔值.

- (BOOL)isTall{return!!(_tallRichHandsome &0b00000001);}

检验一下这种写法的效果,我们在Person类的init方法中给_tallRichHandsome赋值为0b00000101,代表高为1,富为0,帅为1,然后在.m中打印看看:

打印结果

可以看到结果完全正确,更改_tallRichHandsome值后再打印也完全正确.我们使用掩码再继续优化一下上面的写法,把位运算的值宏定义一下:

#definetallMask 0b00000001#definerichMask 0b00000010#definehandsomeMask 0b00000100

Mask 掩码,一般用来按位与运算,最好用括号括起来,怕影响到运算结果

继续优化:

#definetallMask (1<<0)//1 左移 0 位#definerichMask (1<<1)// 1 左移 1 位#definehandsomeMask (1<<2)// 1 左移 2 位

刚才验证了取值,接下来研究一下如何赋值.赋值分为两种情况:如果赋值YES,就使用 按位或运算符(|).按位或表示一个为 1 ,结果就为 1 .如果赋值NO,就把目标位设置为 0 ,其他位全设置YES.

比如赋值为YES:比如原始值为0b 0000 0101,标识高:YES , 帅:YES.现在要把富也设置为YES,也就是0b0000 0010,其他位置不变,就要使用按位或|:

如果赋值为NO,比如原始值为0b 0000 0111,标识高:YES ,富:YES 帅:YES.现在要把高也设置为NO,其他不变.结果就是0b0000 0110,那就应该把目标位设置为0,其他位设置为1,使用按位取反运算符~,掩码就应该是0b1111 1110,然后再按位与&:

代码如下:

- (void)setRich:(BOOL)rich{if(rich) {        _tallRichHandsome |= richMask;    }else{        _tallRichHandsome &= ~richMask;    }}

测试结果完全正常:

现在就能满足我们刚开始的目的了,但是这种做法不好扩展也不利于阅读,我们继续完善一下,使用位域这种技术.

我们把刚才的代码更改一下,把char _tallRichHandsome更改为

struct{chartall :1;//位域 占1位charrich :1;charhandsome :1;    }_tallRichHandsome;

注意:char tall : 1是位域的格式,表示只占一位

相应的setter , getter方法更改如下:

- (void)setHandSome:(BOOL)handsome{    _tallRichHandsome.handsome = handsome;}- (BOOL)isHandSome{return_tallRichHandsome.handsome;}

然后运行一下查看结果:

可以看到给tall赋值后的确发生了变化,但是为什么是-1呢?我们刚才给tall赋的值,然后结构体中的顺序是tall , rich , handsome,内存中的位置会按照结构体中的顺序从右往左开始存放,也就是现在现在内存中的值应该是0b 0000 0001,ok,我们来验证一下:

而01的二进制就是0000 0001,完全符合我们刚在的结论,那为什么打印出来的确是-1呢?

我们看看getter方法代码:

- (BOOL)isTall{return_tallRichHandsome.tall;    }

getter方法返回的是BOOL类型,占用一个字节(8位),而我们_tallRichHandsome.tall取出来的却是一位,把一位的1,存放到8位的地址中0b 0000 0000,1 放在最后一位,前七位全补成1,结果就成了0b1111 1111,就成了无符号的255,有符号的-1.关于这个结论我们也可以验证一下:

的确是 255

所以我们可以还和刚才一样使用!!取出真实布尔值,也可以把char tall : 1改成char tall : 2,让位域占两位.

struct{chartall :2;//位域 占1位charrich :2;charhandsome :2;    }_tallRichHandsome;运行结果如下:周星驰高吗?1富吗?0帅吗0

发现这样就不是-1了,其实这就是位域的一个小特点,我们取出tall的值01,存放到一个字节0000 0000中,结果就是0000 0001,它会把符号位0补充到其他6位中,结果就是正常的.

到目前为止我们尝试了两种方法达到这种目的,一种是使用位运算,另一种是使用结构体的位域.那我们能不能综合前两种方法,对结构体进行位运算呢?我们来试试:

如图所示,结构体根本就无法进行位运算,那怎么办呢?我们可以参考一下苹果大大优化isa指针的做法,使用共用体(union):

union{charbits;struct{chartall :1;//位域 占1位charrich :1;charhandsome :1;        };    }_tallRichHandsome;

我们运行一下发现完全正常,我们把结构体删掉在运行一下:

union{charbits;    }_tallRichHandsome;

发现也完全正常,其实这里的结构体完全就是增加代码的可读性.这种做法其实就是将位运算和结构体的位域结合在一起,利用结构体的位域增加可读性,利用位运算达到想取哪里去哪里的目的.

下面我们详细介绍一下共用体(union),我们将结构体struct和共用体union对比介绍.比如说现在有一个结构体Date和共用体Date:

struct{intyear;intmonth;intday;    }Date;union{intyear;intmonth;intday;    }Day;

他们的内存结构如图所示:

可以看到,结构体的内存都是独立的,每个成员占用4个字节,一共占用12个字节;而共用体的内存是连续的,所有成员公用4个字节的内存,共用体内存的大小取决于最大的成员所分配的内存.

下图就证明了共用体的成员是共用一块内存的,我们给year赋值,然后打印month,结果值确是year的值:

总结:在ARM64位之前isa就是个普通的指针,实例对象的isa指向类对象,类对象的isa指向实例元类对象,在ARM64位之后,isa进行了优化,采取了共用体的结构,使用64位的内存数据存储更多的信息,其中的33位存储具体的地址值.

关于位运算的一些扩展

我们在项目中肯定用过 KVO,[self addObserver:self forKeyPath:@"view" options:NSKeyValueObservingOptionNew | NSKeyValueObservingOptionOld context:nil];它的内部是怎么样处理我们传进的多个值得呢?我们可以模仿一下:

typedefenum{    Monday =1,//0b 0000 0001Tuesday =2,//0b 0000 0010Wedensday =4,//0b 0000 0100Thursday =8,//0b 0000 1000Friday =16,//0b 0001 0000Saturday =32,//0b 0010 0000Sunday =64,//0b 0100 0000}Week;

我们定义一个Week类型的结构体,周一 至 周日,并设置初始值,大家可以看到他们的初始值是有规律的,分别是2的0次方,1次方,2次方...6次方.对应的二进制也是1<<0,1<<1 ... 1<<6.然后我们在定义一个方法- (void)setWeek:(Week)week再调用这个方法[self setWeek:Saturday | Sunday];在这个方法内判断如果是周末就打印 打游戏,是工作日就打印 敲代码.怎么实现呢?

首先我们分析一下[self setWeek:Saturday | Sunday];我们知道或运算是一个为1结果就为1,所以Saturday | Sunday结果应该是:

  0010 0000

| 0100 0000

---------------

  0110 0000

然后我们再用这个结果0110 0000按位与上 Saturday , Sunday,如果结果不为0,就说明符合条件:

- (void)setWeek:(Week)week{if(week & Saturday) {NSLog(@"Saturday 打游戏");    }if(week & Sunday) {NSLog(@"Sunday 打游戏");    }}===============================================2019-01-3111:00:59.609826+0800MultiThread[2195:466594] Saturday 打游戏2019-01-3111:00:59.609980+0800MultiThread[2195:466594] Sunday 打游戏

这样就实现了我们的需求,我们把[self setWeek:Saturday | Sunday];改为[self setWeek:Saturday + Sunday];看看效果:

[selfsetWeek:Saturday + Sunday];===============================================- (void)setWeek:(Week)week{if(week & Saturday) {NSLog(@"Saturday 打游戏");    }if(week & Sunday) {NSLog(@"Sunday 打游戏");    }}===============================================2019-01-3111:12:10.965097+0800MultiThread[2291:476407] Saturday 打游戏2019-01-3111:12:10.965285+0800MultiThread[2291:476407] Sunday 打游戏

结果也完全一样,说明+和|在这里是等价的,但是要注意:只有当他们的初始值是2的n次方的时候才能使用+号,一般不建议使用+,这样会显得你很low所以我们还是使用|

苹果的源码也是这样来设计实现的,我们看看:

NSKeyValueObservingOptionNew=0x01,// 1NSKeyValueObservingOptionOld=0x02,// 2NSKeyValueObservingOptionInitial=0x04,// 4NSKeyValueObservingOptionPrior=0x08,//8

ok,前面讲了这么多的掩码,位域,共用体等等其实都是为了铺垫,都是为了引出最终的 BOSS  ==> isa,现在我们就赖仔细看看isa指针.

首先查看isa源码:

#if__arm64__#defineISA_MASK        0x0000000ffffffff8ULL#defineISA_MAGIC_MASK  0x000003f000000001ULL#defineISA_MAGIC_VALUE 0x000001a000000001ULLstruct{uintptr_tnonpointer        :1;uintptr_thas_assoc        :1;uintptr_thas_cxx_dtor      :1;uintptr_tshiftcls          :33;// MACH_VM_MAX_ADDRESS 0x1000000000uintptr_tmagic            :6;uintptr_tweakly_referenced :1;uintptr_tdeallocating      :1;uintptr_thas_sidetable_rc  :1;uintptr_textra_rc          :19;#defineRC_ONE  (1ULL<<45)#defineRC_HALF  (1ULL<<18)};

对应注解

现在带入到项目中验证一下,注意验证的时候要使用真机,因为模拟器中存储的位置和真机的不一样.

首先我们打印出一个ViewController的内存地址:

然后用计算器查看这个地址的二进制:

我们对比上面的注释图查看分析一下这个二进制:

第0位也就是最后边的1是nonpointer的值,如果为0表示这个isa就是一个普通的指针,值存储着类对象或者元类对象的内存地址;如果为1则表示这个isa是经过优化过的,使用位域存储更多信息.

...

剩下的信息我就不一一对比了,有兴趣的可以自己对比试验

转自:https://www.jianshu.com/p/e78a858ea7cf

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