Objective-C是一门动态性比较强的编程语言,跟C、C++等语言有着很大的不同。
什么叫动态性?
一般应用程序运行需要经过三步:编写代码 -> 编译 -> 运行,如果是C语言,编译之后方法就确定了,运行的时候就会调用那个方法。但是对于OC,由于其动态性,就算编译完了,也可以在程序运行时决定调用哪个方法,而且就算没有实现某个方法,也可在运行的时候动态生成某个方法。
Objective-C的动态性是由Runtime API来支撑的,Runtime API基本是开源的,源码在https://opensource.apple.com/tarballs/搜索objc,点击objc4文件夹进去,下载一个最新的(数字最大的)。
Runtime API提供的接口基本都是C语言的,源码由C\C++\汇编语言编写。
一. isa类型
在isa指针和superclass指针简单讲了,实例对象的isa&ISA_MASK得到类对象的地址值,类对象的isa&ISA_MASK得到元类对象的地址值。
要想学习Runtime,首先要了解它底层的一些常用数据结构,比如isa指针。在arm64架构之前,isa就是一个普通的指针,的确存储着类对象、元类对象的内存地址,从arm64架构开始,对isa进行了优化,变成了一个共用体(union)结构,还使用位域来存储更多的信息。
在objc4搜索“objc_object {”
struct objc_object {
private:
isa_t isa; //64位之前是Class isa
......
}
发现,64位之后isa是isa_t类型的,isa_t是共用体(在isa指针和superclass指针中,我们知道,64位之前isa是Class类型的)进入isa_t:
union isa_t
{
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;
uintptr_t magic : 6;
uintptr_t weakly_referenced : 1;
uintptr_t deallocating : 1;
uintptr_t has_sidetable_rc : 1;
uintptr_t extra_rc : 19;
};
};
二. 为什么要设计成共用体?
创建一个MJPerson对象,里面有三个属性,创建对象,并且打印对象占用内存大小。
int main(int argc, const char * argv[]) {
@autoreleasepool {
MJPerson *person = [[MJPerson alloc] init];
person.rich = YES; // 1字节
person.tall = NO; // 1字节
person.handsome = NO; // 1字节
NSLog(@"tall:%d rich:%d hansome:%d", person.isTall, person.isRich, person.isHandsome);
NSLog(@"%zd", class_getInstanceSize([MJPerson class])); //16字节
}
return 0;
}
打印为16字节(isa指针占8字节,BOOL值一共占用3字节,一共11字节,再加上内存对齐,所以是16字节)
有没有占用内存更小的方式实现给MJPerson添加三个BOOL属性呢?
方式一:char类型的位运算
8位二进制是一个字节(0b0000 0000),BOOL值只有0和1两个值,所以其实我们可以使用一个字节(最后3位)来表示这三个值。
原理很简单,先使用一个char类型的成员变量保存这三个属性:
- 取值:
使用掩码的按位与运算,想取出哪一位的值,就把哪一位置为1,其他为0。 - 设值:
使用掩码按位或运算,可以将某一位置为1,而不改变其他位,实现设置YES。
使用掩码取反的按位与运算,将某一位置为0,而不改变其他位,实现设置NO。
详细见代码和注释:
MJPerson.m
#import
@interface MJPerson : NSObject
//@property (assign, nonatomic, getter=isTall) BOOL tall;
//@property (assign, nonatomic, getter=isRich) BOOL rich;
//@property (assign, nonatomic, getter=isHansome) BOOL handsome;
- (void)setTall:(BOOL)tall;
- (void)setRich:(BOOL)rich;
- (void)setHandsome:(BOOL)handsome;
- (BOOL)isTall;
- (BOOL)isRich;
- (BOOL)isHandsome;
@end
MJPerson.h
#import "MJPerson.h"
//什么是位运算?与或非运算 (& | ~) 感叹号!是给BOOL值取反的
//按位“与”,都是1结果才是1,其他结果都是0
//开发中使用掩码的按位与运算来取出特定位的值,想取出哪一位的值,就把哪一位置为1,其他为0
// 0000 0111
//&0000 0100
//------
// 0000 0100
//掩码按位或运算,可以将某一位置为1,而不改变其他位
//掩码取反的按位与运算,将某一位置为0,而不改变其他位
// 掩码,一般用来做按位与(&)运算的
//#define MJTallMask 0b00000001
//#define MJRichMask 0b00000010
//#define MJHandsomeMask 0b00000100
#define MJTallMask (1<<0) //1往左位移0位
#define MJRichMask (1<<1) //1往左位移1位
#define MJHandsomeMask (1<<2) //1往左位移2位
@interface MJPerson()
{
char _tallRichHansome; //只占一个字节 0b 0000 0000
}
@end
@implementation MJPerson
- (instancetype)init
{
if (self = [super init]) {
_tallRichHansome = 0b00000100;
}
return self;
}
- (void)setTall:(BOOL)tall
{
if (tall) {
//掩码的按位或运算,将某一位置为1
_tallRichHansome |= MJTallMask;
} else {
//掩码取反的按位与运算,将某一位置为0
_tallRichHansome &= ~MJTallMask;
}
}
- (BOOL)isTall
{
//与运算之后,结果要么是0要么有值,但是我们需要的是BOOL值,怎么转换呢?
//取反,再取反
return !!(_tallRichHansome & MJTallMask);
}
- (void)setRich:(BOOL)rich
{
if (rich) {
_tallRichHansome |= MJRichMask;
} else {
_tallRichHansome &= ~MJRichMask;
}
}
- (BOOL)isRich
{
return !!(_tallRichHansome & MJRichMask);
}
- (void)setHandsome:(BOOL)handsome
{
if (handsome) {
_tallRichHansome |= MJHandsomeMask;
} else {
_tallRichHansome &= ~MJHandsomeMask;
}
}
- (BOOL)isHandsome
{
return !!(_tallRichHansome & MJHandsomeMask);
}
@end
上面即可实现使用一个字节给MJPerson对象添加三个BOOL属性。
方式二:结构体的位域
上面使用了一个char类型的位运算来实现,其实还可以使用结构体的位域来实现,也是占一个字节。
#import "MJPerson.h"
@interface MJPerson()
{
//char占一个字节,8位
//结构体支持位域,使用:1代表只占一位,这时候就不看左边的char了
//这时候整个结构体就占3位,系统不可能分配3位给它,所以至少占用一个字节
struct {
char tall : 1;
char rich : 1;
char handsome : 1;
} _tallRichHandsome; // 0b0000 0000
//最后三位放上面的值,而且前面的成员放在最右边(tall最后一位,rich倒数第二位,handsome倒数第三位)
}
@end
@implementation MJPerson
- (void)setTall:(BOOL)tall
{
_tallRichHandsome.tall = tall;
}
- (BOOL)isTall
{
//将0b1 -> 0b0000 0000
//BOOL值是8位二进制,但是我们存下来的是一位(0b1),如何将一位转成8位二进制呢?取反再取反
//如果不做两次取反操作,系统会自动把所有的0都b赋值为1,就是0b1 -> 0b1111 1111,就不对了
return !!_tallRichHandsome.tall;
}
- (void)setRich:(BOOL)rich
{
_tallRichHandsome.rich = rich;
}
- (BOOL)isRich
{
return !!_tallRichHandsome.rich;
}
- (void)setHandsome:(BOOL)handsome
{
_tallRichHandsome.handsome = handsome;
}
- (BOOL)isHandsome
{
return !!_tallRichHandsome.handsome;
}
@end
代码比较简单,可自行看注释。下面验证,执行代码:
int main(int argc, const char * argv[]) {
@autoreleasepool {
MJPerson *person = [[MJPerson alloc] init];
person.rich = YES;
person.tall = YES;
person.handsome = YES;
NSLog(@"tall:%d rich:%d hansome:%d", person.isTall, person.isRich, person.isHandsome);
}
return 0;
}
打断点,打印内存:
(lldb) p/x &(person->_tallRichHandsome)
((anonymous struct) *) $0 = 0x00000001005236a8
(lldb) x 0x00000001005236a8
0x1005236a8: 07 00 00 00 00 00 00 00 b1 41 ec ae ff ff 1d 00 .........A......
0x1005236b8: 8c 07 00 00 01 00 00 00 33 73 75 69 74 65 2f 49 ........3suite/I
打印_tallRichHandsome地址,查看内存,可以看出这个地址值为7,转成二进制就是0b0000 0111,说明代码没问题。
方式三:共同体
isa就是使用了共同体,共用体里面的成员共用一块内存。
如下,结构体:
struct Date {
int year; //4字节
int month; //4字节
int day; //4字节
};
它在内存中结构如下:
如果是共用体:
union Date {
int year; //4字节
int month; //4字节
int day; //4字节
};
在内存中结构如下:
先看如下代码,再看代码后的解释:
#import "MJPerson.h"
#define MJTallMask (1<<0)
#define MJRichMask (1<<1)
#define MJHandsomeMask (1<<2)
#define MJThinMask (1<<3)
@interface MJPerson()
{
union { //共用体
char bits; //1字节
struct {
char tall : 1; //1位
char rich : 1; //1位
char handsome : 1; //1位
char thin : 1; //1位
};//这个结构体仅仅是为了可读性,而且自始至终一直都在操作bits,删除这个结构体也不影响
} _tallRichHandsome;
}
@end
@implementation MJPerson
- (void)setTall:(BOOL)tall
{
if (tall) {
_tallRichHandsome.bits |= MJTallMask;
} else {
_tallRichHandsome.bits &= ~MJTallMask;
}
}
- (BOOL)isTall
{
return !!(_tallRichHandsome.bits & MJTallMask);
}
- (void)setRich:(BOOL)rich
{
if (rich) {
_tallRichHandsome.bits |= MJRichMask;
} else {
_tallRichHandsome.bits &= ~MJRichMask;
}
}
- (BOOL)isRich
{
return !!(_tallRichHandsome.bits & MJRichMask);
}
- (void)setHandsome:(BOOL)handsome
{
if (handsome) {
_tallRichHandsome.bits |= MJHandsomeMask;
} else {
_tallRichHandsome.bits &= ~MJHandsomeMask;
}
}
- (BOOL)isHandsome
{
return !!(_tallRichHandsome.bits & MJHandsomeMask);
}
- (void)setThin:(BOOL)thin
{
if (thin) {
_tallRichHandsome.bits |= MJThinMask;
} else {
_tallRichHandsome.bits &= ~MJThinMask;
}
}
- (BOOL)isThin
{
return !!(_tallRichHandsome.bits & MJThinMask);
}
@end
上面的代码使用了方式一的位运算来设值和取值,使用了方式二的结构体位域来实现代码的可读性。通过共用体将bits和结构体结合起来,而且自始至终一直都在操作bits,没有动结构体,结构体仅仅是为了可读性,所以不会影响bits里面的值,删除这个结构体也不影响。
这种方式就是巧妙的利用共用体,达到了代码可读性的目的。
三. isa_t的共用体
现在看isa_t的共用体你应该就很容易理解了。
union isa_t
{
Class cls;
uintptr_t bits;
# if __arm64__
# define ISA_MASK 0x0000000ffffffff8ULL
# define ISA_MAGIC_MASK 0x000003f000000001ULL
# define ISA_MAGIC_VALUE 0x000001a000000001ULL
struct {
//0,代表普通的指针,存储着Class、Meta-Class对象的内存地址
//1,代表优化过,使用位域存储更多的信息
uintptr_t nonpointer : 1;
//是否有设置过关联对象,如果没有,释放时会更快
uintptr_t has_assoc : 1;
//是否有C++的析构函数(.cxx_destruct),如果没有,释放时会更快
uintptr_t has_cxx_dtor : 1;
//存储着Class、Meta-Class对象的内存地址信息
uintptr_t shiftcls : 33;
//用于在调试时分辨对象是否未完成初始化
uintptr_t magic : 6;
//是否有被弱引用指向过,如果没有,释放时会更快
uintptr_t weakly_referenced : 1;
//对象是否正在释放
uintptr_t deallocating : 1;
//引用计数是否过大无法存储在isa中
//如果为1,那么引用计数会存储在一个叫SideTable的结构体的refcnts成员中,refcnts是个散列表
uintptr_t has_sidetable_rc : 1;
//里面存储的值是引用计数减1
uintptr_t extra_rc : 19;
};
};
- 可以看出64位之后,isa不仅仅用来存放地址值了,还用来存放更多的东西,所有的值都存放在bits里面。
- 冒号后面的数字就是占用多少位,加起来之后一共是64位,8个字节,这也和以前我们说的isa指针占用8字节相吻合。
- 上面的shiftcls就是存放类对象、元类对象的内存地址信息,可以发现有33位。
怎么取出地址值呢?
找到上面的“define ISA_MASK 0x0000000ffffffff8ULL”用计算器转换成二进制就是“0b111111111111111111111111111111111000”,发现就是33个1,就是用这个ISA_MASK来取出地址值的,详细操作请参考:isa指针和superclass指针
我们发现,ISA_MASK最后三位都是0,这就导致:二进制下,类对象、元类对象的地址值打印出来最后三位一定都是0
真机上才是arm64架构,我们将项目跑在真机上,打印验证一下:
- (void)viewDidLoad {
[super viewDidLoad];
NSLog(@"%p", [ViewController class]);
NSLog(@"%p", object_getClass([ViewController class]));
NSLog(@"%p", [MJPerson class]);
NSLog(@"%p", object_getClass([MJPerson class]));
}
打印:
0x107f6ed88
0x107f6edb0
0x107f6ee50
0x107f6ee28
上面是16进制,1位16进制相当于4位二进制,2位16进制相当于8位二进制,8位二进制是1个字节,所以2位16进制就是1个字节。观察上面,最后结尾不是0就是8,如果是0那末尾肯定是0b0000,如果是8那就是01000,所以验证了上面的结论。
简单验证:
关于isa里面存放的其他更多信息,这里简单验证下:
创建对象,打断点:
MJPerson *person = [[MJPerson alloc] init];
获取对象的isa
(lldb) p/x person->isa
(Class) $0 = 0x000000010231bf40 MJPerson
将16进制转换成二进制:
可以发现,一共64位,从上图的右下到左上,分别对应结构体中从上往下存储的值。
比如:右下最后一位0代表现在是普通的指针没有经过优化过(因为我没跑真机),右下倒数第二位0表示没有设置过关联对象,以此类推。
Demo地址:isa-ISA_MASK