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];
这句代码,在运行阶段,可以调用CLPerson
的test
方法,也可以通过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
的后三位来分别存放tall
、rich
、handsome
这三个信息,有什么方法可以办到呢?
取值
首先我们来解决getter
方法,也就是取值问题。如何从特定的位里面取出值呢?没错,——&
(按位与运算)。
假设我们规定
-
tall用
_tallRichHandsome
的右起第1
位表示, -
rich用
_tallRichHandsome
的右起第2
位表示, -
handsome用
_tallRichHandsome
的右起第3
位表示, - 并且tall=
YES
,rich=NO
, handsome=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 << 1
,1<< 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;
表达了三个信息——tall
、rich
、handsome
,如果需要表示10个信息,可想而知这里的命名会非常长,显然扩展性和可读性都非常差。
现在来看一下下面这段代码
@interface CLPerson()
{
struct {
char tall : 1;
char rich : 1;
char handsome : 1;
}_tallRichHandsome;
// char _tallRichHandsome;
}
@end
代码中,使用结构体struct取代之前的char _tallRichHandsome
,结构体内有三个成员——tall
、rich
、handsome
。每个成员后面的: 1
代表这个成员占用1个位。成员前面的类型关键字不产生实际作用,只不过定义变量的语法规定需要有类型关键字,这里为了统一都写成char
,成员实际占用内存的大小由后面的这个: X
来表示,X就表示占用的位数。这个就是位域,关于这个概念的具体内容,可以自行查看C语言相关基础知识。因为struct
作为一个整体单元,分配内存的最小单位是一个字节,那么tall
、rich
、handsome
这三个成员会按照先后定义的顺序,在这一个字节的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)
结果很清晰的显示了,三个成员tall
、rich
、handsome
的值确实是被正确设置了。
此外,还可以通过直接查看_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
,三个成员tall
、rich
、handsome
在其中对应的位上的值分别是0
、1
、0
,这样就和我们的设定吻合了,证明了我们的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
定义成一个union
,union
中的定义的成员是共享内存空间的,按照上面的写法,我们在实际进行位运算实现getter/setter
的时候,使用char bits;
,bits
就是很多位的意思,具体要多少位,靠它前面的类型关键字来确定,这里我们需要8位就够,所以通过char
来定义。因为下面的struct
和char 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
需要注意的是,第一种方案里面,对于成员信息(
tall
、rich
、handsome
)在内存里面的位置,我们是通过结构体来定义的,而苹果的方案里面,则实际上依靠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
—— 里面存储的值是 引用计数 - 1deallocating
——对象是否正在被释放has_sidtable_rc
——引用计数器是否过大无法存储在isa中,若果是,这里就为1,引用计数就会被存储在一个叫SideTable的类的属性中。
为什么上面的has_assoc
、has_cxx_dtor
、weekly_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_referenced
和isa.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
,也就是十六进制下的 8
或0
,因此对象的地址最后一位(十六进制下),一定是8
或0
。体会一下,然后通过代码走一波
#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