Runtime原理探究(一)—— isa的深刻体会(苹果对isa的优化)


Runtime系列文章

Runtime原理探究(一)—— isa的深刻体会(苹果对isa的优化)面试

Runtime原理探究(二)—— Class结构的深刻分析编程

Runtime原理探究(三)—— OC Class的方法缓存cache_t缓存

Runtime原理探究(四)—— 刨根问底消息机制markdown

Runtime原理探究(五)—— super的本质架构

Runtime原理探究(六)—— 面试题中的Runtime编程语言


☕️☕️本文篇幅比较长,创做的目的并非为了在简书上刷赞和阅读量,而是为了本身往后温习知识所用。若是有幸被你发现这篇文章,而且引发了你的阅读兴趣,请休息充分,静下心来,精力充足地开始阅读,但愿这篇文章能对你有所帮助。如发现任何有误之处,肯请留言纠正,谢谢。☕️☕️ide

如何理解Objective-C的动态特性?

不少静态编程语言,编写完代码后,通过编译链接生成可执行文件,最后就能够在电脑上运行起来。函数

以C语言为例oop

void test() {
    printf("Hello World");
}
int main() {
    test();
}
复制代码

以上代码通过编译以后,main函数里面就必定会调用test(),而test()的实现也必定会是和代码中写的同样,这些在编译完成那一刻就决定了,运行过程当中不会发生改变的。C能够说就是典型的静态语言。布局

与之相比,Objective-C就能够在运行阶段修改以前编译阶段肯定好的一些函数和方法。

************************main.m*************************
#import <Foundation/Foundation.h>
#import "CLPerson.h"

int main(int argc, const char * argv[]) {
    @autoreleasepool {
        CLPerson *person = [[CLPerson alloc] init];
        [person test];
    }
    return 0;
}

***********************CLPerson.h************************
#import <Foundation/Foundation.h>
@interface CLPerson : NSObject
- (void)test;
@end


***********************CLPerson.m************************
#import "CLPerson.h"
@implementation CLPerson
- (void)test {
    NSLog(@"%s", __func__);
}

- (void)abc {
    
}
@end
复制代码

如上面所示代码,[person test];这句代码,在运行阶段,能够调用CLPersontest方法,也能够经过OC的动态特性,使其最终调用别的方法,例如abc方法,甚至,还能够调用另一个类的方法。除此以外,OC还能够在程序运行阶段,给类增长方法等,这就是所谓的动态特性

Runtime简介

  • Objective-C是一门动态性比较强的编程语言,根C、C++等语言有很大不一样
  • Objective-C的动态性是由Runtime API来支撑的
  • Runtime API提供的接口基本都是C语言的,源码由C/C++/汇编语言编写

isa详解

深刻Runtime以前,先要解决一个比较重要的概念——isa。在早期的Runtime里面,isa指针直接指向class/meta-class对象的地址,isa就是一个普通的指针。

后来,苹果从ARM64位架构开始,对isa进行了优化,将其定义成一个共用体(union)结构,结合 位域 的概念以及 位运算 的方式来存储更多类相关信息。isa指针须要经过与一个叫ISA_MASK的值(掩码)进行二进制&运算,才能获得真实的class/meta-class对象的地址。接下来,就具体探究一下苹果到底是怎么优化的。

首先从源码角度,对比一下变化isa优化先后的变化

***************************************
typedef struct objc_class *Class;

typedef struct objc_object {
	Class isa;
} *id;
复制代码

上面是64位以前,objc_object的定义如上,isa直接指向objc_class

再看看优化后objc_object的定义

struct objc_object {
private:
    isa_t isa;

public:

复制代码

arm64开始,isa的类型变成了isa_t,这是什么鬼?这个就是接下来讨论的重点,先看一下它的源码

union isa_t {
    isa_t() { }
    isa_t(uintptr_t value) : bits(value) { }

