object之isa指针详解

我们知道,判定是不是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是经过优化过的,使用位域存储更多信息.
    ...
    剩下的信息我就不一一对比了,有兴趣的可以自己对比试验.
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 202,607评论 5 476
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,047评论 2 379
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 149,496评论 0 335
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,405评论 1 273
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,400评论 5 364
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,479评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,883评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,535评论 0 256
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,743评论 1 295
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,544评论 2 319
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,612评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,309评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,881评论 3 306
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,891评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,136评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 42,783评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,316评论 2 342

推荐阅读更多精彩内容