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


Runtime系列文章

Runtime原理探究(一)—— isa的深入体会(苹果对isa的优化)
Runtime原理探究(二)—— Class结构的深入分析
Runtime原理探究(三)—— OC Class的方法缓存cache_t
Runtime原理探究(四)—— 刨根问底消息机制
Runtime原理探究(五)—— super的本质
[Runtime原理探究(六)—— Runtime的应用...待续]-()
[Runtime原理探究(七)—— Runtime的API...待续]-()
Runtime原理探究(八)—— 面试题中的Runtime

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

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

很多静态编程语言,编写完代码后,经过编译连接生成可执行文件,最后就可以在电脑上运行起来。

以C语言为例

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码。缺点是在取值的时候由于存在补位转换,导致最终取值不够精准(第一种方案通过位运算取值的方式不存在这个问题)。

共用体

接下来,我们来研究一下苹果采用的优化方案。苹果实际上是基于上面第一种方案中的位运算方法,结合联合体/共用体(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系列文章

Runtime原理探究(一)—— isa的深入体会(苹果对isa的优化)
Runtime原理探究(二)—— Class结构的深入分析
Runtime原理探究(三)—— OC Class的方法缓存cache_t
Runtime原理探究(四)—— 刨根问底消息机制
Runtime原理探究(五)—— super的本质
[Runtime原理探究(六)—— Runtime的应用...待续]-()
[Runtime原理探究(七)—— Runtime的API...待续]-()
Runtime原理探究(八)—— 面试题中的Runtime

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 203,456评论 5 477
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,370评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,337评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,583评论 1 273
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,596评论 5 365
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,572评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,936评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,595评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,850评论 1 297
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,601评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,685评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,371评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,951评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,934评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,167评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 43,636评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,411评论 2 342