    Class cls;
    uintptr_t bits;

#if SUPPORT_PACKED_ISA

   

# if __arm64__
# define ISA_MASK 0x0000000ffffffff8ULL
# define ISA_MAGIC_MASK 0x000003f000000001ULL
# define ISA_MAGIC_VALUE 0x000001a000000001ULL
    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;
# define RC_ONE (1ULL<<45)
# define RC_HALF (1ULL<<18)
    };

# elif __x86_64__
# define ISA_MASK 0x00007ffffffffff8ULL
# define ISA_MAGIC_MASK 0x001f800000000001ULL
# define ISA_MAGIC_VALUE 0x001d800000000001ULL
    struct {
        uintptr_t nonpointer        : 1;
        uintptr_t has_assoc         : 1;
        uintptr_t has_cxx_dtor      : 1;
        uintptr_t shiftcls          : 44; // MACH_VM_MAX_ADDRESS 0x7fffffe00000
        uintptr_t magic             : 6;
        uintptr_t weakly_referenced : 1;
        uintptr_t deallocating      : 1;
        uintptr_t has_sidetable_rc  : 1;
        uintptr_t extra_rc          : 8;
# define RC_ONE (1ULL<<56)
# define RC_HALF (1ULL<<7)
    };

# else
# error unknown architecture for packed isa
# endif

// SUPPORT_PACKED_ISA
#endif


#if SUPPORT_INDEXED_ISA

# if __ARM_ARCH_7K__ >= 2

# define ISA_INDEX_IS_NPI 1
# define ISA_INDEX_MASK 0x0001FFFC
# define ISA_INDEX_SHIFT 2
# define ISA_INDEX_BITS 15
# define ISA_INDEX_COUNT (1 << ISA_INDEX_BITS)
# define ISA_INDEX_MAGIC_MASK 0x001E0001
# define ISA_INDEX_MAGIC_VALUE 0x001C0001
    struct {
        uintptr_t nonpointer        : 1;
        uintptr_t has_assoc         : 1;
        uintptr_t indexcls          : 15;
        uintptr_t magic             : 4;
        uintptr_t has_cxx_dtor      : 1;
        uintptr_t weakly_referenced : 1;
        uintptr_t deallocating      : 1;
        uintptr_t has_sidetable_rc  : 1;
        uintptr_t extra_rc          : 7;
# define RC_ONE (1ULL<<25)
# define RC_HALF (1ULL<<6)
    };

# else
# error unknown architecture for indexed isa
# endif

// SUPPORT_INDEXED_ISA
#endif

};
复制代码

上面的代码就是苹果对于isa优化的精华所在,为了看懂上面的代码,首先须要从一些基础知识开始说。

场景需求分析

首先定义一个类CLPerson,首先给CLPerson增长几个属性以及成员变量

@interface CLPerson : NSObject
{
    BOOL _tall;
    BOOL _rich;
    BOOL _handsome;
}
@property (nonatomic, assign, getter=isRich) BOOL rich;
@property (nonatomic, assign, getter=isTall) BOOL tall;
@property (nonatomic, assign, getter=isHandsome) BOOL handsome;
复制代码

对于它们的使用,无需多说,以下

#import <Foundation/Foundation.h>
#import "CLPerson.h"
#import <objc/runtime.h>

int main(int argc, const char * argv[]) {
    @autoreleasepool {
        CLPerson *person = [[CLPerson alloc] init];
        person.rich = YES;
        person.tall = NO;
        person.handsome = YES;


        NSLog(@"%zu", class_getInstanceSize([CLPerson class]));
    }
    return 0;
}
复制代码

经过runtime,咱们能够查看到CLPerson类对象的内存占用状况

2019-07-16 13:15:04.083828+0800 OC底层Runtime[2509:80387] 16
Program ended with exit code: 0
复制代码

经过我以前对与对象内存布局的分析的文章,这里能够得出以下结论:

  • isa占用了8个字节
  • _rich_tall_handsome这三个成员变量个占用1个字节
  • 由于有内存对齐和bucketSized的因素,因此类对象占用16个字节的内存空间。

🐞🐞🐞可是_rich_tall_handsome实际上只可能有2个值,YES/NO,也就是0和1,它们彻底能够用一个二进制位来表示,三个加在一块儿也就只须要占用3个二进制位,连半个字节都用不了。有什么方法能够实现这种节约内存的需求呢?🐞🐞🐞

若是直接用属性的话,确定就会自动生成带下划线的成员变量,这样就没法精简内存。因此须要手动实现getter/setter方法以替代属性。

#import <Foundation/Foundation.h>
@interface CLPerson : NSObject

