1、isa结构
在arm64架构之前,isa就是一个普通的指针,存储着Class、Meta-Class对象的内存地址,从arm64架构开始,对isa进行了优化,变成了一个共用体(union)结构,并使用位域来存储更多的信息。
查看 objc源码 objc-private.h,isa共用体结构如下:
// isa共用体(arm64&真机,简化后)
union isa_t {
uintptr_t bits;
Class cls;
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 unused : 1; \
uintptr_t has_sidetable_rc : 1; \
uintptr_t extra_rc : 19
};
};
2、位运算
2.1、位运算简介
使用位运算是为了节省空间,比如一个bool类型变量占用一个字节内存,也就是8位(如:0b 0000 0101),每一位都是由0和1组成,那么如果每一位都能存储一个bool信息的话,就不用创建多个bool变量来存储信息了,也就节省了内存。
比如创建一个Person类,添加高、富、帅三个属性,这样三个属性占用了三个字节,是否可以把三个字节的信息节省为1个字节?
@interface Person : NSObject
@property (nonatomic, assign) BOOL tall;
@property (nonatomic, assign) BOOL rich;
@property (nonatomic, assign) BOOL handsome;
@end
可以用位运算实现,比如字节a和字节b通过位运算得到新的字节
0b 0000 0001 //字节a,最后一位是1
& 0b 0000 0011 //字节b,最后两位是1
----------------
0b 0000 0001 //字节c,最后一位是1
将字节里的每一位取出做&(与)运算,运算结果组成了新的字节c。
想把c转成bool类型可以进行!(取反)操作,比如0b 0000 0001转成10进制就是1,!(1)就是NO,!!(1)就是YES,即如果c有值(c != 0)那么!!c = YES,如果c没有值(c = 0)那么!!c = NO。
2.2、通过位运算实现get方法
Person的三个属性tall、rich、handsome就可以保存在一个字节的数据当中:
char _tallRichHandsome = 0b00000011;
用它的倒数第一位保存tall的信息,倒数第二位保存rich的信息,倒数第三位保存handsome的信息,三个属性的get方法可以写成:
#define TallMask (1<<0)
#define RichMask (1<<1)
#define HandsomeMask (1<<2)
- (BOOL)tall {
return !!(_tallRichHandsome & TallMask);
}
- (BOOL)rich {
return !!(_tallRichHandsome & RichMask);
}
- (BOOL)handsome {
return !!(_tallRichHandsome & HandsomeMask);
}
xxxxMask一般指的是掩码,用于做位运算。 1<<2代表1向左移动2位,(0b 0000 0100)
2.3、通过位运算实现set方法
要实现set方法,就需要更新_tallRichHandsome指定位的值,但又不能改变其他位的值。
如果设置的值是YES,可以通过按位或运算实现:
0b 0000 0010 //字节a,倒数第二位是1
| 0b 0000 0001 //字节b,最后一位是1
----------------
0b 0000 0011 //字节d,最后两位是1
用0b 0000 0001和a做或运算,得到新的字节d,既保留了a其它位的值,又更新了a最后一位的值为1。
如果设置的值是NO,可以通过按位与运算实现:
0b 0000 0010 //字节a,倒数第二位是1
& 0b 1111 1101 //字节b,倒数第二位是0
----------------
0b 0000 0000 //字节d,都是0
用0b 1111 1101和a做或运算,得到新的字节d,既保留了a其它位的值,又更新了a最后倒数第二位的值为0。
Person的set方法可以写成:
- (void)setTall:(BOOL)tall {
if (tall) {
_tallRichHandsome |= TallMask;
}else {
_tallRichHandsome &= ~TallMask;
}
}
- (void)setRich:(BOOL)rich {
if (rich) {
_tallRichHandsome |= RichMask;
}else {
_tallRichHandsome &= ~RichMask;
}
}
- (void)setHandsome:(BOOL)handsome {
if (handsome) {
_tallRichHandsome |= HandsomeMask;
}else {
_tallRichHandsome &= ~HandsomeMask;
}
}
“~”代表按位取反(~0b 0000 0100 = 0b 1111 1011)
3、位域
结构体中有位域的功能,可以设定结构体内部成员占几位,比如:
struct {
char tall : 1;
char rich : 1;
char handsome : 1;
} _tallRichHandsome;
冒号后面的数字表示tall、rich、handsome各占1位。在内存中写在上面的成员会在_tallRichHandsome所占字节的最右边。
利用位域可以优化Person中的set和get方法
- (void)setTall:(BOOL)tall {
_tallRichHandsome.tall = tall;
}
- (void)setRich:(BOOL)rich {
_tallRichHandsome.rich = rich;
}
- (void)setHandsome:(BOOL)handsome {
_tallRichHandsome.handsome = handsome;
}
- (BOOL)tall {
return !!_tallRichHandsome.tall;
}
- (BOOL)rich {
return !!_tallRichHandsome.rich;
}
- (BOOL)handsome {
return !!_tallRichHandsome.handsome;
}
4、共用体
共用体结构如下:
union {
char bits;
struct {
char tall : 1;
char rich : 1;
char handsome : 1;
};
} _tallRichHandsome;
共用体中bits和结构体都共用1个字节的内存,bits是负责保存各个属性的值,使用位运算进行保存和读取。结构体只是为了提高可读性,没有读写逻辑。
使用共用体Person的set和get方法可以修改为:
- (void)setHandsome:(BOOL)handsome {
if (handsome) {
_tallRichHandsome.bits |= HandsomeMask;
}else {
_tallRichHandsome.bits &= ~HandsomeMask;
}
}
- (BOOL)handsome {
return !!(_tallRichHandsome.bits & 4);//0b00000100
}
5、总结
了解位运算、位域、共用体后,就可以知道isa共用体内的bits是用于保存各种信息的,struct是为了提高代码可读性,列举了isa保存的各种信息,isa共用体可以简写为:
// isa共用体
union isa_t {
uintptr_t bits;
Class cls;
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 unused : 1; \
uintptr_t has_sidetable_rc : 1; \
uintptr_t extra_rc : 19
};
};
之前讲解过isa指针是通过&ISA_MASK之后才能得到Class对象的地址:
# define ISA_MASK 0x0000000ffffffff8ULL
将ISA_MASK转成2进制可以看出,从第3位开始总共有33个1,正好对应着isa共用体中的shiftcls,shiftcls存储了class的指针数据,也印证了class = isa&ISA_MASK。(可以发现类对象、元类对象地址值后三位都是0)。
Tips:正常无法拿到OC对象的isa指针地址,可以创建一个和OC对象底层结构一样的结构体,将OC对象转成结构体,再读取isa指针地址。