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。