• 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。

  • 相关阅读:
    连通域搜索
    识别深色浅色
    新年,博客搬家了!!!
    C++11 —— 使用 thread 实现线程池
    自己实现的网络字节序转换函数
    GUI 编程 —— QT 的 QSlider 鼠标点击定位问题
    单生产者/单消费者 的 FIFO 无锁队列
    用模板类特化的方式实现工厂模式
    C++11 —— 简易的旋转锁类
    C++11 —— 获取 tuple 参数列表中指定数据类型的索引位置
  • 原文地址:https://www.cnblogs.com/r360/p/15812442.html
Copyright © 2020-2023  润新知