iOS底层原理 - 探寻Runtime本质 之 isa

面试题引发的思考:

Q: 简述isa指针?

  • __arm64__架构开始,isa指针:
  • 不只是存储了 class对象meta-class对象 的地址;
  • 而是使用 共用体结构 存储了更多信息:
    其中 shiftcls 存储了 class对象meta-class对象 的地址;
    需要 shiftclsISA_MASK 进行 按位& 运算取出。

Objective-C 扩展了 C 语言,并加入了面向对象特性和 Smalltalk 式的消息传递机制。而这个扩展的核心是一个用 C 和 编译语言 写的 Runtime 库。它是 Objective-C 面向对象和动态机制的基石。

要想学习Runtime,首先要了解它底层的一些常用数据结构,比如isa指针:

iOS底层原理 - OC对象的本质(二)可知:

  • 每个OC对象都有一个isa指针;
  • instance对象的isa指向class对象;
  • class对象的isa指向meta-class对象;
  • OC对象的isa指针并不是直接指向class对象或者meta-class对象,而是需要&ISA_MASK通过位运算才能获取到类对象或者元类对象的地址;
    a> 在__arm64__架构之前,isa就是一个普通的指针,存储着Class对象、Meta-Class对象的内存地址;
    b> 从__arm64__架构开始,对isa进行了优化,变成了一个共用体(union)结构,还使用位域来存储更多的信息。

进入OC源码查看isa指针,进行更深入的了解:

isa结构

由上图可知:
isa指针其实是一个isa_t类型的共用体;
此共用体中包含一个结构体;
此结构体内部定义了若干变量,变量后面的值则表示该变量占用的位数,即位域。

接下来一步步分析共用体的优势。


(1) 探究

// TODO: -----------------  Person类  -----------------
@interface Person : NSObject
// 声明属性,系统会自动生成成员变量
@property (nonatomic, assign, getter=isTall) BOOL tall;
@property (nonatomic, assign, getter=isRich) BOOL rich;
@property (nonatomic, assign, getter=isHandsome) BOOL handsome;
@end

@implementation Person
@end

// TODO: -----------------  main  -----------------
int main(int argc, const char * argv[]) {
    @autoreleasepool {
        Person *person  = [[Person alloc] init];
        person.tall = YES;
        person.rich = NO;
        person.handsome = YES;
        NSLog(@"%zd", class_getInstanceSize([Person class]));
     }
    return 0;
}

// 打印结果
Demo[1234:567890] 16

由以上代码可知:
isa指针占8个字节,3个BOOL类型的分别占1个字节,一共11个字节。由于内存对齐原则,所以Person类对象所占内存应该为16个字节。

BOOL值包含01,只需要1个二进制位就可以表示出来,而现在需要占用1个字节共8个二进制位。
可以使用1个字节中的3个二进制位表示3个BOOL值,这样可以很大程度上节省内存空间。

如果声明属性,系统就会自动生成成员变量,占用的内存会大于1位;所以不可以声明属性,需要手动实现每个属性的set方法和get方法。

现在添加一个char类型的成员变量,char变量占用一个字节(8位)的内存空间,使用最后3位来存储3个BOOL值。

1个char值存储3个BOOL值

(2) 位运算

  1. 补码(负数是以补码的形式表示)
    十进制正整数转换为二进制数:除2取余即可;
    十进制负整数转换为二进制数:除2取余,取反加1
// -10用二进制表示
0b0000 0000 0000 1010 -(10除2取余)
0b1111 1111 1111 0101 -(取反)
0b1111 1111 1111 0110 -(加1)
0b1111 1111 1111 0110 -(得出-10的二进制)
  1. 按位与(&)
    同真为真,其余为假:清零特定位、取出特定位;
// 按位与(&)
  0b0000 1111 0000 1111
& 0b0000 0000 1111 1111
  ---------------------
  0b0000 0000 0000 1111
  1. 按位或(|)
    同假为假,其余为真:特定位置1
// 按位或(|)
  0b0000 1111 0000 1111
| 0b0000 0000 1111 1111
  ---------------------
  0b0000 1111 1111 1111
  1. 按位异或(^)
    异值为真,同值为假:特定位取反、交换两变量的值;
// 按位异或(^)
  0b0000 1111 0000 1111
^ 0b0000 0000 1111 1111
  ---------------------
  0b0000 1111 1111 0000
  1. 取反(~)
    按位取反
