美文网首页iOSiOS底层探究
object之isa指针详解

object之isa指针详解

作者: 小心韩国人 | 来源:发表于2019-01-31 10:10 被阅读29次

    我们知道,判定是不是OC对象的本质就是看是否含有isa指针,在ARM64架构之前,objc_objectisa指针就是一个class类型,存储的就是一个指针,而ARM64系统之后,对isa进行了优化,变成了一个共用体(union),还是用位域来存储更多信息.

    共用体
    今天我们就来研究一下共同体(union)和位域.
    我们先通过一个小场景来开始今天的内容,我们先创建一个Person类,类中有三个BOOL类型的属性:tall , rich ,handsome,分别表示高,富,帅.
    然后通过class_getInstanceSize查看这个类对象占用多少字节,发现打印输出是:16.为什么是16个字节呢?因为3个BOOL类型的属性占用3个字节,isa指针占用8个字节,一共占用11个字节,再内存对齐以后,就是16个字节.
    这样我们就会发现一个问题,三个BOOL类型的属性占用了3个字节,其实BOOL类型属性本质就是0 或 1,只占一位,也就是说3个BOOL属性放在一个字节就可以搞定.比如说有一个字节的内存0x0000 0000我们用最后3位分别存放高,富,帅,如图所示:

    ❓思考一下:怎样才能做到只用一位去存放三个属性呢❓
    只能通过位运算做到了.我们先把属性相关的代码删掉,(因为如果添加了属性就会自动生成setter,getter方法) 再手动添加setter,getter方法.然后再在.m中声明一个成员变量_tallRichHandsome存储这三个值:
    @interface Person : NSObject
    
    //@property (nonatomic,assign)BOOL tall;
    //@property (nonatomic,assign)BOOL rich;
    //@property (nonatomic,assign)BOOL handSome;
    
    - (void)setTall:(BOOL)tall;
    - (void)setRich:(BOOL)rich;
    - (void)setHandSome:(BOOL)handsome;
    
    - (BOOL)isTall;
    - (BOOL)isRich;
    - (BOOL)isHandSome;
    
    @end
    
    
    @interface Person ()
    {
    char _tallRichHandsome; //0b00000000
    }
    @end
    
    
    • 取值用 &运算符.按位与是两个为 1 ,结果才为1.如果我们想要获取某一位的值,只需要把那一位设置成1,其他位设置为0,就可以取出特定位的值.
      所以我们只需要在getter方法中按位与一个特定的值即可,比如我们想要获取tall,只需按位与0b 0000 0001;获取rich,就按位与0b 0000 0010,但是这样运算得出的结果并不是一个布尔值,而我们是想要BOOL类型,所以我们可以使用 return (BOOL)(_tallRichHandsome & 0b00000001)转换类型,也可以这样return !!(_tallRichHandsome & 0b00000001),使用两个!!获取真实布尔值.
    - (BOOL)isTall{
        return !!(_tallRichHandsome & 0b00000001);
    }
    

    检验一下这种写法的效果,我们在Person类的init方法中给_tallRichHandsome赋值为0b00000101,代表高为1,富为0,帅为1,然后在.m中打印看看:

    打印结果
    可以看到结果完全正确,更改_tallRichHandsome值后再打印也完全正确.我们使用掩码再继续优化一下上面的写法,把位运算的值宏定义一下:
    #define tallMask 0b00000001
    #define richMask 0b00000010
    #define handsomeMask 0b00000100
    

    Mask 掩码,一般用来按位与运算,最好用括号括起来,怕影响到运算结果
    继续优化:

    #define tallMask (1<<0) //1 左移 0 位
    #define richMask (1<<1)// 1 左移 1 位
    #define handsomeMask (1<<2) // 1 左移 2 位
    
    • 刚才验证了取值,接下来研究一下如何赋值.赋值分为两种情况:如果赋值YES,就使用 按位或运算符(|).按位或表示一个为 1 ,结果就为 1 .如果赋值NO,就把目标位设置为 0 ,其他位全设置YES.
    1. 比如赋值为YES:比如原始值为0b 0000 0101,标识高:YES , 帅:YES.现在要把富也设置为YES,也就是0b0000 0010,其他位置不变,就要使用按位或|:
    2. 如果赋值为NO,比如原始值为0b 0000 0111,标识高:YES ,富:YES 帅:YES.现在要把高也设置为NO,其他不变.结果就是0b0000 0110,那就应该把目标位设置为0,其他位设置为1,使用按位取反运算符~,掩码就应该是0b1111 1110,然后再按位与&:

      代码如下:
    - (void)setRich:(BOOL)rich{
        if (rich) {
            _tallRichHandsome |= richMask;
        }else{
            _tallRichHandsome &= ~richMask;
        }
    }
    

    测试结果完全正常:


    现在就能满足我们刚开始的目的了,但是这种做法不好扩展也不利于阅读,我们继续完善一下,使用位域这种技术.
    我们把刚才的代码更改一下,把char _tallRichHandsome更改为
    struct{
            char tall : 1;//位域 占1位
            char rich : 1;
            char handsome : 1;
        }_tallRichHandsome;
    

    注意:char tall : 1是位域的格式,表示只占一位
    相应的setter , getter方法更改如下:

    - (void)setHandSome:(BOOL)handsome{
        _tallRichHandsome.handsome = handsome;
    }
    
    - (BOOL)isHandSome{
        return _tallRichHandsome.handsome;
    }
    

    然后运行一下查看结果:


    可以看到给tall赋值后的确发生了变化,但是为什么是 -1呢?我们刚才给tall赋的值,然后结构体中的顺序是tall , rich , handsome,内存中的位置会按照结构体中的顺序从右往左开始存放,也就是现在现在内存中的值应该是0b 0000 0001,ok,我们来验证一下:

    01的二进制就是0000 0001,完全符合我们刚在的结论,那为什么打印出来的确是 -1呢?
    我们看看getter方法代码:
    - (BOOL)isTall{
       
       return _tallRichHandsome.tall;
        
    }
    

    getter方法返回的是BOOL类型,占用一个字节(8位),而我们_tallRichHandsome.tall取出来的却是一位,把一位的1,存放到8位的地址中0b 0000 0000,1 放在最后一位,前七位全补成1,结果就成了0b1111 1111,就成了无符号的 255 ,有符号的-1.关于这个结论我们也可以验证一下:

    的确是 255
    所以我们可以还和刚才一样使用!!取出真实布尔值,也可以把char tall : 1改成char tall : 2,让位域占两位.
    struct{
            char tall : 2;//位域 占1位
            char rich : 2;
            char handsome : 2;
        }_tallRichHandsome;
    
    运行结果如下:
    周星驰高吗?1 
    富吗?0    
    帅吗0
    

    发现这样就不是-1了,其实这就是位域的一个小特点,我们取出tall的值01,存放到一个字节0000 0000中,结果就是0000 0001,它会把符号位0补充到其他6位中,结果就是正常的.

    • 到目前为止我们尝试了两种方法达到这种目的,一种是使用位运算,另一种是使用结构体的位域.那我们能不能综合前两种方法,对结构体进行位运算呢?我们来试试:

      如图所示,结构体根本就无法进行位运算,那怎么办呢?我们可以参考一下苹果大大优化isa指针的做法,使用共用体(union):
    union{
            char bits;
            struct{
                char tall : 1;//位域 占1位
                char rich : 1;
                char handsome : 1;
            };
        }_tallRichHandsome;
    

    我们运行一下发现完全正常,我们把结构体删掉在运行一下:

    union{
            char bits;
        }_tallRichHandsome;
    

    发现也完全正常,其实这里的结构体完全就是增加代码的可读性.这种做法其实就是将位运算和结构体的位域结合在一起,利用结构体的位域增加可读性,利用位运算达到想取哪里去哪里的目的.
    下面我们详细介绍一下共用体(union),我们将结构体struct和共用体union对比介绍.比如说现在有一个结构体Date和共用体Date:

        struct{
            int year;
            int month;
            int day;
        }Date;
        
        union{
            int year;
            int month;
            int day;
        }Day;
    

    他们的内存结构如图所示:


    可以看到,结构体的内存都是独立的,每个成员占用4个字节,一共占用12个字节;而共用体的内存是连续的,所有成员公用4个字节的内存,共用体内存的大小取决于最大的成员所分配的内存.
    下图就证明了共用体的成员是共用一块内存的,我们给year赋值,然后打印month,结果值确是year的值:

    总结:在ARM64位之前isa就是个普通的指针,实例对象的isa指向类对象,类对象的isa指向实例元类对象,在ARM64位之后,isa进行了优化,采取了共用体的结构,使用64位的内存数据存储更多的信息,其中的33位存储具体的地址值.

    关于位运算的一些扩展

    我们在项目中肯定用过 KVO,[self addObserver:self forKeyPath:@"view" options:NSKeyValueObservingOptionNew | NSKeyValueObservingOptionOld context:nil];它的内部是怎么样处理我们传进的多个值得呢?我们可以模仿一下:

    typedef enum{
        Monday = 1,    //0b 0000 0001
        Tuesday = 2,   //0b 0000 0010
        Wedensday = 4, //0b 0000 0100
        Thursday = 8,  //0b 0000 1000
        Friday = 16,   //0b 0001 0000
        Saturday = 32, //0b 0010 0000
        Sunday = 64,   //0b 0100 0000
    }Week;
    

    我们定义一个Week类型的结构体,周一 至 周日,并设置初始值,大家可以看到他们的初始值是有规律的,分别是2的0次方,1次方,2次方...6次方.对应的二进制也是1<<0,1<<1 ... 1<<6.然后我们在定义一个方法- (void)setWeek:(Week)week再调用这个方法[self setWeek:Saturday | Sunday];在这个方法内判断如果是周末就打印 打游戏,是工作日就打印 敲代码.怎么实现呢?
    首先我们分析一下[self setWeek:Saturday | Sunday];我们知道或运算是一个为1结果就为1,所以Saturday | Sunday结果应该是:

      0010 0000
    | 0100 0000
    ---------------
      0110 0000
    

    然后我们再用这个结果0110 0000按位与上 Saturday , Sunday,如果结果不为0,就说明符合条件:

    - (void)setWeek:(Week)week{
        if (week & Saturday) {
            NSLog(@"Saturday 打游戏");
        }
        if (week & Sunday) {
            NSLog(@"Sunday 打游戏");
        }
    }
    ===============================================
    2019-01-31 11:00:59.609826+0800 MultiThread[2195:466594] Saturday 打游戏
    2019-01-31 11:00:59.609980+0800 MultiThread[2195:466594] Sunday 打游戏
    

    这样就实现了我们的需求,我们把[self setWeek:Saturday | Sunday];改为[self setWeek:Saturday + Sunday]; 看看效果:

    [self setWeek:Saturday + Sunday];
    ===============================================
    - (void)setWeek:(Week)week{
        if (week & Saturday) {
            NSLog(@"Saturday 打游戏");
        }
        if (week & Sunday) {
            NSLog(@"Sunday 打游戏");
        }
    }
    ===============================================
    2019-01-31 11:12:10.965097+0800 MultiThread[2291:476407] Saturday 打游戏
    2019-01-31 11:12:10.965285+0800 MultiThread[2291:476407] Sunday 打游戏
    

    结果也完全一样,说明+|在这里是等价的,但是要注意:只有当他们的初始值是2的n次方的时候才能使用+号,一般不建议使用+,这样会显得你很low所以我们还是使用|
    苹果的源码也是这样来设计实现的,我们看看:

    NSKeyValueObservingOptionNew =     0x01, // 1
    NSKeyValueObservingOptionOld =     0x02,// 2
    NSKeyValueObservingOptionInitial = 0x04,// 4
    NSKeyValueObservingOptionPrior =   0x08,//8
    

    ok,前面讲了这么多的掩码,位域,共用体等等其实都是为了铺垫,都是为了引出最终的 BOSS ==> isa,现在我们就赖仔细看看isa指针.
    首先查看isa源码:

    # if __arm64__
    #   define ISA_MASK        0x0000000ffffffff8ULL
    #   define ISA_MAGIC_MASK  0x000003f000000001ULL
    #   define ISA_MAGIC_VALUE 0x000001a000000001ULL
        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;
    #       define RC_ONE   (1ULL<<45)
    #       define RC_HALF  (1ULL<<18)
        };
    
    对应注解
    现在带入到项目中验证一下,注意验证的时候要使用真机,因为模拟器中存储的位置和真机的不一样.
    首先我们打印出一个ViewController的内存地址:

    然后用计算器查看这个地址的二进制:

    我们对比上面的注释图查看分析一下这个二进制:
    • 第0位也就是最后边的1是nonpointer的值,如果为0表示这个isa就是一个普通的指针,值存储着类对象或者元类对象的内存地址;如果为1则表示这个isa是经过优化过的,使用位域存储更多信息.
      ...
      剩下的信息我就不一一对比了,有兴趣的可以自己对比试验.

    相关文章

      网友评论

        本文标题:object之isa指针详解

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