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

2021-05-19  本文已影响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
    };
};
上一篇下一篇

猜你喜欢

热点阅读