1、isa结构

在arm64架构之前,isa就是一个普通的指针,存储着Class、Meta-Class对象的内存地址,从arm64架构开始,对isa进行了优化,变成了一个共用体(union)结构,并使用位域来存储更多的信息。
查看 objc源码 objc-private.h,isa共用体结构如下:

  1. // isa共用体(arm64&真机,简化后)
  2. union isa_t {
  3. uintptr_t bits;
  4. Class cls;
  5. struct {
  6. uintptr_t nonpointer : 1; \
  7. uintptr_t has_assoc : 1; \
  8. uintptr_t has_cxx_dtor : 1; \
  9. uintptr_t shiftcls : 33; /*MACH_VM_MAX_ADDRESS 0x1000000000*/ \
  10. uintptr_t magic : 6; \
  11. uintptr_t weakly_referenced : 1; \
  12. uintptr_t unused : 1; \
  13. uintptr_t has_sidetable_rc : 1; \
  14. uintptr_t extra_rc : 19
  15. };
  16. };

2、位运算

2.1、位运算简介

使用位运算是为了节省空间,比如一个bool类型变量占用一个字节内存,也就是8位(如:0b 0000 0101),每一位都是由0和1组成,那么如果每一位都能存储一个bool信息的话,就不用创建多个bool变量来存储信息了,也就节省了内存。
比如创建一个Person类,添加高、富、帅三个属性,这样三个属性占用了三个字节,是否可以把三个字节的信息节省为1个字节?

  1. @interface Person : NSObject
  2. @property (nonatomic, assign) BOOL tall;
  3. @property (nonatomic, assign) BOOL rich;
  4. @property (nonatomic, assign) BOOL handsome;
  5. @end

可以用位运算实现,比如字节a和字节b通过位运算得到新的字节

  1. 0b 0000 0001 //字节a,最后一位是1
  2. & 0b 0000 0011 //字节b,最后两位是1
  3. ----------------
  4. 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就可以保存在一个字节的数据当中:

  1. char _tallRichHandsome = 0b00000011;

用它的倒数第一位保存tall的信息,倒数第二位保存rich的信息,倒数第三位保存handsome的信息,三个属性的get方法可以写成:

  1. #define TallMask (1<<0)
  2. #define RichMask (1<<1)
  3. #define HandsomeMask (1<<2)
  4. - (BOOL)tall {
  5. return !!(_tallRichHandsome & TallMask);
  6. }
  7. - (BOOL)rich {
  8. return !!(_tallRichHandsome & RichMask);
  9. }
  10. - (BOOL)handsome {
  11. return !!(_tallRichHandsome & HandsomeMask);
  12. }

xxxxMask一般指的是掩码,用于做位运算。 1<<2代表1向左移动2位,(0b 0000 0100)

2.3、通过位运算实现set方法

要实现set方法,就需要更新_tallRichHandsome指定位的值,但又不能改变其他位的值。
如果设置的值是YES,可以通过按位或运算实现:

  1. 0b 0000 0010 //字节a,倒数第二位是1
  2. | 0b 0000 0001 //字节b,最后一位是1
  3. ----------------
  4. 0b 0000 0011 //字节d,最后两位是1

用0b 0000 0001和a做或运算,得到新的字节d,既保留了a其它位的值,又更新了a最后一位的值为1。
如果设置的值是NO,可以通过按位与运算实现:

  1. 0b 0000 0010 //字节a,倒数第二位是1
  2. & 0b 1111 1101 //字节b,倒数第二位是0
  3. ----------------
  4. 0b 0000 0000 //字节d,都是0