- (void)setTall:(BOOL)tall;
- (void)setRich:(BOOL)rich;
- (void)setHandsome:(BOOL)handsome;

- (BOOL)isTall;
- (BOOL)isRich;
- (BOOL)isHandsome;
@end
复制代码

而后在.m文件里面,用一个char _tallRichHandsome;(一个字节)来存储tall/rich/handsome的信息。

#import "CLPerson.h"
@interface CLPerson()
{
    char _tallRichHandsome; // 0b 0000 0000
}

@end

@implementation CLPerson

- (void)setTall:(BOOL)tall {
    
}
- (void)setRich:(BOOL)rich {
    
}
- (void)setHandsome:(BOOL)handsome {
    
}

- (BOOL)isTall {
   
}
- (BOOL)isRich {
   
}
- (BOOL)isHandsome {
    
}

@end
复制代码

若是我想利用_tallRichHandsome的后三位来分别存放tallrichhandsome这三个信息,有什么方法能够办到呢?

取值

首先咱们来解决getter方法,也就是取值问题。如何从特定的位里面取出值呢?没错,——&(按位与运算)

假设咱们规定

  • tall_tallRichHandsome的右起第1位表示,
  • rich_tallRichHandsome的右起第2位表示,
  • handsome_tallRichHandsome的右起第3位表示,
  • 而且tall=YESrich=NOhandsome=YES

那么_tallRichHandsome的值应该是 0000 0101

tall (YES) rich (NO) handsome (YES)
_tallRichHandsome 0000 0101 0000 0101 0000 0101
mask码(用来取值) &0000 0001 &0000 0010 &0000 0100
经过&运算获得结果 0000 0001 0000 0000 0000 0100

根据&运算的特色,想要取出特定位上面的值,只需将mask码中对应位设置为1,由于 原来值 & 1 = 原来值,将mask码中其余位的设置为0,就能够屏蔽出特定位以外其他位上面的值,由于 原来值 & 0 = 0,这个应该很好理解。至于取出来的值如何转化成咱们所须要的值(在这里咱们须要的是YES/NO),就有不少办法了。好了,如今去代码里面实现一下。以下所示

*************************main.m*****************************
#import <Foundation/Foundation.h>
#import "CLPerson.h"

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

*************************CLPerson.m*****************************

#import "CLPerson.h"
@interface CLPerson()
{
    char _tallRichHandsome;
}

@end

@implementation CLPerson

- (instancetype)init
{
    self = [super init];
    if (self) {
        _tallRichHandsome = 0b00000101;//设定一个初值
    }
    return self;
}


/* mask码 tall的mask码:二进制 0b 00000001 ---> 十进制 1 rich的mask码:二进制 0b 00000010 ---> 十进制 2 handsome的mask码:二进制 0b 00000100 ---> 十进制 4 */

- (BOOL)isTall {
    return !!(_tallRichHandsome & 1);
}
- (BOOL)isRich {
    return !!(_tallRichHandsome & 2);
}
- (BOOL)isHandsome {
    return !!(_tallRichHandsome & 4);
}
@end

**************************运行结果**************************
2019-07-16 17:54:32.915636+0800 OC底层Runtime[2828:156639] 
tall = 1, 
rich = 0, 
handsome = 1
Program ended with exit code: 0
复制代码

上面的解决方案里面,我是经过!!(_tallRichHandsome & mask值);来转换成BOOL值的,由于_tallRichHandsome & mask值得出的结果,要么是0,要么是一个大于0的整数,所以经过两次!运算,能够获得对应的BOOL值,0对应NO,大于0的数对应YES

mask码的值能够用二进制表示,也能够用十进制表示,可是在具体的使用中,须要大量注释代码说明mask码所表明的含义,所以更好的处理方法,能够将它们定义为宏,经过宏的名字来表述所须要的含义。改写以下:

#define CLTallMask 1
#define CLRichMask 2
#define CLHandsomeMask 4

- (BOOL)isTall {
    return !!(_tallRichHandsome & CLTallMask);
}
- (BOOL)isRich {
    return !!(_tallRichHandsome & CLRichMask);
}
- (BOOL)isHandsome {
    return !!(_tallRichHandsome & CLHandsomeMask);
}
复制代码

可是还有一个问题,从宏定义里面,咱们不容易看出到底mask码是要取出哪一位的值,因此,改为二进制表示更好,以下

