美文网首页
OC底层原理(九):runtime之isa

OC底层原理(九):runtime之isa

作者: 跳跳跳跳跳跳跳 | 来源:发表于2021-05-19 17:05 被阅读0次

    之前在OC底层原理(二)这章内容中有讲过 instance对象的isa & ISA_MASK 指向class对象,class对象的isa & ISA_MASK指向meta-class对象,但是并没有详细讲isa的内部结构

    isa内部结构

    在arm64架构之前,isa就是一个普通指针,存储着class对象或meta-class对象的地址
    在arm64架构之后,苹果用union结构优化了isa指针,让isa能够存储更多的信息
    在objc4源码中搜索isa_t,其结构如下

    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);
    };
    
    # 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)
    
    # else
    #   error unknown architecture for packed isa
    # endif
    
    // SUPPORT_PACKED_ISA
    #endif
    

    精简一下代码如下

    union isa_t {
        uintptr_t bits;
        struct {
            //arm64
            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
        };
    };
    

    union 共用体

    那么什么是共用体呢,就是共用体内部的成员变量都共享一块内存区域

    union testUnion {
        int a;
        int b;
        int c;
    };
    
    struct testStruct {
        int a;
        int b;
        int c;
    };
    

    我们通过union和struct来做对比

    struct和union内存结构示意图
    内存示意图.png

    我们通过代码来验证下
    创建一个命令行项目,在main函数中写入如下代码,并运行

    union testUnion {
        int a;
        int b;
        int c;
    };
    
    struct testStruct {
        int a;
        int b;
        int c;
    };
    
    int main(int argc, const char * argv[]) {
        @autoreleasepool {
            // insert code here...
            union testUnion testUnion;
            testUnion.a = 10;
            testUnion.b = 11;
            testUnion.c = 12;
            NSLog(@"union : %d  %d   %d", testUnion.a, testUnion.b, testUnion.c);
            
            struct testStruct testStruct;
            testStruct.a = 10;
            testStruct.b = 11;
            testStruct.c = 12;
            NSLog(@"struct : %d  %d   %d", testStruct.a, testStruct.b, testStruct.c);
        }
        return 0;
    }
    

    输出结果如下


    输出结果.png

    可以看到struct的存储的值互不影响,而union里的a、b的值被c的值给覆盖了
    我们回到isa_t的结构中来看

    union isa_t {
        uintptr_t bits;
        struct {
            //arm64
            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
        };
    };
    

    可以看出 uintptr_t bits 和 struct 共用一个内存,那么struct 内部又是什么呢
    这里要引申出另一个概念,位域


    位域

    在了解位域之前我们先设想一个场景,我们需要存储三个字段,高富帅,bool类型
    按照我们原来的写法我们会申明三个bool属性来保存

    @interface ZJPerson : NSObject
    @property(nonatomic, assign) bool isTall;
    @property(nonatomic, assign) bool isHandsome;
    @property(nonatomic, assign) bool isRich;
    @end
    

    本来简简单单存三个bool变量,用了三个字节有点浪费内存
    简简单单的0和1可以用1个字节的3bit来存储,这就涉及位域数据结构了
    位域的写法和结构体类似

    struct testWeiYu {
        char a : 1;
        char b : 1;
        char c : 1;
    };
    

    这代表着a占用1bit,不是字节(一字节等于8bit),b占用1bit,c占用1bit
    比如testWeiYu分配了一个字节的内存,其内存示意图如下


    内存示意图.png

    我们通过代码来验证下

    struct testWeiYu {
        char a : 1;
        char b : 1;
        char c : 1;
    };
    
    int main(int argc, const char * argv[]) {
        @autoreleasepool {
            // insert code here...
            
            struct testWeiYu testWeiYu;
            testWeiYu.a = 0;
            testWeiYu.b = 1;
            testWeiYu.c = 1;
            NSLog(@"weiYu : %d  %d   %d", !!testWeiYu.a, !!testWeiYu.b, !!testWeiYu.c);
            
        }
        return 0;
    }
    

    然后在NSLog处打上断点,运行
    然后在控制台查看testWeiYu的内存地址,再窥探这个地址存储的数据
    使用如下代码查看其地址

    p/x &(testWeiYu)
    

    其输出如下


    地址.png

    然后再用如下代码窥探其存储数据

    x 0x00007ffeefbff528
    

    其输出如下


    数据.png

    06即为其存储的数据
    我们讲0x06转换成二进制数据如下


    二进制.png
    可以看到存储的值为0b110
    按照上面的内存示意图就是
    内存示意图.png

    我们看下最后的输出


    输出结果.png
    可以看到确实实现了1个字节存储3个bool变量
    另外细心的朋友可能也看到了NSLog里的两个!!
    NSLog(@"weiYu : %d  %d   %d", !!testWeiYu.a, !!testWeiYu.b, !!testWeiYu.c);
    

    为什么要这样写呢
    这又要涉及另外一个概念了,原码、反码和补码

    原码、反码、补码

    原码

    原码就是第一位为符号位,0代表正,1代表负,别的位表示数值
    比如+1原码为

    0000 0001
    

    -1原码为

    1000 0001
    

    所以8位2进制的原码取值范围为[-127, +127]
    但是如果计算(+1) + (-1)的话,原码计算结果如下

    0000 0001 + 1000 0001 = 1000 0002 
    

    结果为-2,所以基于这种情况,反码被发明了出来

    反码
    如果为正数,原码与反码一致
    如果为负数,反码为原码除符号位外每一位取反
    反码就是在原码的基础上,每一位取反
    如原码中-0的表示为

    1000 0000
    

    反码为

    1111 1111
    

    原码中+0的表示为

    0000 0000
    

    反码为

    0000 0000
    

    原码中+1的表示为

    0000 0001
    

    反码为

    0000 0001
    

    原码中-1的表示为

    1000 0001
    

    反码为

    1111 1110
    

    所以(+1) + (-1)等于-0

    1000 0001 + 1111 1110 = 1111 1111
    

    虽然反码解决了正负相加等于0的问题,却存在两个0,+0和-0
    所以,再反码的基础上又提出了补码的概念

    补码
    如果为正数,反码与补码一致
    如果为负数,补码为反码+1,并丢弃最高位
    反码中-0表示为

    1111 1111
    

    补码中-0表示为

    1111 1111 + 0000 0001 = 1 0000 0000
    丢弃最高位为 
    0000 0000
    

    所以-0和+0表示一样

    原码中-1的表示为

    1000 0001
    

    反码为

    1111 1110
    

    补码为

    1111 1111
    

    计算机采用的是补码来存储值


    回到主题

    NSLog(@"weiYu : %d  %d   %d", !!testWeiYu.a, !!testWeiYu.b, !!testWeiYu.c);
    

    为什么要这样写呢?
    我们从反面角度来考虑,如果不这么写会出现什么情况

    NSLog(@"weiYu : %d  %d   %d", testWeiYu.a, testWeiYu.b, testWeiYu.c);
    

    我们运行看下结果


    结果.png

    为什么会打印-1呢?
    因为char b是有符号类型,而char b又只占用了1位,所以会将1当作符号位负数来处理,计算机存储值用的是补码,所以存储的值为1取反再加上1,最后结果还是1,再加上符号位就打印出-1
    我们也可以通过加a、b、c申明为无符号类型来解决这个问题

    struct testWeiYu {
        unsigned char a : 1;
        unsigned char b : 1;
        unsigned char c : 1;
    };
    

    共用体+位域
    在了解了共用体和位域的概念后,我们回过头看isa_t的结构

    union isa_t {
        uintptr_t bits;
        struct {
            //arm64
            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
        };
    };
    

    可以看到isa其实就是运用了共用体+位域的数据结构来做的优化
    那么这种结构的值的存与取又如何实现呢?
    我们用代码来实现
    申明一个ZJPerson类,它需要存储三个bool变量isTall,isRich,isHandsome,使用共用体+位域技术来实现存值与取值

    //.h
    @interface ZJPerson : NSObject
    - (void)setTall:(BOOL)tall;
    - (BOOL)tall;
    - (void)setRich:(BOOL)rich;
    - (BOOL)rich;
    - (void)setHandsome:(BOOL)handsome;
    - (BOOL)handsome;
    @end
    
    //.m
    @interface ZJPerson()
    {
        union man {
            char bits;
            struct {
                unsigned char isTall;
                unsigned char isRich;
                unsigned char isHandsome;
            };
        }man;
    }
    @end
    
    @implementation ZJPerson
    
    - (void)setTall:(BOOL)tall {
        
    }
    
    - (BOOL)tall {
        return NO;
    }
    
    - (void)setRich:(BOOL)rich {
        
    }
    
    - (BOOL)rich {
        return NO;
    }
    
    - (void)setHandsome:(BOOL)handsome {
        
    }
    
    - (BOOL)handsome {
        return NO;
    }
    
    @end
    

    我们首先考虑取值
    比如tall为YES,rich为NO,handSome为YES,那么共用体存的值应该如下

    0000 0101
    

    想要取tall的话,需要用这个值& 0000 0001
    计算过程如下

     0000 0101
    &0000 0001
    =0000 0001
    

    同理,取rich,需要用这个值& 0000 0010
    同理,取handSome,需要用这个值& 0000 0100
    可以看到,取最右边的一位需要&(1<<0)
    取右边的第二位需要&(1<<1)
    取右边的第三位需要&(1<<2)
    那么我们来更新.m文件中的代码,申明三位掩码来分别取tall,rich和handsome的值

    #define ZJTallMask (1<<0)
    #define ZJRichMask (1<<1)
    #define ZJHandSome (1<<2)
    @interface ZJPerson()
    {
        union man {
            char bits;
            struct {
                unsigned char isTall;
                unsigned char isRich;
                unsigned char isHandsome;
            };
        }man;
    }
    @end
    
    @implementation ZJPerson
    
    - (void)setTall:(BOOL)tall {
        
    }
    
    - (BOOL)tall {
        return !!(man.bits & ZJTallMask);
    }
    
    - (void)setRich:(BOOL)rich {
        
    }
    
    - (BOOL)rich {
        return !!(man.bits & ZJRichMask);
    }
    
    - (void)setHandsome:(BOOL)handsome {
        
    }
    
    - (BOOL)handsome {
        return !!(man.bits & ZJHandSome);
    }
    
    @end
    

    这样,我们取值就完成了
    为什么要加上!!,是因为
    tall值取出来是0000 0001,转换成10进制就是1
    rich值取出来是0000 0010,转换成10进制就是2
    handsome值取出来是0000 0100,转换成10进制就是4
    取出来的值并不是bool类型,而取两次反就可以得到正确的bool值
    比如拿handsome的值4来做例子,!4就是0,!0就是1,1就是YES
    再比如拿0来做例子,!0就是1,!1就是0,0就是NO
    通过两次取反运算就可以得到我们想要的bool值

    我们继续研究存值
    如果我们要将tall的值设置为YES
    就需要将共用体的值 | ZJTallMask
    其运算过程如下

    //原来tall值为NO
      0000 0110
    | 0000 0001
    = 0000 0111
    
    //原来tall值为YES
      0000 0111
    | 0000 0001
    = 0000 0111
    

    如果我们要将tall的值设置为NO,那么|运算则不能完成这个任务了
    那么要怎么实现存值为NO呢?
    首先,我们需要将掩码按位取反,然后用共用体的值&这个取反的值就可以了

    //将掩码按位取反
    ~ZJTallMask//值为1111 1110
    
    //然后再做&运算
    //原来tall值为NO
      0000 0110
    | 1111 1110
    = 0000 0110
    
    //原来tall值为YES
      0000 0111
    | 1111 1110
    = 0000 0111
    

    这样NO的存储也完成了
    我们完善代码如下

    #define ZJTallMask (1<<0)
    #define ZJRichMask (1<<1)
    #define ZJHandSome (1<<2)
    @interface ZJPerson()
    {
        union man {
            char bits;
            struct {
                unsigned char isTall;
                unsigned char isRich;
                unsigned char isHandsome;
            };
        }man;
    }
    @end
    
    @implementation ZJPerson
    
    - (void)setTall:(BOOL)tall {
        if (tall) {
            man.bits |= ZJTallMask;
        }else {
            man.bits &= ~ZJTallMask;
        }
    }
    
    - (BOOL)tall {
        return !!(man.bits & ZJTallMask);
    }
    
    - (void)setRich:(BOOL)rich {
        if (rich) {
            man.bits |= ZJRichMask;
        }else {
            man.bits &= ~ZJRichMask;
        }
    }
    
    - (BOOL)rich {
       return !!(man.bits & ZJRichMask);
    }
    
    - (void)setHandsome:(BOOL)handsome {
        if (handsome) {
            man.bits |= ZJHandSome;
        }else {
            man.bits &= ~ZJHandSome;
        }
    }
    
    - (BOOL)handsome {
        return !!(man.bits & ZJHandSome);
    }
    
    @end
    

    这样,通过共用体+位域实现三个bool变量的存取功能就完成了
    我们测试一下

    int main(int argc, const char * argv[]) {
        @autoreleasepool {
            // insert code here...
            ZJPerson *person = [[ZJPerson alloc]init];
            person.tall = YES;
            person.rich = YES;
            person.handsome = YES;
            NSLog(@"tall:%d rich:%d handsome:%d", person.tall, person.rich, person.handsome);
        }
        return 0;
    }
    
    结果.png

    之前我们说的arm64之后需要isa指针&Mask获取class对象地址或者meta-class对象地址
    我们看看掩码的值

    define ISA_MASK        0x0000000ffffffff8ULL
    

    将其转换成2进制


    二进制.png

    再看isa_t结构

    union isa_t {
        uintptr_t bits;
        struct {
            //arm64
            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
        };
    };
    

    isa & Mask之后就是将shiftcls的值取出来,而shiftcls里存储的就是class对象、meta-class对象的地址


    isa_t其他字段的释义

    union isa_t {
        uintptr_t bits;
        struct {
            //arm64
            // 是否开启 isa 指针优化
            uintptr_t nonpointer        : 1;                                       \
            // 是否有设置过关联对象,如果没有,释放时会更快
            uintptr_t has_assoc         : 1;                                       \
            // 是否有 C++ 的析构函数(.cxx_destruct),如果没有,释放时会更快
            uintptr_t has_cxx_dtor      : 1;                                       \
            存储着Class、Meta-Class对象的内存地址信息
            uintptr_t shiftcls          : 33; /*MACH_VM_MAX_ADDRESS 0x1000000000*/ \
            // 用于在调试时分辨对象是否未完成初始化
            uintptr_t magic             : 6;                                       \
            // 是否有被弱引用指向过,如果没有,释放时会更快
            uintptr_t weakly_referenced : 1;                                       \
            uintptr_t unused            : 1;                                       \
            // 引用计数器是否过大无法存储在 isa 中。如果为 1,那么引用计数会存储在一个叫 SideTable 的类的属性中
            uintptr_t has_sidetable_rc  : 1;                                       \
            // 里面存储的值是引用计数 - 1
            uintptr_t extra_rc          : 19
        };
    };
    

    相关文章

      网友评论

          本文标题:OC底层原理(九):runtime之isa

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