用0b 1111 1101和a做或运算,得到新的字节d,既保留了a其它位的值,又更新了a最后倒数第二位的值为0。
Person的set方法可以写成:

  1. - (void)setTall:(BOOL)tall {
  2. if (tall) {
  3. _tallRichHandsome |= TallMask;
  4. }else {
  5. _tallRichHandsome &= ~TallMask;
  6. }
  7. }
  8. - (void)setRich:(BOOL)rich {
  9. if (rich) {
  10. _tallRichHandsome |= RichMask;
  11. }else {
  12. _tallRichHandsome &= ~RichMask;
  13. }
  14. }
  15. - (void)setHandsome:(BOOL)handsome {
  16. if (handsome) {
  17. _tallRichHandsome |= HandsomeMask;
  18. }else {
  19. _tallRichHandsome &= ~HandsomeMask;
  20. }
  21. }

“~”代表按位取反(~0b 0000 0100 = 0b 1111 1011)

3、位域

结构体中有位域的功能,可以设定结构体内部成员占几位,比如:

  1. struct {
  2. char tall : 1;
  3. char rich : 1;
  4. char handsome : 1;
  5. } _tallRichHandsome;

冒号后面的数字表示tall、rich、handsome各占1位。在内存中写在上面的成员会在_tallRichHandsome所占字节的最右边。

利用位域可以优化Person中的set和get方法

  1. - (void)setTall:(BOOL)tall {
  2. _tallRichHandsome.tall = tall;
  3. }
  4. - (void)setRich:(BOOL)rich {
  5. _tallRichHandsome.rich = rich;
  6. }
  7. - (void)setHandsome:(BOOL)handsome {
  8. _tallRichHandsome.handsome = handsome;
  9. }
  10. - (BOOL)tall {
  11. return !!_tallRichHandsome.tall;
  12. }
  13. - (BOOL)rich {
  14. return !!_tallRichHandsome.rich;
  15. }
  16. - (BOOL)handsome {
  17. return !!_tallRichHandsome.handsome;
  18. }

4、共用体

共用体结构如下:

  1. union {
  2. char bits;
  3. struct {
  4. char tall : 1;
  5. char rich : 1;
  6. char handsome : 1;
  7. };
  8. } _tallRichHandsome;

共用体中bits和结构体都共用1个字节的内存,bits是负责保存各个属性的值,使用位运算进行保存和读取。结构体只是为了提高可读性,没有读写逻辑。
使用共用体Person的set和get方法可以修改为:

  1. - (void)setHandsome:(BOOL)handsome {
  2. if (handsome) {
  3. _tallRichHandsome.bits |= HandsomeMask;
  4. }else {
  5. _tallRichHandsome.bits &= ~HandsomeMask;
  6. }
  7. }
  8. - (BOOL)handsome {
  9. return !!(_tallRichHandsome.bits & 4);//0b00000100
  10. }

5、总结

了解位运算、位域、共用体后,就可以知道isa共用体内的bits是用于保存各种信息的,struct是为了提高代码可读性,列举了isa保存的各种信息,isa共用体可以简写为:

  1. // isa共用体
  2. union isa_t {
  3. uintptr_t bits;
  4. Class cls;
  5. struct {
  6. uintptr_t nonpointer : 1; \
  7. uintptr_t has_assoc : 1; \
  8. uintptr_t has_cxx_dtor : 1; \
  9. uintptr_t shiftcls : 33; /*MACH_VM_MAX_ADDRESS 0x1000000000*/ \
  10. uintptr_t magic : 6; \
  11. uintptr_t weakly_referenced : 1; \
  12. uintptr_t unused : 1; \
  13. uintptr_t has_sidetable_rc : 1; \
  14. uintptr_t extra_rc : 19
  15. };
  16. };

之前讲解过isa指针是通过&ISA_MASK之后才能得到Class对象的地址:
image.png

  1. # define ISA_MASK 0x0000000ffffffff8ULL

image.png
将ISA_MASK转成2进制可以看出,从第3位开始总共有33个1,正好对应着isa共用体中的shiftcls,shiftcls存储了class的指针数据,也印证了class = isa&ISA_MASK。(可以发现类对象、元类对象地址值后三位都是0)。

Tips:正常无法拿到OC对象的isa指针地址,可以创建一个和OC对象底层结构一样的结构体,将OC对象转成结构体,再读取isa指针地址。