[OC底层]关于isa

305 阅读4分钟

1:准备工作

isa是什么?

  • isa是一个Class 类型的指针,每个实例对象的第一个成员变量就是isa指针,他指向对象的类,而Class里也有个isa的指针, 指向meteClass(元类)。

下载好objc的源码,然后我们在源码里找到关于isa的部分. 根据我之前的文章juejin.cn/post/698512… 我们知道alloc在底层的执行流程如下: Screenshot 2021-10-04 at 18.37.20.png 首先在源码中找到_class_createInstanceFromZone这个方法:

Screenshot 2021-10-04 at 18.44.15.png Screenshot 2021-10-04 at 18.40.26.png 在这个方法里面,我注意到了两行代码分别是:

  1. obj->initInstanceIsa(cls, hasCxxDtor);
  2. obj->initIsa(cls); 在然后在源码里找到第1个方法initInstanceIsa. 发现这个方法的本质也是执行方法2initIsa Screenshot 2021-10-04 at 18.44.15.png 接下来找到initIsa, 我们来探索一下:

Screenshot 2021-10-04 at 18.46.57.png 从方法initIsa中我们可以看出isa的本质是isa_t类型. initIsa方法会将objclass进行绑定. 进入到isa_t里面,发现isa_t的本质是一个联合体,如下图所示:

Screenshot 2021-10-04 at 18.49.17.png

2: 关于ISA_BITFIELD

  • 我们在分析了OC对象的本质之后,知道了对象的内部,都至少存在一个isa成员变量,isa占用的空间大小为8字节,也就是8x8=64位。

  • 如果我们把这些空间都用于存放指针,那么造成的空间浪费是巨大的。而类中除了指针,还可能有其他的东西需要进行存储,如果能够对这8字节空间进行一系列优化,就可以节省很多的内存开销。

  • isa_t的定义中, 我发现里面有一个结构体里面存放了ISA_BITFIELDclsbits. 其中clsbits作用如下:

  • cls:是Class类型的指针变量,指向的是对象的类。

  • bits:是结构体位域指针。 然后我们点进去看一下ISA_BITFIELD的定义如下:

# if __arm64__
#   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_BITFIELD                                                   

      uintptr_t nonpointer        : 1;        // 是否为纯指针                                
      uintptr_t has_assoc         : 1;       // 关联对象                     

      uintptr_t has_cxx_dtor      : 1;       // C++析构函数                        

      uintptr_t shiftcls          : 44; /*MACH_VM_MAX_ADDRESS 0x7fffffe00000*/
                                            // 类的指针地址
      uintptr_t magic             : 6;                                         

      uintptr_t weakly_referenced : 1;       // 弱引用                                  
      uintptr_t deallocating      : 1;                                         

      uintptr_t has_sidetable_rc  : 1;       // 散列表             
      
      uintptr_t extra_rc          : 8        // 引用计数

#   define RC_ONE   (1ULL<<56)
#   define RC_HALF  (1ULL<<7)

可以看到ISA_BITFIELD宏在内部分别定义了arm64位架构(iOS)和x86_64架构(macOS)的掩码和位域. 下面我们记录一下ISA_BITFIELD中不同位域的作用, 以arm64举例.

  • 0号位: nonpointer 表示是否对 isa 指针开启指针优化

0:纯isa指针,存储着class, Meta-Class 对象的内存地址 1:优化过的isa指针, 不止是类对象地址, 还包含了类信息、对象的引用计数等

  • 1号位: has_assoc 关联对象标志位,0没有,1存在, 如果没有关联过,释放时会更快
  • 2号位: has_cxx_dtor 该对象是否有 C++ 或者 Objc 的析构器,如果有析构函数,则需要做析构逻辑, 如果没有,则可以更快的释放对象。
  • 3~35号位: shiftcls 存储类指针的值。开启指针优化的情况下,在 arm64 架构中有 33 位用来存储类指针。
  • 36~41号位: magic 用于调试器判断当前对象是真的对象 还是没有初始化的空间
  • 42号位: weakly_referenced 标志对象是否被指向或者曾经指向一个 ARC 的弱变量,没有弱引用的对象可以更快释放
  • 43号位: deallocating 标志对象是否正在释放内存
  • 44号位: has_sidetable_rc 当对象引用计数大于 10 时,则需要借用该变量存储进位
  • 35~64号位: extra_rc 当表示该对象的引用计数值,实际上是引用计数值减 1, 例如,如果对象的引用计数为 10,那么 extra_rc 为 9。如果引用计数大于 10, 则需要使用到下面的 has_sidetable_rc。

2: OC 中对象的分类

  • Instance 对象(实例对象) 实例对象在内存中存储的信息包括:

    1. isa指针
    2. 其他成员变量
  • Class 对象(类对象) 每个类在内存中有且仅有一个 Class 对象。 Class 对象在内存中存储的信息包括:

    1. isa指针
    2. superClass
    3. 属性方法
    4. 对象方法信息
    5. 协议信息成员变量信息
  • Meta-Class 对象(元类对象) 每个类在内存中也是有且仅有一个 Meta-Class 对象。 objectMetaClass 是 NSObject 的 meta-class 对象(元类对象)。