// 取反(~)
~ 0b0000 1111 0000 1111
  ---------------------
  0b1111 0000 1111 0000
  1. 左移(<<)
    按位左移,高位丢弃,低位补0
// 左移(<<)两位
  0b1111 0000 0000 1111 << 2
  ---------------------
  0b1100 0000 0011 1100
  1. 右移(>>)
    按位右移,高位正数补0,负数补1
// 右移(>>)两位
  0b1111 0000 0000 1111 >> 2
  ---------------------
  0b0011 1100 0000 0011

(3) 手动实现属性的set方法和get方法

// TODO: -----------------  Person类  -----------------
//#define TallMask 0b00000001 // 1
//#define RichMask 0b00000010 // 2
//#define HandsomeMask 0b0000100 // 4
#define TallMask (1<<0)
#define RichMask (1<<1)
#define HandsomeMask (1<<2)

@interface Person : NSObject {
    char _tallRichHandsome;  // 0b0000 0000
}
- (void)setTall:(BOOL)tall;
- (void)setRich:(BOOL)rich;
- (void)setHandsome:(BOOL)handsome;
- (BOOL)isTall;
- (BOOL)isRich;
- (BOOL)isHandsome;
@end

@implementation Person
- (void)setTall:(BOOL)tall {
    if (tall) { // 按位或 - 特定位置1
        _tallRichHandsome |= TallMask;
    } else { // 掩码先取反,后按位与 - 特定位置0
        _tallRichHandsome &= ~TallMask;
    }
}
- (void)setRich:(BOOL)rich {
    if (rich) {
        _tallRichHandsome |= RichMask;
    } else {
        _tallRichHandsome &= ~RichMask;
    }
}
- (void)setHandsome:(BOOL)handsome {
    if (handsome) {
        _tallRichHandsome |= HandsomeMask;
    } else {
        _tallRichHandsome &= ~HandsomeMask;
    }
}
- (BOOL)isTall {
    // 按位与 - 取出特定位
    // 两次取反!,强制转换成BOOL类型
    return !!(_tallRichHandsome & TallMask);
}
- (BOOL)isRich {
    return !!(_tallRichHandsome & RichMask);
}
- (BOOL)isHandsome {
    return !!(_tallRichHandsome & HandsomeMask);
}
@end

// TODO: -----------------  main  -----------------
int main(int argc, const char * argv[]) {
    @autoreleasepool {
        Person *person  = [[Person alloc] init];
        person.tall = YES;
        person.rich = NO;
        person.handsome = YES;
        NSLog(@"tall: %d, rich: %d, handsome: %d", person.isTall, person.isRich, person.isHandsome);
    }
    return 0;
}

// 打印结果
Demo[1234:567890] tall: 1, rich: 0, handsome: 1

上述代码可以正常存值和取值,但是可拓展性和可读性差。


(4) 位域

位域声明:位域名 : 位域长度

使用位域需要注意以下3点:

  • 如果一个字节所剩空间不够存放另一位域时,应从下一单元起存放该位域;
    也可以有意使某位域从下一单元开始。
  • 位域的长度不能大于数据类型本身的长度;
    比如int类型就不能超过32位二进制位。
  • 位域可以无位域名,这时它只用来作填充或调整位置;
    无名的位域是不能使用的。
// TODO: -----------------  Person类  -----------------
@interface Person : NSObject {
    // 位域,tall、rich、handsome按序各占1个二进制位
    struct {
        char tall : 1;
        char rich : 1;
        char handsome : 1;
    } _tallRichHandsome;
}
- (void)setTall:(BOOL)tall;
- (void)setRich:(BOOL)rich;
- (void)setHandsome:(BOOL)handsome;
- (BOOL)isTall;
- (BOOL)isRich;
- (BOOL)isHandsome;
@end

@implementation Person
- (void)setTall:(BOOL)tall {
    _tallRichHandsome.tall = tall;
}
- (void)setRich:(BOOL)rich {
    _tallRichHandsome.rich = rich;
}
- (void)setHandsome:(BOOL)handsome {
    _tallRichHandsome.handsome = handsome;
}
- (BOOL)isTall {
    // _tallRichHandsome.tall == 0b1
    // 0b1111 1111 == -1
    return !!_tallRichHandsome.tall;
}
- (BOOL)isRich {
    return !!_tallRichHandsome.rich;
}
- (BOOL)isHandsome {
    return !!_tallRichHandsome.handsome;
}
@end