#define CLTallMask 0b00000001
#define CLRichMask 0b00000010
#define CLHandsomeMask 0b00000100
复制代码

可是仍然不完美,作开发的哪一个没有点强迫症,写这么一大串二进制,太麻烦了,因此咱们有更犀利的方法,没错,经过位移运算符来表示,以下

#define CLTallMask (1 << 0) 
#define CLRichMask (1 << 1)
#define CLHandsomeMask (1 << 2)
复制代码

1表明0b00000001,也就是二进制的1,1 << 0表示左移0位,也就是不移动,那么就表明去右边最低位上的值,同理,1 << 11<< 2就分别表示取右起第二位和第三位上的值,这样就清晰易懂了。

为何叫MASK

刚接触编程的时候,我曾经很困惑,用来获取特定位上的内容的这一串二进制码为何在英文里叫mask,这个mask为何要翻译成掩码?不知道你们有没有困惑过。后来想着想着,忽然开窍了了,这个mask是用来拿到特定位上的值,也就是查看你想要看到的部位。mask这个单词的含义里面有 面具 的意思,面具总知道吧,就下面这个面具上的几个洞,分别是眼睛和嘴,由于你去参加面具party的时候,只想让人看见眼镜和嘴巴,其余地方都遮掩起来。咱们在特定位上面的取值,不是跟这个同样吗,所以老外给这个东西取名叫mask码,其实就是为了形象生动,根本不是啥高大上的东西。只不过中文翻译我我的以为太生硬了,翻译成 面具码 岂不是更好。小感慨一下,英文技术文档里面有挺多这种翻译过来很奇怪的名词,其实就是文化差别,老外从他们的文化角度去给一些概念进行了生动形象的命名,但到了咱们这边的确是翻译的惨不忍睹,简直就是量产罗玉凤啊!!因此学好英文仍是很重要的,有些翻译真是害死人。



设值

接下来看一看如何把外部设定的值保存到对应的位上面去,并且不能影响到其余位上面的值。 (1)设值为1。正好按位或运算(|)就能实现这里的要求。来回顾一下或运算的规则

  • 0 | 0 = 0
  • 0 | 1 = 1
  • 1 | 0 = 1
  • 1 | 1 = 1

所以根据上面的特色,跟mask码进行或运算()就能够将特定值设置到目标位中。由于mask码中,对应目标位的就是1,对应非目标位的就是0。

(2)设值为0。上面还介绍了经过按位与运算(&)取值,结合这里的需求,能够发现,只须要将mask码按位取反以后,在与目标对象进行与运算(&),即可以将指定位设置为0。 对饮实现代码以下

#import "CLPerson.h"
#define CLTallMask (1 << 0)
#define CLRichMask (1 << 1)
#define CLHandsomeMask (1 << 2)


@interface CLPerson()
{
    char _tallRichHandsome;
}

@end

@implementation CLPerson

- (instancetype)init
{
    self = [super init];
    if (self) {
        _tallRichHandsome = 0b00000101;
    }
    return self;
}

//取值操做
- (BOOL)isTall {
    return !!(_tallRichHandsome & CLTallMask);
}
- (BOOL)isRich {
    return !!(_tallRichHandsome & CLRichMask);
}
- (BOOL)isHandsome {
    return !!(_tallRichHandsome & CLHandsomeMask);
}

//设定值操做
- (void)setTall:(BOOL)tall {
    if(tall) {
        _tallRichHandsome |= CLTallMask;
    } else {
        _tallRichHandsome &= ~CLTallMask;
    }
}
- (void)setRich:(BOOL)rich {
    if(rich) {
        _tallRichHandsome |= CLRichMask;
    } else {
        _tallRichHandsome &= ~CLRichMask;
    }
}
- (void)setHandsome:(BOOL)handsome {
    if(handsome) {
        _tallRichHandsome |= CLHandsomeMask;
    } else {
        _tallRichHandsome &= ~CLHandsomeMask;
    }
}

@end
复制代码

调用及打印结果

