Runtime的本质(一)-isa

OC是一门动态性比较强的编程语言,跟C、C++等语言有着很大的不同
OC的动态性是由Runtime API来支撑
Runtime API提供的接口基本都是C语言的,源码由C\C++\汇编语言编写

在学习Runtime之前,我们先更深入的学习下有关isa的知识。

isa再学习

我们知道isa是一个指针,存储着类对象、原类对象的内存地址。
这是在arm64之前的情况。

在arm64之后,对isa进行了优化,变成了一个共同体(union)结构,还使用位域来存储更多的信息。具体就是isa需要&ISA_MASK才能计算出真实的地址。

在这里插入图片描述

首先,我们在源码中,通过全局搜索objc_object {可以找到

在这里插入图片描述

可以看到,isa类型已经不是Class类型了,而是一个isa_t类型,其具体定义可以点进去看到:isa_t是一个共同体,共同体里面使用了位域操作进行存储,充分利用了存储空间,是对存储空间的一大优化。

union isa_t 
{
    isa_t() { }
    isa_t(uintptr_t value) : bits(value) { }

    Class cls;
    uintptr_t bits;
	struct {
        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;
    };
};

为什么使用共同体呢?

打个比方,我们建立一个对象person,里面有三个BOOL类型的字段,高、富、帅。
我们知道一个BOOL类型可以用一个字节保存信息,那么对象person三个字节差不多就可以了。但其实,person占了16个字节

其中,三个BOOL字段,每个占一个字节,person对象里面有一个isa指针,一个指针占8个字节。之前我们写过,一个对象至少占16个字节,因此,该对象占了16个字节。

如果,三个布尔值都用同一个字节里面的不同位表示,那么,三个BOOL类型只需要一个字节(三个位)就可以完成,这比占三个字节省了很多空间。

因此,我们可以使用 十六进制0b0000 0000中,最后三个字节表示分别表示高富帅,然后通过按位与、按位或以及左移等操作,对其位进行操作,从而达到使用位表示BOOL值。

既然例子中person对象的bool值可以使用位操作进行存储表示,那么同样的原理,isa类型使用共同体union的isa_t也可以使用位操作,进行更有效的数据存储。

位运算操作

位运算符有:& | ~ ^ << >>

按位与 & 1假即假
按位或 | 1真即真
按位非(按位取反) ~ 真变假,假变真
按位异或 ^ 不同为真,相同为假(类比男女,不要搞基- -)
左移<< 原数乘以进制^ 移动位数。举例:十进制239,左移2位,23900,即239 *10^2
右移>>原数除以进制^ 移动位数。举例:十进制138,右移3位,0.138,即138 *10^-3
取值

通过对某一特定位 按位与 上一个该位为1其他位为0的数据,即可取出该位的值。
例如
取出11001中倒数第4位的值,可以使用01000与上原数据,即可找到倒数第4位的值为01000,即倒数第4位的值为1
取出11001中倒数第2位的值,可以使用00010与上原数据,即可找到倒数第2位的值为00000,即倒数第2位的值为0
然后对与后的结果进行分析,发现:
只要与出的结果为0,则想取出的位为0
只要与出的结果不为0,则想取出的位为1

设值

如果想将特定位设置为1,则对某一特定位 按位或 上一个该位为1,其他位为0的数据,即可设置该位的值为1。
如果想将特定位设置为0,则对某一特定位 按位与 上一个该位为0,其他位为1的数据,即可设置该位的值位0。
第二条中,“其中该位为0,其他位为1的数据”,其实是对掩码进行取反操作的值。(掩码是00010,取反是11101)。因为设置值与取值需是同一个掩码,因此,需要对掩码做取反操作,而不能随便凑一个数据。

用以上方法,可以实现用某个特定字节的某一位代表一个BOOL值。但是方法有些不太方便,等我们再加一个属性的时候,又是要写很多东西。因此,我们考虑使用结构体的位域做存储。

位域

struct {
        char tall : 1;//char类型的tall 占一个字节
        char rich : 1;//占一个字节
        char handsome : 1;//占一个字节
    } _tallRichHandsome;

_tallRichHandsome的倒数第一个字节是tall的值,倒数第二个字节是rich的值,倒数第三个字节是handsome的值。即先写的值在最后面。
比如tall=0,rich=0,handsome=1,则_tallRichHandsome的值是:
0b0000 0100

举一个栗子:

YZPerson.h
#import <Foundation/Foundation.h>

@interface YZPerson : NSObject
- (void)setTall:(BOOL)tall;
- (void)setRich:(BOOL)rich;
- (void)setHandsome:(BOOL)handsome;

- (BOOL)tall;
- (BOOL)rich;
- (BOOL)handsome;
@end

YZPerson.m
#import "YZPerson.h"

//#define YZTallMask (1<<0)
//#define YZRichMask (1<<1)
//#define YZHandsomeMask (1<<2)

@interface YZPerson()
{
    struct {
        char tall : 1;
        char rich : 1;
        char handsome : 1;
    } _tallRichHandsome;
}

@end
@implementation YZPerson
- (void)setTall:(BOOL)tall
{
    _tallRichHandsome.tall = tall;
}

- (void)setRich:(BOOL)rich
{
    _tallRichHandsome.rich = rich;
}

- (void)setHandsome:(BOOL)handsome
{
    _tallRichHandsome.handsome = handsome;
}

- (BOOL)tall
{
    return _tallRichHandsome.tall;
}

- (BOOL)rich
{
    return _tallRichHandsome.rich;
}

- (BOOL)handsome
{
    return _tallRichHandsome.handsome;
}

@end

main.m
#import <Foundation/Foundation.h>
#import "YZPerson.h"

int main(int argc, const char * argv[]) {
    @autoreleasepool {
        YZPerson *person = [[YZPerson alloc] init];
        person.tall = NO;
        person.rich = NO;
        person.handsome = YES;
        NSLog(@"%d, %d, %d", person.tall, person.rich, person.handsome);
    }

    return 0;
}

在main函数里面打断点,通过命令行

(lldb) p/x &(person->_tallRichHandsome)
((anonymous struct) *) $0 = 0x0000000100769a68


(lldb) p/x person->_tallRichHandsome
((anonymous struct)) $1 = (tall = 0x00, rich = 0x00, handsome = 0x01)

含义是:p是取person的地址,x表以16进制表示
结果是:((anonymous struct)) $3 = (tall = 0x00, rich = 0x00, handsome = 0x01)
可以看到,可以使用这种位域技术实现一个字节里某个特定位代表一个BOOL值。

有个问题,打印结果却是0 0 -1
2020-05-19 10:09:53.726539+0800 block学习[83323:3266744] 0, 0, -1

明明handsome是0x01,怎么打印出来就是-1了呢?

这是因为,handsome位是0x01没有错,但是你打印的时候,handsome是以BOOL类型打印的,也就是打印的时候的handsome是BOOL类型,占一个字节。
0x01需要变为一个字节,0b1的一个位变为类似0b0000 0000的8个位
根据结果可以推敲,xcode做了用1覆盖的操作,即0b1前的空位都使用1覆盖,变为0b1111 1111,该值为-1。具体可以通过赋值打印,查看地址。

知识补充:深入学习0b1转换为8位为-1,也就是补码、源码等操作
当然,我们还可以通过取两次反,得到正确的BOOL值。
也可以不取两次反,而是将struct里面的char 类型值占两个字符即可。

共用体union

person.m文件

#import "YZPerson.h"

#define YZTallMask (1<<0)
#define YZRichMask (1<<1)
#define YZHandsomeMask (1<<2)

@interface YZPerson()
{
    union {
        char bits;
        struct {
            char tall : 1;
            char rich : 1;
            char handsome : 1;
        };
    } _tallRichHandsome;
}

@end
@implementation YZPerson
- (void)setTall:(BOOL)tall
{
    if (tall) {
        _tallRichHandsome.bits |= YZTallMask;
    }else{
        _tallRichHandsome.bits &= ~YZTallMask;
    }
}

- (void)setRich:(BOOL)rich
{
    if (rich) {
        _tallRichHandsome.bits |= YZRichMask;
    }else{
        _tallRichHandsome.bits &= ~YZRichMask;
    }
}

- (void)setHandsome:(BOOL)handsome
{
    if (handsome) {
        _tallRichHandsome.bits |= YZHandsomeMask;
    }else{
        _tallRichHandsome.bits &= ~YZHandsomeMask;
    }
}

- (BOOL)tall
{
    return !!(_tallRichHandsome.bits & YZTallMask);
}

- (BOOL)rich
{
    return !!(_tallRichHandsome.bits & YZRichMask);
}

- (BOOL)handsome
{
    return !!(_tallRichHandsome.bits & YZHandsomeMask);
}

@end

该共同体结合了前面两个的优点:
首先,在存取值的时候,使用的是位运算,而不是结构体的取值,可以增加效率。
然后,使用了结构体里面的位域技术。虽然这里面的位域作用只是为了用户看的方便,去掉不写也是没关系的。

知识补充:
union的基本操作
union与struct的共同点与区别
再反过来看isa_t的定义,是不是有点明白了呢。
里面部分参数代表的意义
在这里插入图片描述

小知识点:
Class类对象或者meta-class元类的地址二进制表示,最后三位都是0,十六进制表示最后一位是0或者8

为什么呢?
这是因为isa与上的ISA_MASK的值为0x0000000ffffffff8,最后一位是8,二进制表示8为1000,也就是所有的类对象和原类对象&ISA_MASK,二进制表示的后三位一定是0。

posted @ 2022-01-17 10:30  任淏  阅读(63)  评论(0编辑  收藏  举报