我们知道,判定是不是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
存储这三个值:
@interface Person : 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
@interface Person ()
{
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
值后再打印也完全正确.我们使用掩码再继续优化一下上面的写法,把位运算的值宏定义一下:
#define tallMask 0b00000001
#define richMask 0b00000010
#define handsomeMask 0b00000100
Mask 掩码,一般用来按位与运算,最好用括号括起来,怕影响到运算结果
继续优化:
#define tallMask (1<<0) //1 左移 0 位
#define richMask (1<<1)// 1 左移 1 位
#define handsomeMask (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{
char tall : 1;//位域 占1位
char rich : 1;
char handsome : 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
.关于这个结论我们也可以验证一下:
所以我们可以还和刚才一样使用
!!
取出真实布尔值,也可以把char tall : 1
改成char tall : 2
,让位域占两位.
struct{
char tall : 2;//位域 占1位
char rich : 2;
char handsome : 2;
}_tallRichHandsome;
运行结果如下:
周星驰高吗?1
富吗?0
帅吗0
发现这样就不是-1
了,其实这就是位域的一个小特点,我们取出tall
的值01
,存放到一个字节0000 0000
中,结果就是0000 0001
,它会把符号位0
补充到其他6位中,结果就是正常的.
- 到目前为止我们尝试了两种方法达到这种目的,一种是使用位运算,另一种是使用结构体的位域.那我们能不能综合前两种方法,对结构体进行位运算呢?我们来试试:
如图所示,结构体根本就无法进行位运算,那怎么办呢?我们可以参考一下苹果大大优化isa
指针的做法,使用共用体(union
):
union{
char bits;
struct{
char tall : 1;//位域 占1位
char rich : 1;
char handsome : 1;
};
}_tallRichHandsome;
我们运行一下发现完全正常,我们把结构体删掉在运行一下:
union{
char bits;
}_tallRichHandsome;
发现也完全正常,其实这里的结构体完全就是增加代码的可读性.这种做法其实就是将位运算和结构体的位域结合在一起,利用结构体的位域增加可读性,利用位运算达到想取哪里去哪里的目的.
下面我们详细介绍一下共用体(union
),我们将结构体struct
和共用体union
对比介绍.比如说现在有一个结构体Date
和共用体Date
:
struct{
int year;
int month;
int day;
}Date;
union{
int year;
int month;
int day;
}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];
它的内部是怎么样处理我们传进的多个值得呢?我们可以模仿一下:
typedef enum{
Monday = 1, //0b 0000 0001
Tuesday = 2, //0b 0000 0010
Wedensday = 4, //0b 0000 0100
Thursday = 8, //0b 0000 1000
Friday = 16, //0b 0001 0000
Saturday = 32, //0b 0010 0000
Sunday = 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-31 11:00:59.609826+0800 MultiThread[2195:466594] Saturday 打游戏
2019-01-31 11:00:59.609980+0800 MultiThread[2195:466594] Sunday 打游戏
这样就实现了我们的需求,我们把[self setWeek:Saturday | Sunday];
改为[self setWeek:Saturday + Sunday];
看看效果:
[self setWeek:Saturday + Sunday];
===============================================
- (void)setWeek:(Week)week{
if (week & Saturday) {
NSLog(@"Saturday 打游戏");
}
if (week & Sunday) {
NSLog(@"Sunday 打游戏");
}
}
===============================================
2019-01-31 11:12:10.965097+0800 MultiThread[2291:476407] Saturday 打游戏
2019-01-31 11:12:10.965285+0800 MultiThread[2291:476407] Sunday 打游戏
结果也完全一样,说明+
和|
在这里是等价的,但是要注意:只有当他们的初始值是2的n次方的时候才能使用+
号,一般不建议使用+
,这样会显得你很low
所以我们还是使用|
苹果的源码也是这样来设计实现的,我们看看:
NSKeyValueObservingOptionNew = 0x01, // 1
NSKeyValueObservingOptionOld = 0x02,// 2
NSKeyValueObservingOptionInitial = 0x04,// 4
NSKeyValueObservingOptionPrior = 0x08,//8
ok,前面讲了这么多的掩码,位域,共用体等等其实都是为了铺垫,都是为了引出最终的 BOSS ==> isa,现在我们就来仔细看看isa指针.
首先查看isa
源码:
# if __arm64__
# define ISA_MASK 0x0000000ffffffff8ULL
# define ISA_MAGIC_MASK 0x000003f000000001ULL
# define ISA_MAGIC_VALUE 0x000001a000000001ULL
struct {
uintptr_t nonpointer : 1;
uintptr_t has_assoc : 1;
uintptr_t has_cxx_dtor : 1;
uintptr_t shiftcls : 33; // MACH_VM_MAX_ADDRESS 0x1000000000
uintptr_t magic : 6;
uintptr_t weakly_referenced : 1;
uintptr_t deallocating : 1;
uintptr_t has_sidetable_rc : 1;
uintptr_t extra_rc : 19;
# define RC_ONE (1ULL<<45)
# define RC_HALF (1ULL<<18)
};
现在带入到项目中验证一下,注意验证的时候要使用真机,因为模拟器中存储的位置和真机的不一样.
首先我们打印出一个
ViewController
的内存地址:然后用计算器查看这个地址的二进制:
我们对比上面的注释图查看分析一下这个二进制:
- 第0位也就是最后边的1是
nonpointer
的值,如果为0表示这个isa就是一个普通的指针,值存储着类对象或者元类对象的内存地址;如果为1则表示这个isa是经过优化过的,使用位域存储更多信息.
...
剩下的信息我就不一一对比了,有兴趣的可以自己对比试验.