美文网首页iOS
iOS底层探索之对象的本质和类的关联特性initIsa(下)

iOS底层探索之对象的本质和类的关联特性initIsa(下)

作者: 俊而不逊 | 来源:发表于2021-06-16 08:56 被阅读0次
    image
    在开始阅读本篇博客之前,建议先去看看我的上一篇博客iOS底层探索之对象的本质和类的关联特性initIsa(上)
    本篇内容主要讲下OC当中类的关联相关的initIsa

    1.initIsa结构

    从苹果开源的objc底层源码可以看到OC底层是通过initIsacls类进行关联的

    //关联对象
        if (!zone && fast) {
            obj->initInstanceIsa(cls, hasCxxDtor);
        } else {
            // Use raw pointer isa on the assumption that they might be
            // doing something weird with the zone or RR.
            obj->initIsa(cls);
        }
    

    command+鼠标左键进入obj->initIsa(cls)

    inline void 
    objc_object::initIsa(Class cls)
    {
        initIsa(cls, false, false);
    }
    

    在进入initIsa(cls, false, false)

    initIsa

    isa_t

    进入isa_t

    isa_t
    我们发现isa_t是一个联合体(union),为了能更好的深入的探索下去,我们得先来了解下联合体相关的知识。

    2.联合体

    我们在C语言中经常看到union,这就是联合体,也叫共用体是一种特殊的数据类型,允许你在相同的内存位置存储不同的数据类型。你可以定义一个带有多成员的共用体,但是任何时候只能有一个成员带有值。共用体提供了一种使用相同的内存位置的有效方式。

    定义联合体

    为了定义共用体,必须使用union语句,方式与定义结构类似。union语句定义了一个新的数据类型,带有多个成员。union 语句的格式如下:

    union [union tag]
    {
       member definition;
       member definition;
       ...
       member definition;
    } [one or more union variables];
    

    union tag 是可选的,每个member definition是标准的变量定义,比如int i;或者 float f;或者其他有效的变量定义。在共用体定义的末尾,最后一个分号之前,可以指定一个或多个共用体变量,这是可选的。

    联合体举例

    下面定义一个名为Data的共用体类型,有三个成员i、f 和 str

    union Data
    {
       int i;
       float f;
       char  str[20];
    };
     
    int main( )
    {
       union Data data;        
       data.i = 10;
       data.f = 220.5;
       strcpy( data.str, "C Programming");
       printf( "data.i : %d\n", data.i);
       printf( "data.f : %f\n", data.f);
       printf( "data.str : %s\n", data.str);
     
       return 0;
    }
    

    代码运行输出结果

    代码运行结果
    我们可以看到共用体if成员的值有损坏,因为最后赋给变量的值占用了内存位置,这也是 str成员能够完好输出的原因。现在让我们再来看一个相同的实例,这次我们在同一时间只使用一个变量,这也演示了使用共用体的主要目的:
       union Data data;        
    
       data.i = 10;
       printf( "data.i : %d\n", data.i);
       
       data.f = 220.5;
       printf( "data.f : %f\n", data.f);
       
       strcpy( data.str, "C Programming");
       printf( "data.str : %s\n", data.str);
    

    代码运行结果


    代码运行结果

    在这里,所有的成员都能完好输出,因为同一时间只用到一个成员。

    对比结构体

    struct Student{
        NSString *name;
        int age;
    } Student;
    
    int main( )
    {
       struct Student stu;
       stu.name = @"RENO";
       stu.age = 18;
       return 0;
    }
    

    代码运行结果


    代码运行结果

    从代码运行的结果我们看到,结构体的所有的成员赋值都能正常输出

    小结

    1. 结构体(struct)中所有变量是“共存”的

      • 优点:是海纳百川“有容乃⼤”
      • 缺点:是内存空间的分配是粗放的,不管你⽤不⽤,我系统都全给你分配。
    2. 联合体(union)中是各变量是“互斥”的,有你没我,有我没你

      • 缺点:就是不够“包容”
      • 优点:是内存使⽤更为精细灵活,也节省了内存空间

    3.位域

    上面👆介绍了联合体,现在我们再来了解下位域的知识,嘿嘿😋提前透露下,待会探索底层需要用到位域

    什么是位域

    C语言允许在一个结构体中以位为单位来指定其成员所占内存长度,这种以位为单位的成员称为“位段”或称“位域”(bit field) 。利用位段能够用较少的位数存储数据。

    光看这文字概念,也不太能理解啊!能不能给举个栗子🌰啊!好,那么接下来我就举个栗子🌰


    举个🌰

    位域举例

    /* 定义简单的结构体 */
    struct status1{
      unsigned int widthValidated;
      unsigned int heightValidated;
    };
    
    /* 定义位域结构 */
    struct status2{
      unsigned int widthValidated : 1;
      unsigned int heightValidated : 1;
    };
    printf( "Memory size occupied by status1 : %lu\n", sizeof(status1));
    printf( "Memory size occupied by status2 : %lu\n", sizeof(status2));
    
    

    当上面的代码被编译和执行时,它会产生下列结果:

    代码运行结果

    从运行结果我们可以得知: status1结构体占用了8 字节的内存空间,而使用位域status 2只占用 4 个字节的内存空间(冒号:后面加数字表示使用多少位),但是只有2 位被用来存储值。如果用了 32个变量,每一个变量宽度为 1位,那么status 2结构将使用 4 个字节,但只要再多用一个变量,使用了33个变量的话,那么它将分配内存的下一段来存储第 33 个变量,这个时候就开始使用8 个字节了。

    上面的举例我们只是为了记录widthheight 是否生效,其实只有TRUE/FALSE 两种情况,使用结构体的话就需要8字节64位来存储,这就大大浪费了,有点大材小用了。而使用位域,只需要2位4字节存储。

    有些信息在存储时,并不需要占用一个完整的字节,而只需占几个或一个二进制位。例如在存放一个开关量时,只有01 两种状态,用 1二进位即可。为了节省存储空间,并使处理简便,所以C 语言提供了"位域""位段"这种数据结构。
    "位域"是把一个字节中的二进位划分为几个不同的区域,并说明每个区域的位数。每个域有一个域名,允许在程序中按域名进行操作。这样就可以把几个不同的对象用一个字节的二进制位域来表示。

    对于位域的定义尚有以下几点说明:

    • 一个位域存储在同一个字节中,如一个字节所剩空间不够存放另一位域时,则会从下一单元起存放该位域。也可以有意使某位域从下一单元开始。例如:
    struct bs{
        unsigned a:4;
        unsigned  :4;    /* 空域 */
        unsigned b:4;    /* 从下一单元开始存放 */
        unsigned c:4
    }
    

    在这个位域定义中,a 占第一字节的 4 位,后 4 位填 0 表示不使用,b 从第二字节开始,占用 4 位,c 占用 4 位。

    • 位域的宽度不能超过它所依附的数据类型的长度,成员变量都是有类型的,这个类型限制了成员变量的最大长度:后面的数字不能超过这个长度。
    • 位域可以是无名位域,这时它只用来作填充或调整位置。无名的位域是不能使用的。例如:
    struct k{
        int a:1;
        int  :2;    /* 该 2 位不能使用 */
        int b:3;
        int c:2;
    };
    

    从以上分析可以看出,位域本质 上就是一种结构体类型,不过其成员是按二进位分配的。

    位域允许用各种格式输出。

    int main(){
        struct bs{
            unsigned a:1;
            unsigned b:3;
            unsigned c:4;
        } bit,*pbit;
        bit.a=1;    /* 给位域赋值(应注意赋值不能超过该位域的允许范围) */
        bit.b=7;    /* 给位域赋值(应注意赋值不能超过该位域的允许范围) */
        bit.c=15;    /* 给位域赋值(应注意赋值不能超过该位域的允许范围) */
        printf("%d,%d,%d\n",bit.a,bit.b,bit.c);    /* 以整型量格式输出三个域的内容 */
        pbit=&bit;    /* 把位域变量 bit 的地址送给指针变量 pbit */
        pbit->a=0;    /* 用指针方式给位域 a 重新赋值,赋为 0 */
        pbit->b&=3;    /* 使用了复合的位运算符 "&=",相当于:pbit->b=pbit->b&3,位域 b 中原有值为 7,与 3 作按位与运算的结果为 3(111&011=011,十进制值为 3) */
        pbit->c|=1;    /* 使用了复合位运算符"|=",相当于:pbit->c=pbit->c|1,其结果为 15 */
        printf("%d,%d,%d\n",pbit->a,pbit->b,pbit->c);    /* 用指针方式输出了这三个域的值 */
    }
    

    上例程序中定义了位域结构 bs,三个位域为 a、b、c。说明了 bs 类型的变量 bit 和指向 bs 类型的指针变量 pbit。这表示位域也是可以使用指针的。

    4.initIsa分析

    在上面我们已经知道了,isa_t是一个联合体,里面除了有isa_t()构造方法,还有uintptr_t类型的bits,还有我们的对象cls,最最重要的是还有一个结构体成员变量ISA_BITFIELD

    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_BITFIELD不就是isa位域吗?我们在深入进去看看,到底是个什么东东???

    ISA_BITFIELD

    command+鼠标左键点击,进去找到了ISA_BITFIELD

    #   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
    

    好家伙?直呼“好家伙”啊!

    在这里插入图片描述
    我的天呢?这内有乾坤啊!这是一个宏定义,分为__arm64____x86_64__两种,其中__arm64__的情况包括模拟器。
    完整定义如下
    # if __arm64__
    // ARM64 simulators have a larger address space, so use the ARM64e
    // scheme even when simulators build for ARM64-not-e.
    #   if __has_feature(ptrauth_calls) || TARGET_OS_SIMULATOR
    #     define ISA_MASK        0x007ffffffffffff8ULL
    #     define ISA_MAGIC_MASK  0x0000000000000001ULL
    #     define ISA_MAGIC_VALUE 0x0000000000000001ULL
    #     define ISA_HAS_CXX_DTOR_BIT 0
    #     define ISA_BITFIELD                                                      \
            uintptr_t nonpointer        : 1;                                       \
            uintptr_t has_assoc         : 1;                                       \
            uintptr_t weakly_referenced : 1;                                       \
            uintptr_t shiftcls_and_sig  : 52;                                      \
            uintptr_t has_sidetable_rc  : 1;                                       \
            uintptr_t extra_rc          : 8
    #     define RC_ONE   (1ULL<<56)
    #     define RC_HALF  (1ULL<<7)
    #   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;                                       \
            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_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
    #   define RC_ONE   (1ULL<<56)
    #   define RC_HALF  (1ULL<<7)
    
    

    进都进来了,你就给我看这个啊?这个uintptr_t nonpointeruintptr_t has_assoc奇奇怪怪的是个什么玩意啊???

    在这里插入图片描述
    那就耐着性子,听我一一道来!

    NONPOINTER_ISA

    1. nonpointer0位,表示是否对isa指针开启指针优化0:isa指针,1:不止是类对象地址,isa包含了类信息、对象的引用计数等。
    2. has_assoc1位,表示关联对象标志位,0:没有,1:有。
    3. has_cxx_dtor2位,表示该对象是否有C++或者Objc的析构器,如果有析构函数,则需要做析构逻辑,如果没有,则可以更快的释放对象。
    4. shiftclsx86架构中占用3~46位,表示存储类指针的值。开启指针优化的情况下,在arm64架构中占用3~35位。
    5. magicx86架构中占用47~52位,在arm64架构中占用36~41位,用于调式器判断当前对象是真的对象还是没有初始化的空间。
    6. weakly_referencedx86架构中占用53位,在arm64架构中占用42位,标志对象是否被指向或者曾经指向一个ARC的弱变量,没有弱引用的对象可以更快释放。
    7. unusedx86架构中占用54位,在arm64架构中占用43位,标志对象是否正在释放内存。
    8. has_sidetable_rcx86架构中占用55位,在arm64架构中占用44位,表示当对象引用计数大于10时,则需要借用该变量存储进位
    9. extra_rcx86架构中占用56~63位,在arm64架构中占用45~63位,当表示该对象的引用计数值时,实际上是引用计数值减1,例如:如果对象的引用计数为10,那么extra_rc9,如果引用计数大于10,则需要使用到has_sidetable_rc

    为了更直观的理解ISA_BITFIELD请看下图:

    ISA_BITFIELD内存分布
    由上面得知OC底层通过运用联合体位域,大大优化了ISA,这就是著名的NONPOINTER_ISA,这样就充分利用了内存的使用。

    isa关联对象过程还原

    先看下面这个代码,在NSLog处打上断点

     JPStudent *stu = [JPStudent alloc];
     NSLog(@"%@",stu);
    

    当代码执行到NSLog,控制台lldb调试

    (lldb) x/4gx stu
    0x10070d140: 0x011d8001000080e9 0x0000000000000000
    0x10070d150: 0x0000000000000000 0xd4cbf20b4a85bce1
    

    我们看到stu对象的指针地址是0x011d8001000080e9(isa),最高位是0,说明64位并没有使用满。
    我们都知道对象是通过指针地址关联到类的,那么我们看看类的地址

    (lldb) p/x JPStudent.class
    (Class) $1 = 0x00000001000080e8 JPStudent
    

    从控制台看到,类的地址占用位数更少了,那到底对象和类是怎么关联的呢???

    ISA_MASK

    在上面的源码中我们看到有一个ISA_MASK的宏定义,ISA_MASK = 0x007ffffffffffff8ULL

    #     define ISA_MASK        0x007ffffffffffff8ULL
    

    这个ISA_MASK,就是ISA面具,和网络中的子网掩码差不多,单独存在没有什么实际的意义。

    比如我想露出眼睛👀或者鼻子👃,那么我就需要遮住脸部的其他地方,把需要露出来的地方露出来,那我戴上一个面具🎭就可以达到这种目的。

    那么我们现在知道了ISA0x011d8001000080e9ISA_MASK0x007ffffffffffff8ULL让它两个作与(&)操作,看看结果

    在这里插入图片描述
    还有谁???看到没有?得到的结果是一模模一样样!

    对象stu通过ISA得到(cls),因为ISA是不纯的,里面还包含了其他信息,所以必须通过掩码(ISA_MASK),得到类的信息。

    ISA位运算

    除了上面的通过掩码得到,也可以通过运算得到,因为我们只要找到shiftcls就可以,在前面的介绍中我们已经知道shiftcls是占中间的44位。

    具体操作如下

    (lldb) x/4gx stu
    0x10070d140: 0x011d8001000080e9 0x0000000000000000
    0x10070d150: 0x0000000000000000 0xd4cbf20b4a85bce1
    (lldb) p/x 0x011d8001000080e9 >> 3
    (long) $6 = 0x0023b0002000101d
    (lldb) p/x 0x0023b0002000101d << 20
    (long) $7 = 0x0002000101d00000
    (lldb) p/x 0x0002000101d00000 >> 17
    (long) $8 = 0x00000001000080e8
    (lldb) p/x JPStudent.class
    (Class) $9 = 0x00000001000080e8 JPStudent
    (lldb) 
    

    具体操作请看下面的图


    isa位运算过程

    🌹请收藏+关注,评论 + 转发,以免你下次找不到我,哈哈😁🌹

    🌹欢迎大家留言交流,批评指正,互相学习😁,提升自我🌹

    相关文章

      网友评论

        本文标题:iOS底层探索之对象的本质和类的关联特性initIsa(下)

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