int main(int argc, const char * argv[]) {
    @autoreleasepool {
        CLPerson *person = [[CLPerson alloc] init];
        
        person.rich = YES;
        person.tall = YES;
        person.handsome = YES;
        
        NSLog(@"\ntall = %d, \nrich = %d, \nhandsome = %d", person.isTall, person.isRich, person.isHandsome);
        
    }
    return 0;
}
***********************************************
2019-08-02 11:36:49.081651+0800 OC底层Runtime[1147:65497] 
tall = 1, 
rich = 1, 
handsome = 1
Program ended with exit code: 0
复制代码

能够看到设定成功。经过以上的尝试,就将本来须要3个字节来表示的信息,存储到了一个字节里面,以达到节省空间的目的。

位域

上面的篇幅,咱们经过&两种位运算,实现节约内存的目标,请思考一下,这样是否完美了呢? 细细分析一下,会发现有以下不足:

  • 后期的维护时,假如咱们有须要新增一个新的属性,那么就须要 增长一个对应的mask码,增长对应的set方法, 增长对应的getter方法,仍是相对麻烦的,并且代码体积也会迅速增长。
  • 咱们经过char _tallRichHandsome;表达了三个信息——tallrichhandsome,若是须要表示10个信息,可想而知这里的命名会很是长,显然扩展性和可读性都很是差。

如今来看一下下面这段代码

@interface CLPerson()
{
    struct {
        char tall : 1;
        char rich : 1;
        char handsome : 1;
        
    }_tallRichHandsome;
    
// char _tallRichHandsome;
}

@end
复制代码

代码中,使用结构体struct取代以前的char _tallRichHandsome,结构体内有三个成员——tallrichhandsome。每一个成员后面的: 1表明这个成员占用1个位。成员前面的类型关键字不产生实际做用,只不过定义变量的语法规定须要有类型关键字,这里为了统一都写成char,成员实际占用内存的大小由后面的这个: X来表示,X就表示占用的位数。这个就是位域,关于这个概念的具体内容,能够自行查看C语言相关基础知识。由于struct做为一个总体单元,分配内存的最小单位是一个字节,那么tallrichhandsome这三个成员会按照前后定义的顺序,在这一个字节的8位空间里面,从右至左排布。 相应地,下面须要调整一下对应的getter/setter方法

******************************CLPerson.m*************************************
@implementation CLPerson

- (BOOL)isTall {
    return _tallRichHandsome.tall;
}
- (BOOL)isRich {
    return _tallRichHandsome.rich;
}
- (BOOL)isHandsome {
    return _tallRichHandsome.handsome;
}

- (void)setTall:(BOOL)tall {
    _tallRichHandsome.tall = tall;
}
- (void)setRich:(BOOL)rich {
    _tallRichHandsome.rich = rich;
}
- (void)setHandsome:(BOOL)handsome {
    _tallRichHandsome.handsome = handsome;
}

@end

******************************main.m*************************************
int main(int argc, const char * argv[]) {
    @autoreleasepool {
        CLPerson *person = [[CLPerson alloc] init];
        person.tall = NO;
        person.rich = YES;
        person.handsome = NO;
        
        NSLog(@"\ntall = %d, \nrich = %d, \nhandsome = %d", person.isTall, person.isRich, person.isHandsome);
                
    }
    return 0;
}
******************************打印输出*************************************
2019-08-02 14:53:31.980516+0800 OC底层Runtime[1333:126711] 
tall = 0, 
rich = -1, 
handsome = 0
Program ended with exit code: 0
复制代码

从上面的输出结果能够看出,貌似getter/setter像是生效了,可是好像rich有点问题,设置成YES,最后打印出来了是-1,应该是1才符合预期的,这个问题先无论后面来解决,咱们能够加一个断点,开看一下结构体_tallRichHandsome状况 也能够在lldb窗口经过命令获得

lldb) p/x person->_tallRichHandsome
((anonymous struct)) $0 = (tall = 0x00, rich = 0x01, handsome = 0x00)
(lldb) 
复制代码

结果很清晰的显示了,三个成员tallrichhandsome的值确实是被正确设置了。 此外,还能够经过直接查看_tallRichHandsome的内存中的状况,来讲明结果。 首先经过下面命令拿到_tallRichHandsome的内存地址

(lldb) p/x &(person->_tallRichHandsome)
((anonymous struct) *) $1 = 0x00000001033025c8
复制代码

而后经过命令查看该地址所对应内存的状况