Class objectMetaClass = object_getClass([NSObject class]); // Runtime API 

// 注:以下获取的是 objectClass 是 Class 对象,并不是 Meta-Class 对象 
Class objectClass = [[NSObject classclass];

Meta-Class 对象在内存中存储的信息包括:

  1. isa指针
  2. superClass
  3. 类方法信息

3:isa的指针和Class的关联

先来看一下initIsa的源码:

inline void 
objc_object::initIsa(Class cls, **bool** nonpointer, **bool** hasCxxDtor) 
{ 
    ASSERT(!isTaggedPointer()); 
    
    if (!nonpointer) {
        isa = isa_t((uintptr_t)cls);
    } else {
        ASSERT(!DisableNonpointerIsa);
        ASSERT(!cls->instancesRequireRawIsa());
        isa_t newisa(0);
#if SUPPORT_INDEXED_ISA
        ASSERT(cls->classArrayIndex() > 0);
        newisa.bits = ISA_INDEX_MAGIC_VALUE;
        // isa.magic is part of ISA_MAGIC_VALUE
        // isa.nonpointer is part of ISA_MAGIC_VALUE
        newisa.has_cxx_dtor = hasCxxDtor;
        newisa.indexcls = (uintptr_t)cls->classArrayIndex();
#else
        newisa.bits = ISA_MAGIC_VALUE;
        // isa.magic is part of ISA_MAGIC_VALUE
        // isa.nonpointer is part of ISA_MAGIC_VALUE
        newisa.has_cxx_dtor = hasCxxDtor;
        newisa.shiftcls = (uintptr_t)cls >> 3;
#endif
        // This write must be performed in a single store in some cases
        // (for example when realizing a class because other threads
        // may simultaneously try to use the class).
        // fixme use atomics here to guarantee single-store and to
        // guarantee memory order w.r.t. the class index table
        // ...but not too atomic because we don't want to hurt instantiation
        isa = newisa;
    }
}

在源码中我们可以看到initIsa主要做了两个事:

  • 1: 通过cls初始化isa 如果是非nonpointer,代表普通的指针,存储着ClassMeta-Class对象的内存地址信息
  • 2: 通过bits初始化isa 如果是nonpointer,则会进行一系列的初始化操作.其中的newisa.shiftcls = (uintptr_t)cls >> 3;中的shiftcls存储着ClassMeta-Class对象的内存地址信息

验证1 :

Screenshot 2021-10-07 at 11.37.09.png 从上图可以看出:

  • cls 由默认值,变成了LGPerson,将isa与cls完美关联

  • shiftcls由0变成了536875123 关于这一过程回答两个问题:

  • 1:为什么在shiftcls赋值时需要类型强转? shiftcls = (uintptr_t)newCls >> 3;

    答: 因为因为内存的存储不能存储字符串,机器码只能识别 0 、1这两种数字,所以需要将其转换为uintptr_t数据类型,这样shiftcls中存储的类信息才能被机器码理解, 其中uintptr_t是long。

  • 2: 为什么需要右移3位?

答: 主要是由于shiftcls处于isa指针地址的中间部分,前面还有3个位域,为了不影响前面的3个位域的数据,需要右移将其抹零。

验证2 通过isa & ISA_MSAK:

编写代码,然后在控制台打印对象和isa的地址: Screenshot 2021-10-06 at 21.09.26.png 从上图可以看出:

  • cls 由默认值,变成了LGPerson,将isa与cls完美关联
  • shiftcls由0变成了536871965

Screenshot 2021-10-06 at 21.03.42.png

  • 最后可以得出结论: 对象的地址 = isa的地址 & ISA_MASK

验证3: 通过object_getClass

通过查看object_getClass的源码实现,同样可以验证isa与类关联的原理,有以下几步:

  • main中导入#import <objc/runtime.h>
  • 搜索object_getClass(最后定位到objc-class.mm这个文件里.

Screenshot 2021-10-07 at 20.53.04.png

内部调用了getIsa();这个方法 调用流程图如下: Screenshot 2021-10-07 at 21.05.02.png 我们看看isa_t::getClass(MAYBE_UNUSED_AUTHENTICATED_PARAM **bool** authenticated) {这个方法里面做了什么?

Screenshot 2021-10-07 at 21.06.49.png 然后我注意到在else流程中,拿到isabits这个位,再 & ISA_MASK

验证4: 通过位运算

  • _class_createInstanceFromZone方法,此时clsisa已经关联完成 由ISA_BITFIELD的定义可知, 在x86_64的电脑上, 3~47 好内存里面放着shiftcls 如下图所示:

Screenshot 2021-10-08 at 20.48.32.png 我们如果想得到shiftcls的地址 则需要将shiftcls左右两侧都清0. 清0过程如下图所示:

Screenshot 2021-10-08 at 21.28.09.png 这一过程也可以通过在_class_createInstanceFromZone这个方法中使用LLDB观测出来.

Screenshot 2021-10-08 at 21.30.23.png

Reference: