iOS-Runtime1-isa存储信息分析

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. 取值:
    使用掩码的按位与运算,想取出哪一位的值,就把哪一位置为1,其他为0。
  2. 设值:
    使用掩码按位或运算,可以将某一位置为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字节
};

它在内存中结构如下:

iOS-Runtime1-isa存储信息分析_第1张图片
结构体.png

如果是共用体:

union Date {
    int year; //4字节
    int month; //4字节
    int day; //4字节
};

在内存中结构如下:

iOS-Runtime1-isa存储信息分析_第2张图片
共用体.png

先看如下代码,再看代码后的解释:

#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;
    };
};
  1. 可以看出64位之后,isa不仅仅用来存放地址值了,还用来存放更多的东西,所有的值都存放在bits里面。
  2. 冒号后面的数字就是占用多少位,加起来之后一共是64位,8个字节,这也和以前我们说的isa指针占用8字节相吻合。
  3. 上面的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进制转换成二进制:

iOS-Runtime1-isa存储信息分析_第3张图片
16->2.png

可以发现,一共64位,从上图的右下到左上,分别对应结构体中从上往下存储的值。
比如:右下最后一位0代表现在是普通的指针没有经过优化过(因为我没跑真机),右下倒数第二位0表示没有设置过关联对象,以此类推。

Demo地址:isa-ISA_MASK

你可能感兴趣的:(iOS-Runtime1-isa存储信息分析)