(lldb) x 0x00000001033025c8
0x1033025c8: 02 00 00 00 00 00 00 00 41 f0 2f 96 ff ff 1d 00  ........A./.....
0x1033025d8: 80 12 00 00 01 00 00 00 06 00 05 00 05 00 00 00  ................
复制代码

这个结果怎么看呢,首先要知道,这种打印方式,是按照16进制来显示的,那么每2个数字就表明一个字节,上面咱们说了_tallRichHandsome实际占用1个字节大小,因此它对应的值应该是打印结果中的最开始的2个数字 02,而这个值转换成二进制是0000 0010,三个成员tallrichhandsome在其中对应的位上的值分别是010,这样就和咱们的设定吻合了,证实了咱们的getter/setter方法生效了。

回到上面咱们遗留的问题,为何被设置成YES的成员,内存里面验证了没有问题,为什么最终被打印出来的倒是-1呢?缘由在于,getter方法中返回值的时候,作了一次强制转换。如何理解呢 咱们经过下面的方法验证,将rich的getter方法调整以下,并在返回的地方加上断点

- (BOOL)isRich {
    BOOL ret = _tallRichHandsome.rich;
    return ret;
}
复制代码

经过lldb打印ret的内存结果以下

(lldb) p/x &ret (BOOL *) $0 = 0x00007ffeefbff42f 255
(lldb) x 0x00007ffeefbff42f
0x7ffeefbff42f: ff bc 9e a9 7b ff 7f 00 00 70 e4 80 01 01 00 00  ....{....p......
0x7ffeefbff43f: 00 80 f4 bf ef fe 7f 00 00 95 0c 00 00 01 00 00  ................
(lldb) 
复制代码

能够看到ret内存里面是ff,也就是二进制的11111111,确实如咱们上面所说,结果在强转是有这个问题,

实际上,在转换的时候,是根据对象值的最左边位上的值进行补值填充操做的,由于NO对应的是0,一位二进制的0转换成BOOL,其他位上都补0,因此不会影响最终结果。

至于这里为何一个字节上的11111111被输出的时候显示-1,有疑问的话请复习一下有符号数的表达方式,这里不作赘述。 对于当前的这个问题,解决办法也很多,咱们能够用以前进行两次!运算,就能够获得1了

- (BOOL)isRich {
        return !!_tallRichHandsome.rich;;
}
复制代码

或者,能够扩充一下成员信息所须要的位数

@interface CLPerson()
{
    struct {
        char tall : 2;
        char rich : 2;
        char handsome : 2;
        
    }_tallRichHandsome;
    
// char _tallRichHandsome;
}

@end
复制代码

这样,若是谁须要设置成YES,由于占用了2位,因此结果会是0b01,按照补位填充的规则,应该是0b0000 0001,不会影响最终值。

小结:使用上面的优化方案,咱们精减了getter/setter的代码实现,还省去了mask码。缺点是在取值的时候因为存在补位转换,致使最终取值不够精准(第一种方案经过位运算取值的方式不存在这个问题)。

d共用体

接下来,咱们来研究一下苹果采用的优化方案。苹果其实是基于上面第一种方案中的位运算方法,结合联合体/共用体(union)这个技术来实现的。

首先来回顾一下union这个概念,

union Person {
    char * name;//占用8个字节
    int age; // 占用 4个字节
    bool isMale ; //占用1个字节
}; 
复制代码

系统会为union Person分配8个字节空间,它的3个成员共用这一段8字节的空间。对比一下struct的定义

struct Person {
    char * name;//占用8个字节
    int age; // 占用 4个字节
    bool isMale ; //占用1个字节
}; 

复制代码

根据内存对其原则,系统为struct Person分配16字节内存,其3个成员会拥有各自独立使用的内存空间。 用一张图来总结以下

回到关于苹果优化的问题,首先看以下代码

@interface CLPerson()
{
    union {
        char bits;
        
        struct {
            char tall : 1;
            char rich : 1;
            char handsome : 1;
            
        };
    } _tallRichHandsome;
    
}

@end
复制代码

_tallRichHandsome定义成一个unionunion中的定义的成员是共享内存空间的,按照上面的写法,咱们在实际进行位运算实现getter/setter的时候,使用char bits;bits就是不少位的意思,具体要多少位,靠它前面的类型关键字来肯定,这里咱们须要8位就够,因此经过char来定义。由于下面的structchar bits;是共享内存的,实际使用中不会用到这个struct,可是能够借助它来解释bits里面各个位所表明的含义,体会一下。那么getter/setter修改以下

@implementation CLPerson
- (BOOL)isTall {
    return !!(_tallRichHandsome.bits & CLTallMask);
}
- (BOOL)isRich {
    return !!(_tallRichHandsome.bits & CLRichMask);
}
- (BOOL)isHandsome {
    return !!(_tallRichHandsome.bits & CLHandsomeMask);
}

- (void)setTall:(BOOL)tall {
    if(tall) {
        _tallRichHandsome.bits |= CLTallMask;
    } else {
        _tallRichHandsome.bits &= ~CLTallMask;
    }
}
- (void)setRich:(BOOL)rich {
    if(rich) {
        _tallRichHandsome.bits |= CLRichMask;
    } else {
        _tallRichHandsome.bits &= ~CLRichMask;
    }
}
- (void)setHandsome:(BOOL)handsome {
    if(handsome) {
        _tallRichHandsome.bits |= CLHandsomeMask;
    } else {
        _tallRichHandsome.bits &= ~CLHandsomeMask;
    }
}
@end

*************************main.m***************************
int main(int argc, const char * argv[]) {
    @autoreleasepool {

        CLPerson *person = [[CLPerson alloc] init];
        person.tall = NO;
        person.rich = YES;
        person.handsome = NO;

        NSLog(@"\ntall = %d, \nrich = %d, \nhandsome = %d", person.isTall, person.isRich, person.isHandsome);

    }
    return 0;
}
************************输出结果**************************
2019-08-02 17:20:07.157392+0800 OC底层Runtime[1673:197854] 
tall = 0, 
rich = 1, 
handsome = 0
Program ended with exit code: 0
复制代码

能够看到,成功实现了getter/setter的需求。实际上和咱们第一种方案里面的char _tallRichHandsome的使用是彻底相同的,只不过这里换成了_tallRichHandsome.bits,不一样的是这里咱们经过union中的struct来加强代码的可读性,其实用下面的写法,省略掉struct定义,获得的结果彻底相同

@interface CLPerson()
{
    union {
        char bits;
    } _tallRichHandsome;
    
}

@end
复制代码

须要注意的是,第一种方案里面,对于成员信息(tallrichhandsome)在内存里面的位置,咱们是经过结构体来定义的,而苹果的方案里面,则实际上依靠mask码来控制的,mask码的位移数就表明了成员信息的位置,而union里面的那个struct最重要的做用就是解释说明bits内部的成员信息,就是为了加强可读性,就是为了让人容易看懂。因此之后在阅读源码的时候再看到这种union,不再用惧怕了,就那么回事。


苹果isa优化总结

如今回到开篇的有关isa的源码

struct objc_object {
private:
    isa_t isa;

public:

复制代码
union isa_t 
{
    isa_t() { }
    isa_t(uintptr_t value) : bits(value) { }

    Class cls;
    uintptr_t bits;

# if __arm64__
#   define ISA_MASK        0x0000000ffffffff8ULL
#   define ISA_MAGIC_MASK  0x000003f000000001ULL
#   define ISA_MAGIC_VALUE 0x000001a000000001ULL
    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;

    };

#endif

};
复制代码

这里,精减掉了一些兼容性代码,只保留针对iOS部分的代码,根据本文研究的一些话题,咱们能够将苹果对isa的优化归纳为

经过位运算和位域以及联合体技术,更加充分的利用了isa的内存空间,将对象的真正的地址存放在了isa内存的其中33位上面,其他的31位被用来存放对象相关的其余信息。下面是isa其余位上的做用说明

  • nonpointer—— 0,表明普通指针,存储着class、meta-class对象的内存地址;1,表明优化过,使用位域存储更多信息
  • has_assoc—— 是否设置过关联对象,若是没有,施放时会速度更快
  • has_cxx_dtor—— 是否有C++的稀构函数,若是没有,施放时会更快
  • shiftcls—— 这个部分存储的是真正的Class、Meta-Class对象的内存地址信息,所以要经过 isa & ISA_MASK才能取出这里33位的值,获得对象的真正地址。
  • magic—— 用于在调试的时候分辨对象是否完成了初始化
  • weekly_referenced—— 是否被弱饮用指针指向过,若是没有,释放时会更快
  • extra_rc—— 里面存储的值是 引用计数 - 1
  • deallocating——对象是否正在被释放
  • has_sidtable_rc——引用计数器是否过大没法存储在isa中,若果是,这里就为1,引用计数就会被存储在一个叫SideTable的类的属性中。

为何上面的has_assochas_cxx_dtorweekly_referenced会影响对象释放的速度呢?objc源码里面有答案:对象在释放的时候,会调用void *objc_destructInstance(id obj) 方法

/*********************************************************************** * objc_destructInstance * Destroys an instance without freeing memory. * Calls C++ destructors. * Calls ARC ivar cleanup. * Removes associative references. * Returns `obj`. Does nothing if `obj` is nil. **********************************************************************/
void *objc_destructInstance(id obj) {
    if (obj) {
        // Read all of the flags at once for performance.
        bool cxx = obj->hasCxxDtor();
        bool assoc = obj->hasAssociatedObjects();

        // This order is important.
        if (cxx) object_cxxDestruct(obj);
        if (assoc) _object_remove_assocations(obj);
        obj->clearDeallocating();
    }

    return obj;
}
复制代码

从源码的注释以及实现逻辑,很容易看出,程序会

  • 根据obj->hasCxxDtor()来决定是否调用object_cxxDestruct(obj)进行C++析构,
  • 根据obj->hasAssociatedObjects()来决定是否调用_object_remove_assocations(obj)进行关联对象引用的移除。

obj->clearDeallocating();里面isa.weakly_referencedisa.has_sidetable_rc会决定是否进行 weak_clear_no_lock(&table.weak_table, (id)this);table.refcnts.erase(this);操做。 所以isa中上述的这几个值会影响到对象释放的速度。


ISA_MASK的细节

我在详解isa&superclass指针中有过以下总结 而本文开篇的iOS源码里面中有以下规定,在iOS下(也就是arm64),

# if __arm64__
# define ISA_MASK 0x0000000ffffffff8ULL
复制代码

上面的ISA_MASK 是经过16进制表示的,不太方便看,咱们经过科学计算器转换一下

这样能够清晰的看到,经过 isa & ISA_MASK 取出来的究竟是哪几位上面的值。同时还能够发现一个小细节,最终得出来的对象的地址值,会获得36个有效二进制位,而最后的四位,只多是 1000 或者 0000,也就是十六进制下的 80,所以对象的地址最后一位(十六进制下),必定是80。体会一下,而后经过代码走一波

#import "ViewController.h"
#import <objc/runtime.h>
#import "CLPerson.h"


@implementation ViewController

- (void)viewDidLoad {
    [super viewDidLoad];
    
    NSLog(@"ViewController类对象地址:%p", [ViewController class]);
    NSLog(@"ViewController元类对象地址:%p", object_getClass([ViewController class]));
    NSLog(@"CLPerson类对象地址:%p", [CLPerson class]);
    NSLog(@"CLPerson元类对象地址:%p", object_getClass([CLPerson class]));
    
}
@end

*************************************************************打印输出

2019-08-05 10:49:42.408303+0800 iOS-Runtime[1276:57991] ViewController类对象地址:0x103590dc8
2019-08-05 10:49:42.408405+0800 iOS-Runtime[1276:57991] ViewController元类对象地址:0x103590df0
2019-08-05 10:49:42.408481+0800 iOS-Runtime[1276:57991] CLPerson类对象地址:0x103590e90
2019-08-05 10:49:42.408565+0800 iOS-Runtime[1276:57991] CLPerson元类对象地址:0x103590e68
(lldb) 
复制代码

有关isa的探讨到这里就结束了。


🦋🦋🦋传送门🦋🦋🦋

Runtime原理探究(一)—— isa的深刻体会(苹果对isa的优化)

Runtime原理探究(二)—— Class结构的深刻分析

Runtime原理探究(三)—— OC Class的方法缓存cache_t

Runtime原理探究(四)—— 刨根问底消息机制

Runtime原理探究(五)—— super的本质

Runtime原理探究(六)—— 面试题中的Runtime

相关文章
相关标签/搜索