// 打印结果
Demo[1234:567890] tall: 1, rich: 0, handsome: 1

上述代码使用结构体的位域,不需要再使用掩码,缺点是效率比使用位运算时低。


(5) 共用体

// TODO: -----------------  Person类  -----------------
#define TallMask (1<<0)
#define RichMask (1<<1)
#define HandsomeMask (1<<2)

@interface Person : NSObject {
    union { // 共用体
        char bits;
        //struct 增加代码可读性,可注释掉
        struct {
            char tall : 1;
            char rich : 1;
            char handsome : 1;
        };
    }_tallRichHandsome;
}
- (void)setTall:(BOOL)tall;
- (void)setRich:(BOOL)rich;
- (void)setHandsome:(BOOL)handsome;
- (BOOL)isTall;
- (BOOL)isRich;
- (BOOL)isHandsome;
@end

@implementation Person
- (void)setTall:(BOOL)tall {
    if (tall) {
        _tallRichHandsome.bits |= TallMask;
    } else {
        _tallRichHandsome.bits &= ~TallMask;
    }
}
- (void)setRich:(BOOL)rich {
    if (rich) {
        _tallRichHandsome.bits |= RichMask;
    } else {
        _tallRichHandsome.bits &= ~RichMask;
    }
}
- (void)setHandsome:(BOOL)handsome {
    if (handsome) {
        _tallRichHandsome.bits |= HandsomeMask;
    } else {
        _tallRichHandsome.bits &= ~HandsomeMask;
    }
}
- (BOOL)isTall {
    // 两次取反!,强制转换成BOOL类型
    return !!(_tallRichHandsome.bits & TallMask);
}
- (BOOL)isRich {
    return !!(_tallRichHandsome.bits & RichMask);
}
- (BOOL)isHandsome {
    return !!(_tallRichHandsome.bits & HandsomeMask);
}
@end

// 打印结果
Demo[1234:567890] tall: 1, rich: 0, handsome: 1

上述代码使用共用体对数据进行位运算取值和赋值,效率高同时占用内存少,代码可读性高。


(6) isa详解

经过以上分析,我们可以清晰地了解到位运算、位域以及共用体的相关知识。
下面我们深入分析isa的结构:

isa结构

由上图可知:
isa_t共用体存储了8个字节64位的值,所有信息都存储在bits中,这些值在结构体中展现出来,这些值通过对bits进行掩码位运算取出。

名称 作用
nonpointer 0代表普通的指针,存储着Class,Meta-Class对象的内存地址;1代表优化过,使用位域存储更多的信息
has_assoc 是否有设置过关联对象,如果没有,释放时会更快
has_cxx_dtor 是否有C++析构函数,如果没有,释放时会更快
shiftcls 存储着Class、Meta-Class对象的内存地址信息
magic 用于在调试时分辨对象是否未完成初始化
weakly_referenced 是否有被弱引用指向过,如果没有,释放时会更快
deallocating 对象是否正在释放
has_sidetable_rc 引用计数器是否过大无法存储在isa中;如果为1,那么引用计数会存储在一个叫SideTable的类的属性中
extra_rc 里面存储的值是引用计数器减1

重点介绍一下shiftcls

  • shiftcls存储着Class、Meta-Class对象的内存地址信息;
  • 通过bits & ISA_MASK取得shiftcls的值;
  • 而掩码ISA_MASK值为0x0000000ffffffff8ULL,说明Class、Meta-Class对象的内存地址值后三位一定为0

接下来验证一下:

int main(int argc, const char * argv[]) {
    @autoreleasepool {
        Person *person  = [[Person alloc] init];
        person.tall = YES;
        person.rich = NO;
        person.handsome = YES;
        NSLog(@"tall: %d, rich: %d, handsome: %d", person.isTall, person.isRich, person.isHandsome);
        NSLog(@"class: %p, meta-class: %p", [Person class], object_getClass([Person class]));
    }
    return 0;
}
// 打印结果
Demo[1234:567890] tall: 1, rich: 0, handsome: 1
Demo[1234:567890] class: 0x100001260, meta-class: 0x100001238

由打印结果可知:
class对象的地址值为0x100001260,尾数是0;meta-class对象的地址值为0x100001238,尾数是8;在16进制下,内存地址的后三位都为0;验证结束。


总结可知:

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

推荐阅读更多精彩内容