isa详解 & class 内部结构

isa、共用体、位域、class

Posted by miniLV on March 18, 2019

面试驱动技术合集(初中级iOS开发),关注仓库,及时获取更新 Interview-series

patrick-ward-503767-unsplash

isa详解

arm64架构之后,isa进行了优化,变成了一个共用体( union)结构,使用了位域的技术,可以存储更多的信息

  #define MNWhiteMask (1<<0) //Ox0000 0001
  #define MNRichMask (1<<1) //Ox0000 0010
  #define MNBeautyMask (1<<2) //Ox0000 0100
  
  
  @interface MNObject ()
  {
      //位域
      struct {
          char white : 1;
          char rich : 1;
          char beauty : 1;
      }_whiteRichBeauty;
  }
  @end

番外篇 - 假设一个程序员都喜欢对象,有白富美三个属性,能否用一个字节去存储呢?

具体实现

  @implementation MNGirl
  
  - (void)setWhite:(BOOL)white{
      _whiteRichBeauty.white = white;
  }
  - (void)setRich:(BOOL)rich{
      _whiteRichBeauty.rich = rich;
  }
  - (void)setBeauty:(BOOL)beauty{
      _whiteRichBeauty.beauty = beauty;
  }
  
  - (BOOL)white{
      return _whiteRichBeauty.white;
  }
  - (BOOL)rich{
      return _whiteRichBeauty.rich;
  }
  - (BOOL)beauty{
      return _whiteRichBeauty.beauty;
  }
  
  @end

使用情况

  MNGirl *girl = [MNGirl new];
  [girl setWhite:NO];
  [girl setRich:YES];
  [girl setBeauty:NO];
  
  NSLog(@"white = %d, rich = %d, beauty = %d", girl.white, girl.rich, girl.beauty);
  
  ------------------------------------------------------------------
  输出:
  white = 0, rich = -1, beauty = 0

white 和 beauty的值都是正确的,为啥rich 出错了?

涉及到符号位覆盖的问题!

  (lldb) p/x girl->_whiteRichBeauty
  ((anonymous struct)) $1 = (white = 0x00, rich = 0x01, beauty = 0x00)

赋值的时候,其实还是正确的,rich = 1,取值的时候,怎么出问题了呢?

这里假设结构体 _whiteRichBeauty 占8位,假设是0x 0000 0000

rich = 1,占1位,就是 0x1

要把 0x1 赋值到 0x 0000 0000,就要涉及到拷贝

0x的第一位是1,所以将1拷贝过去,最后变成 0x 1111 1111

image-20190311230151798

全是1,8个1 = 255 = 0xff

0xff 有符号 = 255,无符号 = -1,

所以这里输出-1 ==> 错误输出!

解决办法:

  {
      //位域
      struct {
          char white : 2;
          char rich : 2;
          char beauty : 2;
      }_whiteRichBeauty;
  }

一个占2位,三个属性合起来也才6位,1个字节还是可以放得下

然后现在 rich = 1,占2位,就是 0x01

rich的第一位是0,第二位才是1,所以将第一位0拷贝过去,最后变成 0x 0000 0001 = 结果!

image-20190311230354335

苹果的做法 -> 共用体:

里面的成员共用一块内存,和结构体不一样,存储采取位运算,结构体只是增加可读性

  @interface MNGirl ()
  {
      //共用体
      union {
          //存放所有数据
          char bits;
          
          //位域:增加可读性,可以省略
          struct {
              char white : 1;
              char rich : 1;
              char beauty : 1;
          };
      }_whiteRichBeauty;
      
  }
  @end
  
  //共用体
  union isa_t {
      isa_t() { }
      isa_t(uintptr_t value) : bits(value) { }
  
      Class cls;
      uintptr_t bits;
  #if defined(ISA_BITFIELD)
      struct {
          ISA_BITFIELD;  // defined in isa.h
      };
  #endif
  };
  ISA_BITFIELD = 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
  }

arm64架构下,isa不是直接指向class的地址,而且要&上一个MASK

shiftcls: class存放的地址,通过 & ISA_MASK ==> 取出表示 class地址的44位

这样其他位就还能存储数据了

释放更快的源码 - objc_destructInstance

  void *objc_destructInstance(id obj) 
  {
      if (obj) {
          // Read all of the flags at once for performance.
          //如果没有C++析构函数
          bool cxx = obj->hasCxxDtor();
          //没有设置过关联对象
          bool assoc = obj->hasAssociatedObjects();
  
          // This order is important.
          //移除操作
          if (cxx) object_cxxDestruct(obj);
          if (assoc) _object_remove_assocations(obj);
          obj->clearDeallocating();
      }
  
      return obj;
  }

Class内部结构

class_rw_t

rw –> readwrite 可读可写,可以修改

  struct class_rw_t {
  
      method_array_t methods;//方法列表
      property_array_t properties;//属性列表
      protocol_array_t protocols;//协议列表
      }

class_rw_t 中的 methods、properties、protocols 都是 二维数组 ,以methods为例

methods 里面是一组一组的 method_list_t,可以理解为一个一个的分类,每个method_list_t里面是该分类下的一个一个method_t 组成的方法列表

对于分类想了解得更深一点的,怎么拼接组装的,底层实现的,可以看下Category相关知识

class_ro_t

ro –> readonly,只读,不可修改,保留类最初始的属性

  struct class_ro_t {
  
      method_list_t * baseMethodList;//原方法列表
      protocol_list_t * baseProtocols;
      const ivar_list_t * ivars;//成员变量列表
      }

class_ro_t 里面baseMethodList 是一维数组,因为原始的类对象只有一个,就是宿主类,不包含分类信息,所以不需要二维数组,直接 baseMethodList 里面直接放 method_t就行了

  struct method_t {
      SEL name;//函数名
      const char *types;//返回值类型&参数
      MethodListIMP imp;//函数地址 
      }
  

Type Encodings

方法缓存3

  // "i24@0:8i16f20"
  // 0id 8SEL 16int 20float  == 24
  - (int)test:(int)age height:(float)height

每一个方法都有两个默认参数self_cmd 我们可以查到id类型为@SEL类型为:

  • 1、第一个参数i返回值
  • 2、第二个参数@id 类型的self
  • 3、第三个参数:SEL 类型的_cmd
  • 4、第四个参数iInt age
  • 5、第五个参数ffloat height

其中加载的数字其实是跟所占字节有关

  • 1、24 总共占有多少字节
  • 2、@0id 类型的self的起始位置为0
  • 3、:8 是因为id 类型的self占字节为8,所以SEL 类型的_cmd`的起始位置为8

这里的数字都可以省略 i24@0:8i16f20 ==> i@:if