美文网首页iOS备忘录
runtime(一) isa 指针

runtime(一) isa 指针

作者: 小新0514 | 来源:发表于2019-03-15 14:50 被阅读0次

    本文章基于 objc4-750 进行测试.
    objc4 的代码可以在 https://opensource.apple.com/tarballs/objc4/ 中得到.

    类和对象

    id, Class 和 NSObject 是 iOS 类和对象中比较重要的, 在 objc.h 和 NSObject.h 中找到了他们的定义:

    // NSObject.h
    @interface NSObject <NSObject> {
        Class isa  OBJC_ISA_AVAILABILITY;
    }
    
    // objc.h
    typedef struct objc_object *id;
    struct objc_object {
        Class _Nonnull isa  OBJC_ISA_AVAILABILITY;
    };
    typedef struct objc_class *Class;
    

    objc_class 这个结构体可以在 runtime.h 中找到, 但 runtime.h 中有众多的 OBJC2_UNAVAILABLE 标记, 其中 objc_class 就是其中一个:

    struct objc_class {
        ...
    } OBJC2_UNAVAILABLE;
    

    查看 runtime 的源码可以发现, 工程中有 objc_private.h 以及 objc_runtime_new.h 文件, 继续探索可以发现:

    // NSObject.h 中:
    #include <objc/objc.h> 
    
    // NSObject.mm 中:
    #include "objc-private.h" 
    #include "NSObject.h"
    
    // objc.h 中
    #if !OBJC_TYPES_DEFINED // 宏定义为 0 才会编译下面的代码
    typedef struct objc_class *Class;
    struct objc_object {
        Class _Nonnull isa  OBJC_ISA_AVAILABILITY;
    };
    typedef struct objc_object *id;
    #endif
    
    // objc-private.h 中
    #ifdef _OBJC_OBJC_H_ // 在引入 objc-private.h 之前引入 objc.h 的话会出现错误
    #error include objc-private.h before other headers
    #endif
    
    #define OBJC_TYPES_DEFINED 1 // 宏定义的值为 1, 避免 objc.h 编译相关代码
    #undef OBJC_OLD_DISPATCH_PROTOTYPES
    #define OBJC_OLD_DISPATCH_PROTOTYPES 0
    

    可以看出, NSObject 是先引入的 objc-private.h, 后引入的 objc.h, 所以 objc.h 无法编译 Class 和 id 相关的部分, objc2 中的 Class 和 id 是在 objc_private.h 和 objc_runtime_new.h 中定义的. 由于代码过长, 我只贴出一部分:

    // objc_private.h
    typedef struct objc_class *Class;
    typedef struct objc_object *id;
    
    // objc_runtime_new.h
    struct objc_class : objc_object {
        // Class ISA;
        Class superclass;
        cache_t cache;             // formerly cache pointer and vtable
        class_data_bits_t bits;    // class_rw_t * plus custom rr/alloc flags
        ...
    }
    

    经过探索我们看到, 不管是类还是对象, 最终都落到了结构体 objc_object 上.

    isa 指针

    在 runtime 的源码中可以找到两个 isa 指针:

    @interface NSObject <NSObject> {
        Class isa  OBJC_ISA_AVAILABILITY;
    }
    
    struct objc_object {
    private:
        isa_t isa;
    public:
         ...
    } 
    

    NSObject 调用 alloc, 会返回一个 id 类型的指针, id 类型强制转换为 NSObject 之后, 访问的 Class 类型的 isa 实际上就是结构体 objc_object 中 isa_t 类型的 isa. 可以依照如下方法测试一下(需要支持 C++):

    struct Woman {
    private:
        NSInteger age;
    public:
        void setAge(NSInteger newAge) {
            age = newAge;
        }
    };
    
    struct Man {
        NSInteger age;
    };
    
    int main(int argc, char * argv[]) {
        Woman woman = Woman();
        woman.setAge(10);
        void * unknow = &woman;
        Man * man = (Man *)unknow;
        NSLog(@"%ld", (long)man->age);
    }
    

    所以整个 isa 指针部分, 最终都归结到一个 isa_t 联合体上.

    isa 指针的优化

    我们知道 isa 指针实际上是指向对应的类对象的, 但 iOS 现在已经进入 64 位的时代了, 64bit 可寻址的范围十分巨大, 而最新的 iPhone XS Max 设备的运行内存也不过 4 个 G, 实际上 32bit 就可以完成 4 个 G 的寻址任务, 所以使用 64bit 来寻址就有些浪费了, 而且程序运行中指针的数量也是十分的多, 会浪费很多内存. 所以从 32 位机过渡到 64 位机的同时, 苹果也考虑到了指针的优化问题.

    isa_t

    isa_t 是一个联合体(共用体), 联合体和结构体类似, 区别在于它所有的成员共用一段内存.

    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_t 联合体的大小就是 64 bit, 联合体中的 cls、bits 和 一个结构体共同使用这 64 bit 的地址空间, 比较重要的就是结构体中的宏定义 ISA_BITFIELD, 该宏定义在 isa.h 中找到了定义处:

    #   define ISA_MASK        0x0000000ffffffff8ULL
    #   define ISA_MAGIC_MASK  0x000003f000000001ULL
    #   define ISA_MAGIC_VALUE 0x000001a000000001ULL
    #   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 deallocating      : 1;                                       \
          uintptr_t has_sidetable_rc  : 1;                                       \
          uintptr_t extra_rc          : 19
    #   define RC_ONE   (1ULL<<45)
    #   define RC_HALF  (1ULL<<18)
    

    这里我只摘录了真机环境(ARM64)下的 ISA_BITFIELD 的定义. 在对象 alloc 结束后, 会调用初始化 isa 联合体的函数, 在这个函数中, 会将对象的地址赋值给 isa 联合体的成员.

    • nonpointer
      共用体 isa_t 中的一个结构体成员, 共用 isa_t 的第一个 bit. 这个 bit 标记 isa 指针是否支持优化, 目前 ARM64 环境下都是支持优化的. 只有这个标记位为 1, 才会有后面的 isa 优化.
    • has_assoc
      共用 isa_t 的第二个 bit, 标记这个对象是否有绑定关联对象, 对应 objc_setAssociatedObject() 和 objc_getAssociatedObject(), 如果没有绑定, 则会跳过释放关联对象的步骤, 能够在释放对象时节省时间.
    • has_cxx_dtor
      共用 isa_t 的第三个 bit, 标记本对象是否有析构函数, 如果没有析构函数, 则会跳过析构逻辑, 能加快对象的释放.
    • shiftcls
      共用 isa_t 的第 4 到第 36 个 bit, 用于存储类对象的地址. 优化后的 isa 指针使用了 33 个 bit 来存储它的类对象地址, 33 bit 可以寻址 0~8G 的地址空间, 对目前最大允许内存为 4G 的 iPhone 手机来说是绰绰有余的.
    • magic
      和 AutoreleasePoolPage 里的 magic 类似, 在申请一段内存空间并初始化后设置为一个固定的值, 作为已完成内存申请并初始化的标记.
    • weakly_referenced
      标记本对象是否有 weak 指针指向, 如果没有, 则释放本对象时, 就会跳过 weak 指针的处理逻辑, 加快释放速度.
    • deallocating
      标记本对象是否正在回收, 可以避免使用到正处于回收中的对象, 造成错误.
    • has_sidetable_rc
      isa 指针中用来存储引用计数的位数有限, 虽然可以存储 2^19 引用计数, 但最终还是要考虑到超过这个数字时的方案, 苹果给的方案是使用 SideTable, 一个 hash 表. 这个标记位标记引用计数已经超出 isa 指针预留的数目.
    • extra_rc
      isa 指针的最高 19bit, 用来存储对象的引用计数, 通常情况下这里存储的是实际的引用计数减去 1 的结果, 当 release 的时候, 如果存储的是0, 就会启动释放流程. 平时我们输出 retainCount 的时候, 都会把这个数字加 1 后返回.
    • ISA_MASK
      isa & ISA_MASK 可以保留 isa 的第 4 到 36 个 bit, 得出的结果再向右移 3 位, 就是指向对应的类对象的指针的值了.
    • ISA_MAGIC_MASK 和 ISA_MAGIC_VALUE
      如果一个 isa & ISA_MAGIC_MASK == ISA_MAGIC_VALUE, 那么这个 isa 指针是一个完整可用 isa 指针.
    • RC_ONE
      一个宏定义, 1 << 45 刚好是 extra_rc 的最低位, 当对一个对象进行 retain 操作时, 直接给这个对象的 isa 指针加上一个 RC_ONE, 就相当于给 extra_rc 加 1.
    • RC_HALF
      一旦引用计数过大, isa 无法存储时, runtime 会利用 isa 和 SideTable 同时来存储引用计数, extra_rc = RC_HALF, 来存储 2^18 个引用计数, 其它的使用 SideTable. 如果 has_sidetable_rc 为 1, 管理引用计数时会优先操作 SideTable, 直到引用计数小于 2^18, 就会将 has_sidetable_rc 设置为 0, 按照正常的 isa 管理流程来执行.

    接下来我们测试一下 isa 指针

    这里注意要用真机测试, 真机是 ARM64 环境, 模拟器是 x86_64 环境.

    - (void)test {
        NSObject * obj = [[NSObject alloc] init];
        NSLog(@""); //在这里打一个断点
    }
    

    断点停在 NSLog 处之后, 我们用 LLDB 来分别调试一下(p/x 是以十六进制形式输出).
    (lldb) p/x obj
    (lldb) p/x obj->isa
    (lldb) p/x [obj class]
    (lldb) p/x &obj
    (lldb) p/x &obj->isa

    对应的输出分别是:

    (NSObject *) $0 = 0x0000000283044760
    (Class) $1 = 0x000001a22b16feb1
    (Class) $2 = 0x000000022b16feb0
    (NSObject **) $3 = 0x000000016fd7d4b8
    (Class *) $4 = 0x0000000283044760
    
    1. obj 和 &obj->isa 输出的都是 obj 对象在内存中的地址, C 语言中, 一个结构体的地址就是这个结构体第一个成员的地址, 而 obj 的第一个成员就是 isa, 所以 isa 的地址就是 obj 的地址.
    2. obj->isa 和 [obj class]
      一个对象的 isa 指针指向它的类对象. 所以 obj->isa 和 [obj class] 输出的地址应该是一样的, 这里的 obj->isa 输出了优化后的 isa 指针, 将 isa & ISA_MASK 得到的结果, 就是 [obj class] 的输出结果. 另外可以看到 obj->isa 的高 19 位都是 0, 也就是说这个对象只有一个强引用, 引用计数是1, 并且没有关联对象、没有 weak 引用, 也没有超出引用计数范围等.
    3. &obj
      obj 是一个指针, 它指向的地址是 obj 这个对象的地址, &obj 实际上是这个指针在栈上的地址.

    下一篇打算写一下 isa 的补充--SideTable (已更新: https://www.jianshu.com/p/ea4c176ffb2b)

    相关文章

      网友评论

        本文标题:runtime(一) isa 指针

        本文链接:https://www.haomeiwen.com/subject/hvmkmqtx.html