之前在OC底层原理(二)这章内容中有讲过 instance对象的isa & ISA_MASK 指向class对象,class对象的isa & ISA_MASK指向meta-class对象,但是并没有详细讲isa的内部结构
isa内部结构
在arm64架构之前,isa就是一个普通指针,存储着class对象或meta-class对象的地址
在arm64架构之后,苹果用union结构优化了isa指针,让isa能够存储更多的信息
在objc4源码中搜索isa_t,其结构如下
union isa_t {
isa_t() { }
isa_t(uintptr_t value) : bits(value) { }
uintptr_t bits;
private:
// Accessing the class requires custom ptrauth operations, so
// force clients to go through setClass/getClass by making this
// private.
Class cls;
public:
#if defined(ISA_BITFIELD)
struct {
ISA_BITFIELD; // defined in isa.h
};
bool isDeallocating() {
return extra_rc == 0 && has_sidetable_rc == 0;
}
void setDeallocating() {
extra_rc = 0;
has_sidetable_rc = 0;
}
#endif
void setClass(Class cls, objc_object *obj);
Class getClass(bool authenticated);
Class getDecodedClass(bool authenticated);
};
# if __arm64__
// ARM64 simulators have a larger address space, so use the ARM64e
// scheme even when simulators build for ARM64-not-e.
# if __has_feature(ptrauth_calls) || TARGET_OS_SIMULATOR
# define ISA_MASK 0x007ffffffffffff8ULL
# define ISA_MAGIC_MASK 0x0000000000000001ULL
# define ISA_MAGIC_VALUE 0x0000000000000001ULL
# define ISA_HAS_CXX_DTOR_BIT 0
# define ISA_BITFIELD \
uintptr_t nonpointer : 1; \
uintptr_t has_assoc : 1; \
uintptr_t weakly_referenced : 1; \
uintptr_t shiftcls_and_sig : 52; \
uintptr_t has_sidetable_rc : 1; \
uintptr_t extra_rc : 8
# define RC_ONE (1ULL<<56)
# define RC_HALF (1ULL<<7)
# else
# define ISA_MASK 0x0000000ffffffff8ULL
# define ISA_MAGIC_MASK 0x000003f000000001ULL
# define ISA_MAGIC_VALUE 0x000001a000000001ULL
# define ISA_HAS_CXX_DTOR_BIT 1
# define ISA_BITFIELD \
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 unused : 1; \
uintptr_t has_sidetable_rc : 1; \
uintptr_t extra_rc : 19
# define RC_ONE (1ULL<<45)
# define RC_HALF (1ULL<<18)
# endif
# elif __x86_64__
# define ISA_MASK 0x00007ffffffffff8ULL
# define ISA_MAGIC_MASK 0x001f800000000001ULL
# define ISA_MAGIC_VALUE 0x001d800000000001ULL
# define ISA_HAS_CXX_DTOR_BIT 1
# define ISA_BITFIELD \
uintptr_t nonpointer : 1; \
uintptr_t has_assoc : 1; \
uintptr_t has_cxx_dtor : 1; \
uintptr_t shiftcls : 44; /*MACH_VM_MAX_ADDRESS 0x7fffffe00000*/ \
uintptr_t magic : 6; \
uintptr_t weakly_referenced : 1; \
uintptr_t unused : 1; \
uintptr_t has_sidetable_rc : 1; \
uintptr_t extra_rc : 8
# define RC_ONE (1ULL<<56)
# define RC_HALF (1ULL<<7)
# else
# error unknown architecture for packed isa
# endif
// SUPPORT_PACKED_ISA
#endif
精简一下代码如下
union isa_t {
uintptr_t bits;
struct {
//arm64
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 unused : 1; \
uintptr_t has_sidetable_rc : 1; \
uintptr_t extra_rc : 19
};
};
union 共用体
那么什么是共用体呢,就是共用体内部的成员变量都共享一块内存区域
union testUnion {
int a;
int b;
int c;
};
struct testStruct {
int a;
int b;
int c;
};
我们通过union和struct来做对比
struct和union内存结构示意图
我们通过代码来验证下
创建一个命令行项目,在main函数中写入如下代码,并运行
union testUnion {
int a;
int b;
int c;
};
struct testStruct {
int a;
int b;
int c;
};
int main(int argc, const char * argv[]) {
@autoreleasepool {
// insert code here...
union testUnion testUnion;
testUnion.a = 10;
testUnion.b = 11;
testUnion.c = 12;
NSLog(@"union : %d %d %d", testUnion.a, testUnion.b, testUnion.c);
struct testStruct testStruct;
testStruct.a = 10;
testStruct.b = 11;
testStruct.c = 12;
NSLog(@"struct : %d %d %d", testStruct.a, testStruct.b, testStruct.c);
}
return 0;
}
输出结果如下
可以看到struct的存储的值互不影响,而union里的a、b的值被c的值给覆盖了
我们回到isa_t的结构中来看
union isa_t {
uintptr_t bits;
struct {
//arm64
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 unused : 1; \
uintptr_t has_sidetable_rc : 1; \
uintptr_t extra_rc : 19
};
};
可以看出 uintptr_t bits 和 struct 共用一个内存,那么struct 内部又是什么呢
这里要引申出另一个概念,位域
位域
在了解位域之前我们先设想一个场景,我们需要存储三个字段,高富帅,bool类型
按照我们原来的写法我们会申明三个bool属性来保存
@interface ZJPerson : NSObject
@property(nonatomic, assign) bool isTall;
@property(nonatomic, assign) bool isHandsome;
@property(nonatomic, assign) bool isRich;
@end
本来简简单单存三个bool变量,用了三个字节有点浪费内存
简简单单的0和1可以用1个字节的3bit来存储,这就涉及位域数据结构了
位域的写法和结构体类似
struct testWeiYu {
char a : 1;
char b : 1;
char c : 1;
};
这代表着a占用1bit,不是字节(一字节等于8bit),b占用1bit,c占用1bit
比如testWeiYu分配了一个字节的内存,其内存示意图如下
我们通过代码来验证下
struct testWeiYu {
char a : 1;
char b : 1;
char c : 1;
};
int main(int argc, const char * argv[]) {
@autoreleasepool {
// insert code here...
struct testWeiYu testWeiYu;
testWeiYu.a = 0;
testWeiYu.b = 1;
testWeiYu.c = 1;
NSLog(@"weiYu : %d %d %d", !!testWeiYu.a, !!testWeiYu.b, !!testWeiYu.c);
}
return 0;
}
然后在NSLog处打上断点,运行
然后在控制台查看testWeiYu的内存地址,再窥探这个地址存储的数据
使用如下代码查看其地址
p/x &(testWeiYu)
其输出如下
然后再用如下代码窥探其存储数据
x 0x00007ffeefbff528
其输出如下
06即为其存储的数据
我们讲0x06转换成二进制数据如下
可以看到存储的值为0b110
按照上面的内存示意图就是
我们看下最后的输出
可以看到确实实现了1个字节存储3个bool变量
另外细心的朋友可能也看到了NSLog里的两个!!
NSLog(@"weiYu : %d %d %d", !!testWeiYu.a, !!testWeiYu.b, !!testWeiYu.c);
为什么要这样写呢
这又要涉及另外一个概念了,原码、反码和补码
原码、反码、补码
原码
原码就是第一位为符号位,0代表正,1代表负,别的位表示数值
比如+1原码为
0000 0001
-1原码为
1000 0001
所以8位2进制的原码取值范围为[-127, +127]
但是如果计算(+1) + (-1)的话,原码计算结果如下
0000 0001 + 1000 0001 = 1000 0002
结果为-2,所以基于这种情况,反码被发明了出来
反码
如果为正数,原码与反码一致
如果为负数,反码为原码除符号位外每一位取反
反码就是在原码的基础上,每一位取反
如原码中-0的表示为
1000 0000
反码为
1111 1111
原码中+0的表示为
0000 0000
反码为
0000 0000
原码中+1的表示为
0000 0001
反码为
0000 0001
原码中-1的表示为
1000 0001
反码为
1111 1110
所以(+1) + (-1)等于-0
1000 0001 + 1111 1110 = 1111 1111
虽然反码解决了正负相加等于0的问题,却存在两个0,+0和-0
所以,再反码的基础上又提出了补码的概念
补码
如果为正数,反码与补码一致
如果为负数,补码为反码+1,并丢弃最高位
反码中-0表示为
1111 1111
补码中-0表示为
1111 1111 + 0000 0001 = 1 0000 0000
丢弃最高位为
0000 0000
所以-0和+0表示一样
原码中-1的表示为
1000 0001
反码为
1111 1110
补码为
1111 1111
计算机采用的是补码来存储值
回到主题
NSLog(@"weiYu : %d %d %d", !!testWeiYu.a, !!testWeiYu.b, !!testWeiYu.c);
为什么要这样写呢?
我们从反面角度来考虑,如果不这么写会出现什么情况
NSLog(@"weiYu : %d %d %d", testWeiYu.a, testWeiYu.b, testWeiYu.c);
我们运行看下结果
为什么会打印-1呢?
因为char b是有符号类型,而char b又只占用了1位,所以会将1当作符号位负数来处理,计算机存储值用的是补码,所以存储的值为1取反再加上1,最后结果还是1,再加上符号位就打印出-1
我们也可以通过加a、b、c申明为无符号类型来解决这个问题
struct testWeiYu {
unsigned char a : 1;
unsigned char b : 1;
unsigned char c : 1;
};
共用体+位域
在了解了共用体和位域的概念后,我们回过头看isa_t的结构
union isa_t {
uintptr_t bits;
struct {
//arm64
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 unused : 1; \
uintptr_t has_sidetable_rc : 1; \
uintptr_t extra_rc : 19
};
};
可以看到isa其实就是运用了共用体+位域的数据结构来做的优化
那么这种结构的值的存与取又如何实现呢?
我们用代码来实现
申明一个ZJPerson类,它需要存储三个bool变量isTall,isRich,isHandsome,使用共用体+位域技术来实现存值与取值
//.h
@interface ZJPerson : NSObject
- (void)setTall:(BOOL)tall;
- (BOOL)tall;
- (void)setRich:(BOOL)rich;
- (BOOL)rich;
- (void)setHandsome:(BOOL)handsome;
- (BOOL)handsome;
@end
//.m
@interface ZJPerson()
{
union man {
char bits;
struct {
unsigned char isTall;
unsigned char isRich;
unsigned char isHandsome;
};
}man;
}
@end
@implementation ZJPerson
- (void)setTall:(BOOL)tall {
}
- (BOOL)tall {
return NO;
}
- (void)setRich:(BOOL)rich {
}
- (BOOL)rich {
return NO;
}
- (void)setHandsome:(BOOL)handsome {
}
- (BOOL)handsome {
return NO;
}
@end
我们首先考虑取值
比如tall为YES,rich为NO,handSome为YES,那么共用体存的值应该如下
0000 0101
想要取tall的话,需要用这个值& 0000 0001
计算过程如下
0000 0101
&0000 0001
=0000 0001
同理,取rich,需要用这个值& 0000 0010
同理,取handSome,需要用这个值& 0000 0100
可以看到,取最右边的一位需要&(1<<0)
取右边的第二位需要&(1<<1)
取右边的第三位需要&(1<<2)
那么我们来更新.m文件中的代码,申明三位掩码来分别取tall,rich和handsome的值
#define ZJTallMask (1<<0)
#define ZJRichMask (1<<1)
#define ZJHandSome (1<<2)
@interface ZJPerson()
{
union man {
char bits;
struct {
unsigned char isTall;
unsigned char isRich;
unsigned char isHandsome;
};
}man;
}
@end
@implementation ZJPerson
- (void)setTall:(BOOL)tall {
}
- (BOOL)tall {
return !!(man.bits & ZJTallMask);
}
- (void)setRich:(BOOL)rich {
}
- (BOOL)rich {
return !!(man.bits & ZJRichMask);
}
- (void)setHandsome:(BOOL)handsome {
}
- (BOOL)handsome {
return !!(man.bits & ZJHandSome);
}
@end
这样,我们取值就完成了
为什么要加上!!,是因为
tall值取出来是0000 0001,转换成10进制就是1
rich值取出来是0000 0010,转换成10进制就是2
handsome值取出来是0000 0100,转换成10进制就是4
取出来的值并不是bool类型,而取两次反就可以得到正确的bool值
比如拿handsome的值4来做例子,!4就是0,!0就是1,1就是YES
再比如拿0来做例子,!0就是1,!1就是0,0就是NO
通过两次取反运算就可以得到我们想要的bool值
我们继续研究存值
如果我们要将tall的值设置为YES
就需要将共用体的值 | ZJTallMask
其运算过程如下
//原来tall值为NO
0000 0110
| 0000 0001
= 0000 0111
//原来tall值为YES
0000 0111
| 0000 0001
= 0000 0111
如果我们要将tall的值设置为NO,那么|运算则不能完成这个任务了
那么要怎么实现存值为NO呢?
首先,我们需要将掩码按位取反,然后用共用体的值&这个取反的值就可以了
//将掩码按位取反
~ZJTallMask//值为1111 1110
//然后再做&运算
//原来tall值为NO
0000 0110
| 1111 1110
= 0000 0110
//原来tall值为YES
0000 0111
| 1111 1110
= 0000 0111
这样NO的存储也完成了
我们完善代码如下
#define ZJTallMask (1<<0)
#define ZJRichMask (1<<1)
#define ZJHandSome (1<<2)
@interface ZJPerson()
{
union man {
char bits;
struct {
unsigned char isTall;
unsigned char isRich;
unsigned char isHandsome;
};
}man;
}
@end
@implementation ZJPerson
- (void)setTall:(BOOL)tall {
if (tall) {
man.bits |= ZJTallMask;
}else {
man.bits &= ~ZJTallMask;
}
}
- (BOOL)tall {
return !!(man.bits & ZJTallMask);
}
- (void)setRich:(BOOL)rich {
if (rich) {
man.bits |= ZJRichMask;
}else {
man.bits &= ~ZJRichMask;
}
}
- (BOOL)rich {
return !!(man.bits & ZJRichMask);
}
- (void)setHandsome:(BOOL)handsome {
if (handsome) {
man.bits |= ZJHandSome;
}else {
man.bits &= ~ZJHandSome;
}
}
- (BOOL)handsome {
return !!(man.bits & ZJHandSome);
}
@end
这样,通过共用体+位域实现三个bool变量的存取功能就完成了
我们测试一下
int main(int argc, const char * argv[]) {
@autoreleasepool {
// insert code here...
ZJPerson *person = [[ZJPerson alloc]init];
person.tall = YES;
person.rich = YES;
person.handsome = YES;
NSLog(@"tall:%d rich:%d handsome:%d", person.tall, person.rich, person.handsome);
}
return 0;
}
之前我们说的arm64之后需要isa指针&Mask获取class对象地址或者meta-class对象地址
我们看看掩码的值
define ISA_MASK 0x0000000ffffffff8ULL
将其转换成2进制
再看isa_t结构
union isa_t {
uintptr_t bits;
struct {
//arm64
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 unused : 1; \
uintptr_t has_sidetable_rc : 1; \
uintptr_t extra_rc : 19
};
};
isa & Mask之后就是将shiftcls的值取出来,而shiftcls里存储的就是class对象、meta-class对象的地址
isa_t其他字段的释义
union isa_t {
uintptr_t bits;
struct {
//arm64
// 是否开启 isa 指针优化
uintptr_t nonpointer : 1; \
// 是否有设置过关联对象,如果没有,释放时会更快
uintptr_t has_assoc : 1; \
// 是否有 C++ 的析构函数(.cxx_destruct),如果没有,释放时会更快
uintptr_t has_cxx_dtor : 1; \
存储着Class、Meta-Class对象的内存地址信息
uintptr_t shiftcls : 33; /*MACH_VM_MAX_ADDRESS 0x1000000000*/ \
// 用于在调试时分辨对象是否未完成初始化
uintptr_t magic : 6; \
// 是否有被弱引用指向过,如果没有,释放时会更快
uintptr_t weakly_referenced : 1; \
uintptr_t unused : 1; \
// 引用计数器是否过大无法存储在 isa 中。如果为 1,那么引用计数会存储在一个叫 SideTable 的类的属性中
uintptr_t has_sidetable_rc : 1; \
// 里面存储的值是引用计数 - 1
uintptr_t extra_rc : 19
};
};