美文网首页
iOS 底层 :isa 与类关联的原理

iOS 底层 :isa 与类关联的原理

作者: 木槿WEIXIAO | 来源:发表于2021-06-17 18:28 被阅读0次

    本文的目的主要是理解类与 isa 是如何关联的
    在介绍正文之前,首先要理解一个概念:oc 对象的本质是什么

    OC 对象的本质

    在探索本质之前,先了解一个编译器 clang

    Clang

    • \color{#DC143C}{clang} 是有 Apple 主导编写,基于 LLVM 和 c/c++/oc 的编译器
    • 主要是用于底层编译,将一些文件输出成 c++文件,例如 main.m 输出成 main.cpp,其目的是为了更好的观察底层实现逻辑.

    探索对象本质

    • 在 main 中自定义一个 LGPerson 类,写一个 name 属性
    @interface LGPerson : NSObject
    @property(nonatomic,copy)NSString * name;
    @end
    
    @implementation LGPerson
    @end
    
    • 通过终端,将 main.m 编译成 main.cpp 文件,可以通过以下几种方式
    //1、将 main.m 编译成 main.cpp
    clang -rewrite-objc main.m -o main.cpp
    
    //2、将 ViewController.m 编译成  ViewController.cpp
    clang -rewrite-objc -fobjc-arc -fobjc-runtime=ios-13.0.0 -isysroot / /Applications/Xcode.app/Contents/Developer/Platforms/iPhoneSimulator.platform/Developer/SDKs/iPhoneSimulator13.7.sdk ViewController.m
    
    //以下两种方式是通过指定架构模式的命令行,使用xcode工具 xcrun
    //3、模拟器文件编译
    - xcrun -sdk iphonesimulator clang -arch arm64 -rewrite-objc main.m -o main-arm64.cpp 
    
    //4、真机文件编译
    - xcrun -sdk iphoneos clang -arch arm64 -rewrite-objc main.m -o main- arm64.cpp 
    
    • 打开 cpp 文件,找到 LGPerson 的定义,发现被定义成了一个结构体
    • LGPerson_IMPL 中的第一个属性其实就是 isa,是继承自 NSObject,属于伪继承,伪继承的方式就是把NSObject_IMPL定义为 LGPerson_IMPL 的第一个属性,意味着 LGPerson 就有了 NSObject 的所有属性
    struct LGPerson_IMPL {
        struct NSObject_IMPL NSObject_IVARS;
        NSString *_name;
    };
    struct NSObject_IMPL {
        Class isa;
    };
    

    通过以上分析,理解了 oc 对象的本质,但是有一个疑问,为什么 isa 的类型是 Class???

    • 底层 2中分析过,alloc 核心方法之一initInstanceIsa,通过这个方法的实现,isa 是一个 isa_t 类型
    • 而在 NSObject 中定义的 isa 是 class 类型,根本原因在于,isa 对外反馈的是类信息,为了让开发人员更加清晰明确,在 isa 返回时做了一个强制类型转换,源码中的强转如下
    inline Class
    isa_t::getDecodedClass(bool authenticated) {
    #if SUPPORT_INDEXED_ISA
        if (nonpointer) {
            return classForIndex(indexcls);
        }
        return (Class)cls;
    #else
        return getClass(authenticated);
    #endif
    }
    
    inline Class
    objc_object::ISA(bool authenticated)
    {
        ASSERT(!isTaggedPointer());
        return isa.getDecodedClass(authenticated);
    }
    
    image.png

    总结

    • oc 对象的本质是结构体
    • LGPerson 中 isa 继承自 NSObject 中的 isa

    objc_setProperty 源码探索

    除了 LGPerson 的底层定义,我们还发现了 name 的 set 和 get 方法定义,其中 set 方法依赖objc_setProperty


    image.png

    下面就来探索 objc_setProperty 源码

    • 在源码中搜索 objc_setProperty,找到源码实现
    void objc_setProperty(id self, SEL _cmd, ptrdiff_t offset, id newValue, BOOL atomic, signed char shouldCopy) 
    {
        bool copy = (shouldCopy && shouldCopy != MUTABLE_COPY);
        bool mutableCopy = (shouldCopy == MUTABLE_COPY);
        reallySetProperty(self, _cmd, newValue, offset, atomic, copy, mutableCopy);
    }
    
    • 进入reallySetProperty源码实现,其方法主要是新值 retain,旧值 release


      image.png

    总结

    通过 objc_setProperty 源码探索,有几下几点说明

    • object_setProperty 主要作用就是关联上层 set 方法以及底层reallySetProperty方法,作为一个中间层
    • 设计的原因.上层的 set 方法有很多,如果直接调用底层方法,会产生很多的临时变量,当你想查找一个 sel 的时候,会非常麻烦
    • 所以苹果采用了适配器设计模式(将底层接口适配为客户端需要的接口),对外提供一个接口,供上层使用,对内调用底层的 set 方法,使其相互不受影响,无论上层怎么变,下层都不变,主要达到一个上下层隔离的目的
      下图代表,上层,隔离层,底层的关系


      未命名文件.png

    cls 与类的关联原理

    探索出发点就是initInstanceIsa函数, 探究 isa 与类是如何关联到一起的
    在这之前需要了解一个联合体, 为什么 isa 的类型 isa_t 是联合体类型

    联合体union

    构造数据类型的方式有两种

    • 结构体(struct)
    • 联合体(union,也叫共用体)

    结构体

    结构体是指把不同的数据组合成一个整体,其变量是共存的,变量不管是否使用,都会分配内存

    • 缺点:所有变量都分配内存,比较浪费内存,假设有 4 个 int 成员,一共分配了 16 字节的空间,但在使用的时候,你只用了 4 个, 就会有 12 字节浪费掉了
    • 优点:存储量大,包容性强, 互相之间不影响

    联合体

    联合体也是由不同的数据类型组成,但其变量是互斥的,所有成员共占同一块内存,而且共用体采用了内存覆盖覆盖技术,同一时刻只能保存一个成员的值,如果对新的成员赋值,就会把原来成员的值覆盖掉

    • 缺点:包容性弱
    • 优点:所有成员共用一段内存,节省了内存空间

    两者的区别

    • 内存占用情况
      - 结构体的各个成员占用不同的内存,互相不影响
      - 共用体各个成员占用同一段内存,修改其中一个成员,会影响其他所有成员
    • 内存分配大小
      - 结构体的内存 >= 内部所有成员内存相加(中间会有间隙)
      - 共用体占用的内存 == 其内部最大成员占用的内存

    isa 的类型 isa_t

    以下是 isa 指针的类型 isa_t 的定义,从定义可以看出是通过联合体(union)定义的

    union isa_t {
        isa_t() { }
        isa_t(uintptr_t value) : bits(value) { }
    
        uintptr_t bits;
    
    private:
        // Accessing the class requires custom ptrauth operations, so
        // force clients to go through setClass/getClass by making this
        // private.
        Class cls;
    
    public:
    #if defined(ISA_BITFIELD)
        struct {
            ISA_BITFIELD;  // defined in isa.h
        };
    
        bool isDeallocating() {
            return extra_rc == 0 && has_sidetable_rc == 0;
        }
        void setDeallocating() {
            extra_rc = 0;
            has_sidetable_rc = 0;
        }
    #endif
    
        void setClass(Class cls, objc_object *obj);
        Class getClass(bool authenticated);
        Class getDecodedClass(bool authenticated);
    };
    

    isa类型使用联合体的原因也是基于内存优化考虑的,这里的内存优化是指在 isa 指针中通过char+位域(即二进制中的每一位都可以代表不同的信息)的原理实现,通常来说 isa 指针占用的内存大小是 8 字节,也就是 64 位,足够存储很多信息了,这样可以极高的节省内存
    从 isa 的定义中可以看出

    • 提供了两个成员 cls 和 bits,由联合体的定义可知,这两个成员是互斥的,也就意味着,初始化 isa 指针,有两种方式
      - 通过 cls 初始化,bits 无值
      - 通过 bits 初始化,cls 无值
      还提供了一个结构体定义的位域,用于存储类信息和其他信息,结构体的成员ISA_BITFIELD这是一个宏定义,有两个版本,arm64(对应 ios 移动端)和x86_64(macos)
    #   else
    #     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;  //是否对 isa 指针开启了指针优化
            uintptr_t has_assoc         : 1; //是否有关联对象
            uintptr_t has_cxx_dtor      : 1;  //是否有 c++实现
            uintptr_t shiftcls          : 33; //存储类信息
            uintptr_t magic             : 6; //调试器判断对象是真对象还是未初始化空间
            uintptr_t weakly_referenced : 1;  //对象是否被指向或者曾经指向一个ARC 弱变量
            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_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          : 44; /*MACH_VM_MAX_ADDRESS 0x7fffffe00000*/ \
          uintptr_t magic             : 6;                                         \
          uintptr_t weakly_referenced : 1;                                         \
          uintptr_t unused            : 1;                                         \
          uintptr_t has_sidetable_rc  : 1;                                         \
          uintptr_t extra_rc          : 8
    
    • nonpointer有两个值,标识自定义的类等,占 1 位
      • 0:纯 isa 指针
      • 1:不只是类对象地址,还包含了类信息,对象的引用计数等
    • has_assoc:标识关联对象标志,占一位
      • 0:没有关联对象
      • 1:有关联对象
    • has_cxx_dtor:表示该对象是否有 c++/oc 的析构器(dealloc),占 1 位
      • 如果有,做析构逻辑
      • 如果没有,可以更快的释放对象
    • shiftcls:表示存储类的指针值,即类信息
      • arm64 占 33 位,开启指针优化的情况下,在 arm64 架构下有 33 位存储类指针
      • x86_64 占 44 位,
    • magic:用于调试器判断当前对象是真对象还是未初始化空间,占 6 位
    • weakly_referenced:代表对象是否被指向或者曾经指向一个ARC 的弱变量
      • 没有弱引用的变量可以更快的释放
    • has_sidetable_rc:表示当对象的引用计数大于 10(举例而已,不一定是 10),则需要借用该变量存储进位
    • extra_rc:额外的引用计数,表示该对象的引用计数值,实际是引用计数值减 1,
      • 如果对象的引用计数为 10,那么 extra_rc 的值为 9(举例而已),实际上 iphone 真机上的exra_rc 是使用 19 位来存储引用计数的
        针对 arm64 平台 isa存储情况如下


        未命名文件-2.png

    原理探索

    • 通过alloc->_objc_rootAlloc->callAlloc->_objc_rootAllocWithZone->_class_createInstanceFromZone->initInstanceIsa进入查看实现
    objc_object::initInstanceIsa(Class cls, bool hasCxxDtor)
    {
        ASSERT(!cls->instancesRequireRawIsa());
        ASSERT(hasCxxDtor == cls->hasCxxDtor());
    
        initIsa(cls, true, hasCxxDtor);
    }
    
    • 进入 initIsa 实现,主要是初始化 isa 指针


      image.png

      该方法的逻辑主要分为两部分

    • 1.通过 cls 初始化 isa
    • 2.通过 bits 初始化 isa

    验证 isa 指针 位域(0-64)

    根据前面提到的位域信息,可以在这里验证一下位域是真的存在的,在newisa.bits 处打一个断点


    image.png

    在执行到这句代码是,通过 lldb 打印p newisa, 然后走到下一行在打印一次 newisa,得到的信息如下图


    image.png
    通过与前一个newisa 相比,后一个的nonpointer变成了 1, magic变成了 59,
    • 其中的 59 是十进制的体现, 把 isa 指针从 47 位(x86 下,前面的位域占47 位)开始读取 6 位,在转成 10 进制,就是 59


      未命名文件.png

    isa 与类的关联

    cls 与 isa 关联的原理,就是 isa 中的 shiftcls 位域存储了类信息,其中initInstanceIsa的过程是将calloc 指针与当前类关联起来,有以下几种验证方式

    • 1.通过 initIsa 中的newisa.shiftcls = (uintptr_t)cls >> 3验证
    • 2.通过 isa 指针地址与 ISA_MASK 值 & 验证
    • 3.通过 runtime 的方法 object_getClass 验证
    • 4.通过位运算验证

    1.通过 initIsa

    • 运行到shiftcls = (uintptr_t)newCls >> 3,其中 shiftcls 存储的是当前类的值信息
      • 查看 newCls 信息是 SATest类
      • shiftCls 赋值的逻辑是将编码后的 SATest 数据右移3 位


        image.png
    • 执行 lldb 指令, 打印p (uintptr_t)newCls >> 3得到值存储到 shiftCls 中


      image.png
    • 继续执行到isa = newisa; 打印 p newisa


      image.png

    与bits 赋值结果对比,bits 位域中有两处变化

    • cls 由默认值变成了 SATest, isa 与类完美关联
    • shiftCls 从 0 变成了有值

    为什么在shiftcls 赋值时需要强转

    因为内存存储时,不能存储字符串,机器码只能识别 0 和 1 这两种数字,所以需要将其转换为uintptr_t数据,这样 shiftcls 中的数据才能被机器识别,其中uintptr_t为 long

    为什么需要右移 3 位

    因为 shiftcls 处于 isa 中间部分,前面还有 3 个位域,为了不影响前面 3 个位域,需要右移将其抹零

    方式 2:通过 isa & ISA_MASK

    • 在 main 中断点到 SATest 创建时,按照下图方式进行打印

    arm64 中 ISA_MASK 为0x0000000ffffffff8ULL
    x86 中 ISA_MASK 为0x00007ffffffffff8ULL


    image.png

    方式 3 通过 runtime中的函数 object_getClass

    • 查找 object_getClass源码实现


      image.png
    • 进入getIsa实现


      image.png
    • 进入 ISA()实现


      image.png
    • 进入getDecodedClass实现


      image.png
    • 进入getClass实现


      image.png

    方式 4:通过位运算

    • 在 main 中 SATest 创建处加一个断点,通过x/4gx test打印test 存储信息,当前类的信息存储在 isa 指针中,切此时的 shiftcls 占 44 位(因为在 macos 环境下)


      image.png
    • 想要读取中间的 44 位信息,就需要经过位运算,将 shiftcls 右边的 3 位和左边的 17 位都要抹零,相对位置不能变,分为如下几步
      - 1.先将 isa >> 3: 将前三位抹零
      - 2. 然后用第一步的结果 << 20 (本身左边是 17 位,但是经过第一步以后,左边变成了 20 位)
      - 3.第二步的结果 >> 17(回到最初 shiftcls 在 isa 中的初始位置,此时左右已经全部抹零)


      image.png

    相关文章

      网友评论

          本文标题:iOS 底层 :isa 与类关联的原理

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