面试题引发的思考:
Q: 简述isa
指针?
- 从
__arm64__
架构开始,isa
指针: - 不只是存储了 class对象 或 meta-class对象 的地址;
-
而是使用 共用体结构 存储了更多信息:
其中shiftcls
存储了 class对象 或 meta-class对象 的地址;
需要shiftcls
同ISA_MASK
进行按位&
运算取出。
Objective-C 扩展了 C 语言,并加入了面向对象特性和 Smalltalk 式的消息传递机制。而这个扩展的核心是一个用 C 和 编译语言 写的 Runtime 库。它是 Objective-C 面向对象和动态机制的基石。
要想学习Runtime,首先要了解它底层的一些常用数据结构,比如isa
指针:
- 每个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_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
值包含0
或1
,只需要1个二进制位就可以表示出来,而现在需要占用1个字节共8个二进制位。
可以使用1个字节中的3个二进制位表示3个BOOL
值,这样可以很大程度上节省内存空间。
如果声明属性,系统就会自动生成成员变量,占用的内存会大于1位;所以不可以声明属性,需要手动实现每个属性的set
方法和get
方法。
现在添加一个char
类型的成员变量,char
变量占用一个字节(8位)的内存空间,使用最后3位来存储3个BOOL
值。
(2) 位运算
-
补码(负数是以补码的形式表示)
十进制正整数转换为二进制数:除2
取余即可;
十进制负整数转换为二进制数:除2
取余,取反加1
;
// -10用二进制表示
0b0000 0000 0000 1010 -(10除2取余)
0b1111 1111 1111 0101 -(取反)
0b1111 1111 1111 0110 -(加1)
0b1111 1111 1111 0110 -(得出-10的二进制)
-
按位与(&)
同真为真,其余为假:清零特定位、取出特定位;
// 按位与(&)
0b0000 1111 0000 1111
& 0b0000 0000 1111 1111
---------------------
0b0000 0000 0000 1111
-
按位或(|)
同假为假,其余为真:特定位置1
;
// 按位或(|)
0b0000 1111 0000 1111
| 0b0000 0000 1111 1111
---------------------
0b0000 1111 1111 1111
-
按位异或(^)
异值为真,同值为假:特定位取反、交换两变量的值;
// 按位异或(^)
0b0000 1111 0000 1111
^ 0b0000 0000 1111 1111
---------------------
0b0000 1111 1111 0000
-
取反(~)
按位取反
// 取反(~)
~ 0b0000 1111 0000 1111
---------------------
0b1111 0000 1111 0000
-
左移(<<)
按位左移,高位丢弃,低位补0
;
// 左移(<<)两位
0b1111 0000 0000 1111 << 2
---------------------
0b1100 0000 0011 1100
-
右移(>>)
按位右移,高位正数补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_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对象的地址。