前言
之前,咱们在探求动画及烘托相关原理的时分,咱们输出了几篇文章,解答了
iOS动画是怎么烘托,特效是怎么工作的疑惑
。咱们深感体系规划者在创作这些体系结构的时分,是如此脑洞大开,也深深意识到了解一门技能的底层原理关于从事该方面工作的重要性。
因而咱们决定
进一步探求iOS底层原理的使命
。在这篇文章中咱们围绕Runtime
打开,会逐个探求:isa详解
、class的结构
、办法缓存cache_t
一、Runtime简介
1. OC言语的实质回忆
咱们 之前在 探求 OC言语的实质时,了解到Apple官网对OC的介绍:
- Objective-C是程序员在为OS X和iOS编写软件时运用的首要编程言语(之一,现在现已还有Swift言语)
- 它
是C编程言语的超集
,提供面向目标
的功用和动态运转
时 - Objective-C
承继了C言语的语法
、根本类型
和流操控句子
,并增加了用于界说类和办法
的语法。(OC彻底兼容规范C言语) - 它还增加了
面向目标
办理和目标字面量
的言语级别支持,一起提供动态类型和绑定
,将许多责任推迟到运转时
2. Runtime
官网中介绍OC言语时,提及的动态运转
、动态类型和绑定
、将许多责任推迟到运转时
等许多运转时特性,便是讲经过Runtime这套底层API来完结的。
尽管,Objective-C
是一门闭源的言语,但官方也对该言语有了恰当的开源。咱们通常能够经过该地址去查找苹果官方开源的一些源码:opensource.apple.com/tarballs/
经过大局查找objc
,能够找到objc4
,然后下载最新的开源版本代码
咱们能够从官方开源的代码中也能够看到 官方开源的一些完结,其间就包含了runtime的一些完结
综上,咱们不难得出定论:
-
Objective-C
是一门动态性比较强的编程言语,跟C、C++
等言语有着很大的不同; -
Objective-C
的动态性是由Runtime API
来支撑的 -
Runtime API
提供的接口根本都是C言语
的,源码由C\C++\汇编言语
编写
二、isa详解
前面,咱们在探求 OC中的几种目标和目标的isa指针的时分得出一些定论,咱们简略回忆一下: Objective-C中的目标,简称OC目标,首要能够分为3种
-
instance
目标(实例目标) -
class
目标(类目标) -
meta-class
目标(元类目标)
1. instance
目标
instance目标
便是经过类alloc
出来的目标,每次调用alloc都会产生新的instance目标
- object1、object2是NSObject的instance目标(实例目标)
- 它们是不同的两个目标,别离占有着
两块不同的内存
- instance目标在内存中存储的信息包含
isa指针
- 其他成员变量
2. class
目标
- objectClass1 ~ objectClass5都是NSObject的
class目标
(类目标) - 它们是同一个目标。每个类在
内存中有且只要一个
class目标
- class目标在内存中存储的信息首要包含:
isa指针
superclass指针
- 类的
特点
信息(@property)、类的目标办法
信息(instance method) - 类的
协议
信息(protocol)、类的成员变量
信息(ivar) - ……
3. meta-class
目标
- objectMetaClass是NSObject的
meta-class目标
(元类目标) - 每个类在内存中
有且只要一个meta-class目标
- meta-class目标和class目标的内存结构是相同的,可是用途不相同,在内存中存储的信息首要包含
isa指针
superclass指针
- 类的
类办法
信息(class method) - ……
4. isa
指针
-
instance的
isa
指向class- 当调用
目标办法
时,经过instance的isa
找到class
,终究找到目标办法的完结进行调用
- 当调用
-
class的
isa
指向meta-class- 当调用
类办法
时,经过class的isa
找到meta-class
,终究找到类办法的完结进行调用
- 当调用
class目标的superclass指针
- 当Student的
instance
目标要调用Person的目标办法时,会先经过isa
找到Student的class
- 然后经过
superclass
找到Person的class
,终究找到目标办法的完结进行调用
meta-class目标的superclass指针
- 当Student的
class
要调用Person的类办法时,会先经过isa
找到Student的meta-class
- 然后经过
superclass
找到Person的meta-class
,终究找到类办法的完结进行调用
5. 对isa
、superclass
总结
isa
-
instance
的isa
指向class
-
class
的isa
指向meta-class
-
meta-class
的isa
指向基类的meta-class
- 基类的
class
的isa
指向基类的meta-class
- 基类的
meta-class
的isa
指向基类的meta-class
自身
superclass
-
class
的superclass
指向父类的class
- 假如没有父类,superclass指针为nil
-
meta-class
的superclass
指向父类的meta-class
- 基类的meta-class的superclass指向基类的class
办法调用
-
instance调用目标办法的轨道
- isa找到class,办法不存在,就经过superclass找父类
-
class调用类办法的轨道
- isa找meta-class,办法不存在,就经过superclass找父类
6. 综上
结合前面的定论,咱们不难得知,OC言语中的三类目标,是经过isa指针建立联系的,而OC的运转时特性所依赖的的RuntimeAPI正是在必定程度上基于isa指针建立的三类目标的联系,完结 动态运转时的。
因而,要想学习Runtime
,首要要了解它底层的一些常用数据结构
,比方isa指针
- 在arm64架构之前,
isa
便是一个一般的指针
,存储着Class
、Meta-Class
目标的内存地址 - 从arm64架构开端,对isa进行了优化,变成了
一个共用体(union)结构
,还运用位域来存储更多的信息
。需求经过ISA_MASK进行必定的位运算才干进一步获取具体的信息
7. isa
的实质
在arm64架构之后 OC目标的isa指针
并不是直接指向类目标
或许元类目标
,而是需求&ISA_MASK
经过位运算
才干获取到类目标
或许元类目标
的地址。
今天来探寻一下为什么需求&ISA_MASK
才干获取到类目标
或许元类目标
的地址,以及这样的优点。(苹果官方为什么做这个优化呢?咱们来一步一步探求一下!)
首要在源码中找到isa指针
,看一下isa指针
的实质。
// 截取objc_object内部分代码
struct objc_object {
private:
isa_t isa;
}
isa指针
其实是一个isa_t
类型的共用体,来到isa_t
内部检查其结构
// 精简过的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
#endif
上述源码中isa_t
是union
类型,union
表明共用体。
从源码中咱们能够看到:
- 共用体中有一个结构体
- 结构体内部别离界说了一些变量
- 变量后边的值代表的是该变量占用多少个字节,也便是位域技能
了解共用体
- 在进行某些算法的C言语编程的时分,需求使几种不同类型的变量的值寄存到同一段内存单元中;
- 这种几个不同的变量共同占用一段内存的结构,在C言语中,被称作“共用体”类型结构,简称共用体
接下来运用共用体的办法来深入的了解apple为什么要运用共用体,以及运用共用体的优点。
7.1 探寻进程
7.1.1 仿照底层对数据的存储
接下来运用代码来仿照底层的做法,创立一个person类并含有三个BOOL类型的成员变量。
@interface Person : NSObject
@property (nonatomic, assign, getter = isTall) BOOL tall;
@property (nonatomic, assign, getter = isRich) BOOL rich;
@property (nonatomic, assign, getter = isHansome) BOOL handsome;
@end
int main(int argc, const char * argv[]) {
@autoreleasepool {
NSLog(@"%zd", class_getInstanceSize([Person class]));
}
return 0;
}
// 打印内容
// Runtime - union探寻[52235:3160607] 16
上述代码中Person含有3个BOOL类型的特点,打印Person类目标占有内存空间为16
- 也便是
(isa指针 = 8) + (BOOL tall = 1) + (BOOL rich = 1) + (BOOL handsome = 1) = 13
- 由于
内存对齐
准则所以Person类目标占有内存空间为16(关于这内存对齐相关的常识,咱们在这篇文章介绍过)
经过共用体技能,能够使几个不同的变量寄存到同一段内存中去,能够很大程度上节省内存空间
尝试用一个字节存储三个BOOL类型的变量的值
- 那么咱们知道BOOL值只要两种情况 0 或许 1,可是却占有了一个字节的内存空间
- 而一个内存空间中有8个二进制位,而且二进制只要 0 或许 1
- 那么是否能够运用1个二进制位来表明一个BOOL值
- 也便是说3个BOOL值终究只运用3个二进制位,也便是一个内存空间即可呢?
- 怎么完结这种办法?
首要假如运用这种办法 需求自己写setter、getter办法的声明与完结
:
- 不能够写特点声明,由于一旦写特点,体系会主动帮咱们增加成员变量(会拓荒内存空间、也会完结setter、getter。我为了探求要躲避体系的主动生成)
别的想要将三个BOOL值寄存在一个字节中,咱们能够增加一个char
类型的成员变量
-
char
类型占有一个字节内存空间,也便是8个二进制位 - 能够运用其间终究三个二进制位来存储3个BOOL值。
@interface Person()
{
char _tallRichHandsome;
}
例如_tallRichHansome的值为 0b 0000 0010
,那么只运用8个二进制位中的终究3个,别离为其赋值0或许1来代表tall、rich、handsome
的值。如下图所示:
那么现在面临的问题便是怎么取出8个二进制位中的某一位的值,或许为某一位赋值呢?
a.) 取值
假设将三个BOOL变量的值 存在 一个字节里边,咱们首要讨论一下怎么从一个字节里边 取出 这三个变量的具体值。
能够运用1个二进制位来表明一个BOOL值,那么从低位开端,一个二进制位代表一个值。
- 假设char类型的成员变量中存储的二进制为
0b 0000 0010
- 假如想将倒数第2位的值也便是rich的值取出来,则需求进行 进制位的
位运算
- 咱们能够运用&进行
按位与
运算从而取出相应方位的值
了解【&:按位与】 同真为真,其他都为假
// 示例
// 取出倒数第三位 tall
0000 0010
& 0000 0100
------------
0000 0000 // 取出倒数第三位的值为0,其他位都置为0
// 取出倒数第二位 rich
0000 0010
& 0000 0010
------------
0000 0010 // 取出倒数第二位的值为1,其他位都置为0
定论:
按位与能够用来取出特定的二进制位的值
- 想取出哪一位就将那一方位为1,其他为都置为0
- 然后同原数据进行按位与核算,即可取出特定的位
用
按位与
运算来完结get办法
#define TallMask 0b00000100 // 4
#define RichMask 0b00000010 // 2
#define HandsomeMask 0b00000001 // 1
- (BOOL)tall
{
return !!(_tallRichHandsome & TallMask);
}
- (BOOL)rich
{
return !!(_tallRichHandsome & RichMask);
}
- (BOOL)handsome
{
return !!(_tallRichHandsome & HandsomeMask);
}
上述代码中运用两个!!(非)
来将值改为bool类型。相同运用上面的比如
// 取出倒数第二位 rich
0000 0010 // _tallRichHandsome
& 0000 0010 // RichMask
------------
0000 0010 // 取出rich的值为1,其他位都置为0
上述代码中(_tallRichHandsome & TallMask)
的值为0000 0010
也便是2,可是咱们需求的是一个BOOL类型的值 0 或许 1
- 那么
!!2
就将 2 先转化为 0 ,之后又转化为 1 - 相反假如按位与取得的值为 0 时,
!!0
将 0 先转化为 1 之后又转化为 0 - 因而运用
!!
两个非操作将值转化为 0 或许 1 来表明相应的值。
7.1.2 优化掩码,使其增加可读性
掩码: 一般用来 进行
按位与(&)
运算的值称之为掩码
- 上述代码中界说了三个宏,用来别离进行按位与运算而取出相应的值
- 三个宏的具体值都是掩码
- 为了能更明晰的表明掩码是为了取出哪一位的值,上述三个宏的界说能够运用
左移运算符:<<
来优化
左移运算符
A<<n
,表明在A数值的二进制数据中左移n位得到一个值
那么上述宏界说能够运用<<(左移)
优化成如下代码
#define TallMask (1<<2) // 0b00000100 4
#define RichMask (1<<1) // 0b00000010 2
#define HandsomeMask (1<<0) // 0b00000001 1
b.) 设值
咱们假如想给某一个二进制位赋值
0或许1,仍然能够运用 位运算
假如想设置某个值的某个二进制位的值为1,那么只要在该二进制位 与1进行 |(按位或
运算即可
按位或 运算:
| : 按位或,只要有一个1即为1,否则为0。
在当时谈论的事例中,也能够说:
假如想设置BOOL值为YES的话,那么将本来的值与掩码(该方位二进制位的值是1)进行按位或的操作即可。
例如咱们想将tall置为1
// 将倒数第三位 tall置为1
0000 0010 // _tallRichHandsome
| 0000 0100 // TallMask
------------
0000 0110 // 将tall置为1,其他位值都不变
按位与 运算:
&: 按位与,同真为真,其他都为假
在当时谈论的事例中,也能够说:
假如想设置BOOL值为NO的话,需求将掩码按位取反(~ : 按位取反符)(该方位二进制位的值即变成0),之后在与本来的值进行按位与操作即可。
// 将倒数第二位 rich置为0
0000 0010 // _tallRichHandsome
& 1111 1101 // RichMask按位取反
------------
0000 0000 // 将rich置为0,其他位值都不变
此刻set办法内部完结如下
- (void)setTall:(BOOL)tall
{
if (tall) { // 假如需求将值置为1 // 按位或掩码
_tallRichHandsome |= TallMask;
}else{ // 假如需求将值置为0 // 按位与(按位取反的掩码)
_tallRichHandsome &= ~TallMask;
}
}
- (void)setRich:(BOOL)rich
{
if (rich) {
_tallRichHandsome |= RichMask;
}else{
_tallRichHandsome &= ~RichMask;
}
}
- (void)setHandsome:(BOOL)handsome
{
if (handsome) {
_tallRichHandsome |= HandsomeMask;
}else{
_tallRichHandsome &= ~HandsomeMask;
}
}
写完set、get办法之后经过代码来检查一下是否能够设值、取值成功。
int main(int argc, const char * argv[]) {
@autoreleasepool {
Person *person = [[Person alloc] init];
person.tall = YES;
person.rich = NO;
person.handsome = YES;
NSLog(@"tall : %d, rich : %d, handsome : %d", person.tall,person.rich,person.handsome);
}
return 0;
}
打印内容
Runtime - union探寻[58212:3857728] tall : 1, rich : 0, handsome : 1
能够看出上述代码能够正常赋值和取值。可是代码还是有必定的局限性:
- 当需求增加新特点的时分,需求重复上述工作,而且代码可读性比较差
- 接下来运用结构体的位域特性来优化上述代码
7.1.3 用 位域 技能 完结 变量的 存取
将上述代码进行优化,运用结构体位域,能够使代码可读性更高。 位域声明 位域名 : 位域长度;
运用位域需求注意以下3点:
- 1.假如一个字节所剩空间不够寄存另一位域时,应从下一单元起寄存该位域。
- 也能够有意使某位域从下一单元开端
-
- 位域的长度不能大于数据类型自身的长度
- 比方int类型就不能超越32位二进位。
- 3.位域能够无位域名,这时它只用来作填充或调整方位
- 无名的位域是不能运用的
上述代码运用结构体位域优化之后。
@interface Person()
{
struct {
char handsome : 1; // 位域,代表占用一位空间
char rich : 1; // 按照顺序只占一位空间
char tall : 1;
}_tallRichHandsome;
}
set、get办法中能够直接经过结构体赋值和取值
- (void)setTall:(BOOL)tall
{
_tallRichHandsome.tall = tall;
}
- (void)setRich:(BOOL)rich
{
_tallRichHandsome.rich = rich;
}
- (void)setHandsome:(BOOL)handsome
{
_tallRichHandsome.handsome = handsome;
}
- (BOOL)tall
{
return _tallRichHandsome.tall;
}
- (BOOL)rich
{
return _tallRichHandsome.rich;
}
- (BOOL)handsome
{
return _tallRichHandsome.handsome;
}
经过代码验证一下是否能够赋值或取值正确
int main(int argc, const char * argv[]) {
@autoreleasepool {
Person *person = [[Person alloc] init];
person.tall = YES;
person.rich = NO;
person.handsome = YES;
NSLog(@"tall : %d, rich : %d, handsome : %d", person.tall,person.rich,person.handsome);
}
return 0;
}
首要在log处打个断点,检查_tallRichHandsome内存储的值
由于_tallRichHandsome
占有一个内存空间,也便是8个二进制位,咱们将05十六进制转化为二进制检查
上图中能够发现,倒数第三位也便是tall值为1,倒数第二位也便是rich值为0,倒数一位也便是handsome值为1,如此看来和上述代码中咱们设置的值相同。能够成功赋值。
接着持续打印内容: Runtime - union探寻[59366:4053478] tall : -1, rich : 0, handsome : -1
此刻能够发现问题,tall与handsome咱们设值为YES,讲道理应该输出的值为1为何上面输出为-1呢?
而且上面经过打印_tallRichHandsome
中存储的值,也承认tall
和handsome
的值都为1。咱们再次打印_tallRichHandsome
结构体内变量的值。
上图中能够发现,handsome的值为0x01,经过核算器将其转化为二进制
能够看到值的确为1的,为什么打印出来值为-1呢?此刻应该能够想到应该是get办法内部有问题。咱们来到get办法内部经过打印断点检查获取到的值。
- (BOOL)handsome
{
BOOL ret = _tallRichHandsome.handsome;
return ret;
}
打印ret的值
经过打印ret的值发现其值为255,也便是1111 1111
,此刻也就能解释为什么打印出来值为 -1了,首要此刻经过结构体获取到的handsome
的值为0b1
只占一个内存空间中的1位,可是BOOL值占有一个内存空间,也便是8位。当仅有1位的值扩展成8位的话,其他空位就会依据前面一位的值悉数补位成1,因而此刻ret的值就被映射成了0b 11111 1111
。
11111111
在一个字节时,有符号数则为-1,无符号数则为255。因而咱们在打印时分打印出的值为-1
为了验证当1位的值扩展成8位时,会悉数补位,咱们将tall、rich、handsome值设置为占有两位。
@interface Person()
{
struct {
char tall : 2;
char rich : 2;
char handsome : 2;
}_tallRichHandsome;
}
此刻在打印就发现值能够正常打印出来。 Runtime - union探寻[60827:4259630] tall : 1, rich : 0, handsome : 1
这是由于,在get办法内部获取到的_tallRichHandsome.handsome
为两位的也便是0b 01
,此刻在赋值给8位的BOOL类型的值时,前面的空值就会主动依据前面一位补全为0,因而回来的值为0b 0000 0001
,因而打印出的值也就为1了。
因而上述问题相同能够运用!!
双感叹号来处理问题。!!
的原理上面现已解说过,这儿不再赘述了。
运用结构体位域优化之后的代码
@interface Person()
{
struct {
char tall : 1;
char rich : 1;
char handsome : 1;
}_tallRichHandsome;
}
@end
@implementation Person
- (void)setTall:(BOOL)tall
{
_tallRichHandsome.tall = tall;
}
- (void)setRich:(BOOL)rich
{
_tallRichHandsome.rich = rich;
}
- (void)setHandsome:(BOOL)handsome
{
_tallRichHandsome.handsome = handsome;
}
- (BOOL)tall
{
return !!_tallRichHandsome.tall;
}
- (BOOL)rich
{
return !!_tallRichHandsome.rich;
}
- (BOOL)handsome
{
return !!_tallRichHandsome.handsome;
}
上述代码中运用结构体的位域则不在需求运用掩码,使代码可读性增强了许多,可是功率相比直接运用位运算的办法来说差许多,假如想要高功率的进行数据的读取与存储一起又有较强的可读性就需求运用到共用体了。
7.1.4 用 共用体 和 来存储 变量的值
为了使代码存储数据高功率的一起,有较强的可读性,能够运用共用体来增强代码可读性,一起运用位运算来进步数据存取的功率。
运用共用体优化的代码
#define TallMask (1<<2) // 0b00000100 4
#define RichMask (1<<1) // 0b00000010 2
#define HandsomeMask (1<<0) // 0b00000001 1
@interface Person()
{
union {
char bits;
// 结构体只是是为了增强代码可读性,无实质用途
struct {
char tall : 1;
char rich : 1;
char handsome : 1;
};
}_tallRichHandsome;
}
@end
@implementation Person
- (void)setTall:(BOOL)tall
{
if (tall) {
_tallRichHandsome.bits |= TallMask;
}else{
_tallRichHandsome.bits &= ~TallMask;
}
}
- (void)setRich:(BOOL)rich
{
if (rich) {
_tallRichHandsome.bits |= RichMask;
}else{
_tallRichHandsome.bits &= ~RichMask;
}
}
- (void)setHandsome:(BOOL)handsome
{
if (handsome) {
_tallRichHandsome.bits |= HandsomeMask;
}else{
_tallRichHandsome.bits &= ~HandsomeMask;
}
}
- (BOOL)tall
{
return !!(_tallRichHandsome.bits & TallMask);
}
- (BOOL)rich
{
return !!(_tallRichHandsome.bits & RichMask);
}
- (BOOL)handsome
{
return !!(_tallRichHandsome.bits & HandsomeMask);
}
上述代码中运用位运算这种比较高效的办法存取值,运用union共用体来对数据进行存储。增加读取功率的一起增强代码可读性。
其间_tallRichHandsome
共用体只占用一个字节,由于结构体中tall、rich、handsome都只占一位二进制空间,所以结构体只占一个字节,而char类型的bits也只占一个字节,他们都在共用体中,因而共用一个字节的内存即可。
而且在get、set
办法中并没有运用到结构体,结构体只是为了增加代码可读性,指明共用体中存储了哪些值,以及这些值各占多少位空间。一起存值取值还运用位运算来增加功率,存储运用共用体,寄存的方位仍然经过与掩码进行位运算来操控。
此刻代码现已算是优化完结了,高效的一起可读性高,那么此刻在回头看isa_t
共用体的源码
7.2 isa_t源码
此刻咱们在回头检查isa_t源码
// 精简过的isa_t共用体
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;
# define RC_ONE (1ULL<<45)
# define RC_HALF (1ULL<<18)
};
#endif
};
经过前面 对位运算
、位域
以及共用体
的介绍,现在再来看源码现已能够很明晰的理解其间的内容:
- 源码中经过共用体的办法存储了64位的值,这些值在结构体中被展现出来,经过对
bits
进行位运算而取出相应方位的值 -
shiftcls
-
shiftcls
中存储着Class、Meta-Class
目标的内存地址信息 - 咱们之前在OC目标的实质中提到过,目标的isa指针需求同
ISA_MASK
经过一次&(按位与)运算才干得出真正的Class目标地址
-
那么此刻咱们从头来看ISA_MASK
的值0x0000000ffffffff8ULL
,咱们将其转化为二进制数
- 上图中能够看出
ISA_MASK
的值转化为二进制中有33位都为1,前面提到过按位与的效果是能够取出这33位中的值 - 那么此刻很明显了,同
ISA_MASK
进行按位与运算即能够取出Class
或Meta-Class
的值。
一起能够看出ISA_MASK
终究三位的值为0,那么任何数同ISA_MASK
按位与运算之后,得到的终究三位必定都为0,因而任何类目标或元类目标的内存地址终究三位必定为0,转化为十六进制末位必定为8或许0。
7.3 isa
中存储的信息及效果
将结构体取出来符号一下这些信息的效果。
struct {
// 0代表一般的指针,存储着Class,Meta-Class目标的内存地址。
// 1代表优化后的运用位域存储更多的信息。
uintptr_t nonpointer : 1;
// 是否有设置过相关目标,假如没有,开释时会更快
uintptr_t has_assoc : 1;
// 是否有C++析构函数,假如没有,开释时会更快
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;
// 里边存储的值是引证计数器减1
uintptr_t extra_rc : 19;
// 引证计数器是否过大无法存储在isa中
// 假如为1,那么引证计数会存储在一个叫SideTable的类的特点中
uintptr_t has_sidetable_rc : 1;
};
7.3.1 验证 isa
中存储的信息是否可靠
经过下面一段代码验证上述信息存储的方位及效果
// 以下代码需求在真机中运转,由于真机中才是__arm64__ 位架构
- (void)viewDidLoad {
[super viewDidLoad];
Person *person = [[Person alloc] init];
NSLog(@"%p",[person class]);
NSLog(@"%@",person);
}
首要打印person
类目标的地址,之后经过断点打印一下person
目标的isa指针
地址。
首要来看一下打印的内容
将类目标地址转化为二进制
将person的isa指针地址转化为二进制
shiftcls : shiftcls
中存储类目标地址,经过上面两张图对比能够发现存储类目标地址的33位二进制内容彻底相同。
extra_rc : extra_rc
的19位中存储着的值为引证计数减一,由于此刻person的引证计数为1,因而此刻extra_rc
的19位二进制中存储的是0。
magic : magic
的6位用于在调试时分辩目标是否未完结初始化,上述代码中person现已完结初始化,那么此刻这6位二进制中存储的值011010
即为共用体中界说的宏# define ISA_MAGIC_VALUE 0x000001a000000001ULL
的值。
nonpointer : 这儿肯定是运用的优化后的isa,因而nonpointer
的值肯定为1
由于此刻person目标没有相关目标而且没有弱指针引证过,能够看出has_assoc
和weakly_referenced
值都为0,接着咱们为person目标增加弱引证和相关目标,来调查一下has_assoc
和weakly_referenced
的改变。
- (void)viewDidLoad {
[super viewDidLoad];
Person *person = [[Person alloc] init];
NSLog(@"%p",[person class]);
// 为person增加弱引证
__weak Person *weakPerson = person;
// 为person增加相关目标
objc_setAssociatedObject(person, @"name", @"xx_cc", OBJC_ASSOCIATION_RETAIN_NONATOMIC);
NSLog(@"%@",person);
}
从头打印person的isa指针地址将其转化为二进制能够看到has_assoc
和weakly_referenced
的值都变成了1
注意:只要设置过相关目标或许弱引证引证过目标has_assoc
和weakly_referenced
的值就会变成1,不管之后是否将相关目标置为nil或断开弱引证。
假如没有设置过相关目标,目标开释时会更快,这是由于目标在毁掉时会判别是否有相关目标从而对相关目标开释。来看一下目标毁掉的源码
void *objc_destructInstance(id obj)
{
if (obj) {
Class isa = obj->getIsa();
// 是否有c++析构函数
if (isa->hasCxxDtor()) {
object_cxxDestruct(obj);
}
// 是否有相关目标,假如有则移除
if (isa->instancesHaveAssociatedObjects()) {
_object_remove_assocations(obj);
}
objc_clear_deallocating(obj);
}
return obj;
}
相信至此咱们现已对isa指针
有了新的知道:
-
arm64
架构之后,isa指针
不单单只存储了Class
或Meta-Class
的地址,而是运用共用体的办法存储了更多信息 - 其间
shiftcls
存储了Class
或Meta-Class
的地址,需求同ISA_MASK
进行按位&运算才干够取出其内存地址值。
三、class的结构
1. 回忆一下Class的内部结构
咱们在之前在探求OC的三类目标的时分,从简略探求过Class的内部结构,且对Class结构的知道终究以一张图作总结:
咱们在前面的篇幅中对isa指针
有了新的知道之后,也需求基于此 对Class有 进一步的探求,从头知道Class内部结构:
首要回忆一下Class的内部结构相关的源码:
struct objc_class : objc_object {
// Class ISA;
Class superclass;
cache_t cache; // formerly cache pointer and vtable
class_data_bits_t bits; // class_rw_t * plus custom rr/alloc flags
class_rw_t *data() {
return bits.data();
}
void setData(class_rw_t *newData) {
bits.setData(newData);
}
}
class_rw_t* data() {
return (class_rw_t *)(bits & FAST_DATA_MASK);
}
1.1 class_rw_t
从源码中咱们不难得知:
-
bits & FAST_DATA_MASK
位运算之后,能够得到class_rw_t
- 而
class_rw_t
中存储着办法
列表、特点
列表以及协议
列表等 - 来看一下
class_rw_t
部分代码:struct class_rw_t { // Be warned that Symbolication knows the layout of this structure. uint32_t flags; uint32_t version; const class_ro_t *ro; method_array_t methods; // 办法列表 property_array_t properties; // 特点列表 protocol_array_t protocols; // 协议列表 Class firstSubclass; Class nextSiblingClass; char *demangledName; };
- 从
class_rw_t
结构体内部的成员:method_array_t
、property_array_t
、protocol_array_t
其实都是二维数组 - 咱们能够去看下
method_array_t
、property_array_t
、protocol_array_t
的内部结构:class method_array_t : public list_array_tt<method_t, method_list_t> { typedef list_array_tt<method_t, method_list_t> Super; public: method_list_t **beginCategoryMethodLists() { return beginLists(); } method_list_t **endCategoryMethodLists(Class cls); method_array_t duplicate() { return Super::duplicate<method_array_t>(); } }; class property_array_t : public list_array_tt<property_t, property_list_t> { typedef list_array_tt<property_t, property_list_t> Super; public: property_array_t duplicate() { return Super::duplicate<property_array_t>(); } }; class protocol_array_t : public list_array_tt<protocol_ref_t, protocol_list_t> { typedef list_array_tt<protocol_ref_t, protocol_list_t> Super; public: protocol_array_t duplicate() { return Super::duplicate<protocol_array_t>(); } };
- 咱们这儿以
method_array_t
为例,剖析一下其二维数组的构成:-
method_array_t
自身便是一个数组,数组里边寄存的是数组method_list_t
-
method_list_t
里边终究寄存的是method_t
-
method_t
是一个办法目标
-
- 从
class_rw_t
里边的methods、properties、protocols
是二维数组,是可读可写的,其间包含了类的初始内容
以及分类的内容
。 (这儿以methods为例,实际上properties
和protocols
都是相似的构成)
1.2 class_ro_t
咱们之前提到过class_ro_t
中也有存储办法
、特点
、协议
列表,别的还有成员变量
列表。
接着来看一下class_ro_t
部分代码
struct class_ro_t {
uint32_t flags;
uint32_t instanceStart;
uint32_t instanceSize;
#ifdef __LP64__
uint32_t reserved;
#endif
const uint8_t * ivarLayout;
const char * name;//类名
method_list_t * baseMethodList;//办法列表
protocol_list_t * baseProtocols;//协议列表
const ivar_list_t * ivars;//成员变量
const uint8_t * weakIvarLayout;
property_list_t *baseProperties;//特点列表
method_list_t *baseMethods() const {
return baseMethodList;
}
};
从class_rw_t
的源码中咱们能够看到class_ro_t *ro
成员,可是其是被const润饰的,也便是是只读,不可修正的。咱们进一步去看一下class_ro_t
的内部结构咱们能够得知:
- 内部直接存储的直接便是
method_list_t、protocol_list_t 、property_list_t
类型的一维数组
- 数组里边别离寄存的是
类的初始信息
- 以
method_list_t
为例,method_list_t
中直接寄存的便是method_t
,可是是只读的,不允许增修改查。
1.3 总结
以办法列表为例,class_rw_t
中的methods
是二维数组的结构,而且可读可写
- 因而能够
动态的增加办法
,而且更加便于分类办法的增加 - 由于咱们在Category的实质里边提到过,
attachList
函数内经过memmove 和 memcpy
两个操作将分类的办法列表
兼并在本类的办法列表中
(也即是class_rw_t
的methods
中) - 那么此刻就将分类的办法和本类的办法统一整合到一起了
其实一开端类的办法,特点,成员变量特点协议等等都是寄存在class_ro_t
中的
- 当程序运转的时分,需求将分类中的列表跟类初始的列表兼并在一起的时,就会将
class_ro_t
中的列表和分类中的列表兼并起来寄存在class_rw_t
中 - 也便是说
class_rw_t
中有部分列表是从class_ro_t
里边拿出来的。而且终究和分类的办法兼并 - 能够经过源码看到这一部分的完结:
static Class realizeClass(Class cls) { runtimeLock.assertWriting(); const class_ro_t *ro; class_rw_t *rw; Class supercls; Class metacls; bool isMeta; if (!cls) return nil; if (cls->isRealized()) return cls; assert(cls == remapClass(cls)); // 最开端cls->data是指向ro的 ro = (const class_ro_t *)cls->data(); if (ro->flags & RO_FUTURE) { // rw现已初始化而且分配内存空间 rw = cls->data(); // cls->data指向rw ro = cls->data()->ro; // cls->data()->ro指向ro cls->changeInfo(RW_REALIZED|RW_REALIZING, RW_FUTURE); } else { // 假如rw并不存在,则为rw分配空间 rw = (class_rw_t *)calloc(sizeof(class_rw_t), 1); // 分配空间 rw->ro = ro; // rw->ro从头指向ro rw->flags = RW_REALIZED|RW_REALIZING; // 将rw传入setData函数,等于cls->data()从头指向rw cls->setData(rw); } }
源码解读: 那么从上述源码中能够发现:
- 类的初始信息本来其实是存储在
class_ro_t
中的- 而且
ro
本来是指向cls->data()
的 - 也便是说
bits.data()
得到的是ro
- 而且
- 可是在运转进程中创立了
class_rw_t
,并将cls->data
指向rw
- 一起将初始信息
ro
赋值给rw
中的ro
- 终究在经过setData(rw)设置data
- 一起将初始信息
- 那么此刻
bits.data()
得到的便是rw
- 之后再去检查是否有
分类
,一起将分类的办法
、特点
、协议
列表整合存储在class_rw_t
的办法
,特点
及协议
列表中
经过上述对源码的剖析,咱们对class_rw_t
内存储办法
、特点
、协议
列表的进程有了更明晰的知道,那么接下来探寻class_rw_t
中是怎么存储办法的。
2. class_rw_t中是怎么存储办法的?
2.1 method_t
咱们知道 method_array_t
中终究存储的是method_t
-
method_t
是对办法、函数的封装,每一个办法目标便是一个method_t
- 经过源码看一下
method_t
的结构体:
struct method_t {
SEL name; // 函数名
const char *types; // 编码(回来值类型,参数类型)
IMP imp; // 指向函数的指针(函数地址)
};
method_t
结构体中能够看到三个成员,咱们依次来看看这三个成员变量别离代表什么:
2.1.1 SEL
SEL
代表办法\函数名,一般叫做选择器,底层结构跟char *
相似
-
SEL
能够经过@selector()
和sel_registerName()
取得SEL sel1 = @selector(test); SEL sel2 = sel_registerName("test");
- 也能够经过
sel_getName()
和NSStringFromSelector()
将SEL
转成字符串char *string = sel_getName(sel1); NSString *string2 = NSStringFromSelector(sel2);
- 不同类中相同姓名的办法,所对应的办法选择器是相同的
-
SEL
只是代表办法的姓名,而且不同类中相同的办法名的SEL
是大局唯一的。
NSLog(@"%p,%p", sel1,sel2); Runtime-test[23738:8888825] 0x1017718a3,0x1017718a3
-
typedef struct objc_selector *SEL;
,能够把SEL
看做是办法姓名符串。
2.1.2 types
types
包含了函数回来值,参数编码的字符串
- 经过字符串拼接的办法将回来值和参数拼接成一个字符串
- 这个字符串能够用于 代表函数
回来值
及参数
咱们经过代码检查一下types
是怎么代表函数回来值及参数的:
- 首要经过在本地写几个与runtime底层完结class相同的结构体,用于模仿Class的内部完结
- 咱们曾在探寻Class的实质时,做过该操作:经过类型强制转化来探寻内部数据
Person *person = [[Person alloc] init];
xx_objc_class *cls = (__bridge xx_objc_class *)[Person class];
class_rw_t *data = cls->data();
经过断点能够在data中找到types的值
- 上图中能够看出
types
的值为v16@0:8
- 那么这个值代表什么呢?
- apple为了能够明晰的运用字符串表明办法及其回来值,制定了一系列对应规矩,经过下表能够看到逐个对应联系
将types的值同表中的逐个对照检查types
的值v16@0:8
代表什么
- (void) test;
v 16 @ 0 : 8
void id SEL
// 16表明参数的占用空间巨细,id后边跟的0表明从0位开端存储,id占8位空间。
// SEL后边的8表明从第8位开端存储,SEL相同占8位空间
咱们知道任何办法都默认有两个参数的,id
类型的self
,和SEL
类型的_cmd
,而上述经过对types
的剖析一起也验证了这个说法。
为了能够看的更加明晰,咱们为test增加回来值及参数之后从头检查types的值。
相同经过上表找出逐个对应的值,检查types的值代表的办法
- (int)testWithAge:(int)age Height:(float)height
{
return 0;
}
i 24 @ 0 : 8 i 16 f 20
int id SEL int float
// 参数的总占用空间为 8 + 8 + 4 + 4 = 24
// id 从第0位开端占有8位空间
// SEL 从第8位开端占有8位空间
// int 从第16位开端占有4位空间
// float 从第20位开端占有4位空间
iOS提供了@encode
的指令,能够将具体的类型转化成字符串编码。
NSLog(@"%s",@encode(int));
NSLog(@"%s",@encode(float));
NSLog(@"%s",@encode(id));
NSLog(@"%s",@encode(SEL));
// 打印内容
Runtime-test[25275:9144176] i
Runtime-test[25275:9144176] f
Runtime-test[25275:9144176] @
Runtime-test[25275:9144176] :
上述代码中能够看到,对应联系的确如上表所示。
2.1.3 IMP
IMP
代表函数的具体完结
- 存储的内容是函数地址
- 也便是说当找到
IMP
的时分就能够找到函数完结,从而对函数进行调用
在上述代码中打印IMP
的值
Printing description of data->methods->first.imp:
(IMP) imp = 0x000000010c66a4a0 (Runtime-test`-[Person testWithAge:Height:] at Person.m:13)
之后在test
办法内部打印断点,并来到其办法内部能够看出IMP
中的存储的地址也便是办法完结的地址。
四、cache_t办法缓存
经过前面的探求咱们知道了办法列表是怎么存储在Class类目标
中的
- 可是当屡次承继的子类想要调用基类办法时,就需求经过
superclass
指针一层一层找到基类,在从基类办法列表中找到对应的办法进行调用 - 假如
屡次调用
基类办法,那么就需求屡次遍历
每一层父类的办法列表,这对功能来说无疑是伤害巨大的
Apple经过办法缓存技能
的办法处理了这一问题,接下来咱们来探寻Class类目标
是怎么进行办法缓存的
回到类目标结构体objc_class
。里边有一个成员变量cache
- 这个
cache
成员变量便是用于完结办法缓存技能
的支撑struct objc_class : objc_object { // Class ISA; Class superclass; cache_t cache; // 办法缓存 // formerly cache pointer and vtable class_data_bits_t bits; // class_rw_t * plus custom rr/alloc flags class_rw_t *data() { return bits.data(); } void setData(class_rw_t *newData) { bits.setData(newData); } }
Class
内部结构中有个办法缓存(cache_t
),用散列表
(哈希表
)来缓存曾经调用过的办法,能够进步办法的查找速度
回忆办法调用进程:
- 调用办法的时分,需求去办法列表里边进行遍历查找
- 假如办法不在列表里边,就会经过
superclass
找到父类的类目标,在去父类类目标办法列表里边遍历查找。
- 假如办法不在列表里边,就会经过
- 假如办法需求调用许屡次的话,那就相当于
每次调用都需求去遍历
屡次办法列表
cache_t
技能
- 为了能够快速查找办法,
apple
规划了cache_t
来进行办法缓存: - 每逢调用办法的时分,会先去
cache
中查找是否有缓存的办法:- 假如没有缓存,在去类目标办法列表中查找,以此类推直到找到办法之后,就会将办法直接存储在
cache
中 - 下一次在调用这个办法的时分,就会在类目标的
cache
里边找到这个办法,直接调用了
- 假如没有缓存,在去类目标办法列表中查找,以此类推直到找到办法之后,就会将办法直接存储在
1. cache_t 怎么进行缓存
那么cache_t
是怎么对办法进行缓存的呢?首要来看一下cache_t
的内部结构。
struct cache_t {
struct bucket_t *_buckets; // 散列表 数组
mask_t _mask; // 散列表的长度 -1
mask_t _occupied; // 现已缓存的办法数量
};
bucket_t
是以数组的办法存储办法列表的,看一下bucket_t
内部结构
struct bucket_t {
private:
cache_key_t _key; // SEL作为Key
IMP _imp; // 函数的内存地址
};
从源码中能够看出:
-
bucket_t
中存储着SEL
和_imp
- 经过
key->value
的办法:- 以
SEL
为key
-
函数完结的内存地址 _imp
为value
来存储办法
- 以
经过一张图来展现一下cache_t
的结构
办法散列表
bucket_t
- 上述
bucket_t
列表咱们称之为散列表(哈希表) - 散列表(Hash table,也叫哈希表),是依据要害码值(Key value)而直接进行访问的数据结构
- 也便是说,它经过把要害码值映射到表中一个方位来访问记载,以加快查找的速度。
- 这个映射函数叫做
散列函数
,寄存记载的数组叫做散列表
那么apple怎么在散列表中快速而且准确的找到对应的key以及函数完结呢?
这就需求咱们经过源码来看一下apple的散列函数是怎么规划的:
2.散列函数及散列表原理
首要来看一下办法缓存的源码(首要检查几个函数,要害代码都有注释,便不再打开介绍)
2.1 cache_fill 及 cache_fill_nolock 函数
void cache_fill(Class cls, SEL sel, IMP imp, id receiver)
{
#if !DEBUG_TASK_THREADS
mutex_locker_t lock(cacheUpdateLock);
cache_fill_nolock(cls, sel, imp, receiver);
#else
_collecting_in_critical();
return;
#endif
}
static void cache_fill_nolock(Class cls, SEL sel, IMP imp, id receiver)
{
cacheUpdateLock.assertLocked();
// 假如没有initialize直接return
if (!cls->isInitialized()) return;
// 确保线程安全,没有其他线程增加缓存
if (cache_getImp(cls, sel)) return;
// 经过类目标获取到cache
cache_t *cache = getCache(cls);
// 将SEL包装成Key
cache_key_t key = getKey(sel);
// 占用空间+1
mask_t newOccupied = cache->occupied() + 1;
// 获取缓存列表的缓存才能,能存储多少个键值对
mask_t capacity = cache->capacity();
if (cache->isConstantEmptyCache()) {
// 假如为空的,则创立空间,这儿创立的空间为4个。
cache->reallocate(capacity, capacity ?: INIT_CACHE_SIZE);
}
else if (newOccupied <= capacity / 4 * 3) {
// 假如所占用的空间占总数的3/4一下,则持续运用现在的空间
}
else {
// 假如占用空间超越3/4则扩展空间
cache->expand();
}
// 经过key查找适宜的存储空间。
bucket_t *bucket = cache->find(key, receiver);
// 假如key==0则阐明之前未存储过这个key,占用空间+1
if (bucket->key() == 0) cache->incrementOccupied();
// 存储key,imp
bucket->set(key, imp);
}
2.2 expand ()函数
当散列表的空间被占用超越3/4的时分,散列表会调用expand ()
函数进行扩展,咱们来看一下expand ()
函数内散列表怎么进行扩展的。
void cache_t::expand()
{
cacheUpdateLock.assertLocked();
// 获取旧的散列表的存储空间
uint32_t oldCapacity = capacity();
// 将旧的散列表存储空间扩容至两倍
uint32_t newCapacity = oldCapacity ? oldCapacity*2 : INIT_CACHE_SIZE;
// 为新的存储空间赋值
if ((uint32_t)(mask_t)newCapacity != newCapacity) {
newCapacity = oldCapacity;
}
// 调用reallocate函数,从头创立存储空间
reallocate(oldCapacity, newCapacity);
}
2.3 reallocate 函数
经过上述源码看到reallocate
函数担任分配散列表空间,来到reallocate
函数内部。
void cache_t::reallocate(mask_t oldCapacity, mask_t newCapacity)
{
// 旧的散列表能否被开释
bool freeOld = canBeFreed();
// 获取旧的散列表
bucket_t *oldBuckets = buckets();
// 经过新的空间需求量创立新的散列表
bucket_t *newBuckets = allocateBuckets(newCapacity);
assert(newCapacity > 0);
assert((uintptr_t)(mask_t)(newCapacity-1) == newCapacity-1);
// 设置Buckets和Mash,Mask的值为散列表长度-1
setBucketsAndMask(newBuckets, newCapacity - 1);
// 开释旧的散列表
if (freeOld) {
cache_collect_free(oldBuckets, oldCapacity);
cache_collect(false);
}
}
上述源码中初次传入reallocate
函数的newCapacity
为INIT_CACHE_SIZE
,INIT_CACHE_SIZE
是个枚举值,也便是4。因而散列表开始创立的空间便是4个。
enum {
INIT_CACHE_SIZE_LOG2 = 2,
INIT_CACHE_SIZE = (1 << INIT_CACHE_SIZE_LOG2)
};
上述源码中能够发现散列表进行扩容时会将容量增至之前的2倍。
2.4 find 函数
终究来看一下散列表中怎么快速的经过key
找到相应的bucket
呢?咱们来到find
函数内部
bucket_t * cache_t::find(cache_key_t k, id receiver)
{
assert(k != 0);
// 获取散列表
bucket_t *b = buckets();
// 获取mask
mask_t m = mask();
// 经过key找到key在散列表中存储的下标
mask_t begin = cache_hash(k, m);
// 将下标赋值给i
mask_t i = begin;
// 假如下标i中存储的bucket的key==0阐明当时没有存储相应的key,将b[i]回来出去进行存储
// 假如下标i中存储的bucket的key==k,阐明当时空间内现已存储了相应key,将b[i]回来出去进行存储
do {
if (b[i].key() == 0 || b[i].key() == k) {
// 假如满足条件则直接reutrn出去
return &b[i];
}
// 假如走到这儿阐明上面不满足,那么会往前移动一个空间从头进行判定,知道能够成功return停止
} while ((i = cache_next(i, m)) != begin);
// hack
Class cls = (Class)((uintptr_t)this - offsetof(objc_class, cache));
cache_t::bad_cache(receiver, (SEL)k, cls);
}
函数cache_hash (k, m)
用来经过key
找到办法在散列表中存储的下标,来到cache_hash (k, m)
函数内部
static inline mask_t cache_hash(cache_key_t key, mask_t mask)
{
return (mask_t)(key & mask);
}
能够发现cache_hash (k, m)
函数内部只是是进行了key & mask
的按位与运算,得到下标即存储在相应的方位上。按位与运算在上文中已具体解说过,这儿不在赘述。
2.5 _mask
经过上面的剖析咱们知道_mask
的值是散列表的长度减一,那么任何数经过与_mask
进行按位与运算之后取得的值都会小于等于_mask
,因而不会出现数组溢出的情况。
举个比如,假设散列表的长度为8,那么mask的值为7
0101 1011 // 任意值
& 0000 0111 // mask = 7
------------
0000 0011 //获取的值一直等于或小于mask的值
3.办法调用总结
-
初次办法查找与缓存:
-
初次办法查找: 当第一次运用办法时,
音讯机制
经过isa指针
找到class/meta-class
-
办法缓存: 遍历办法列表找到办法之后(假如找不到就调用superclass去父类中找),会对办法以
SEL为keyIMP为value
的办法缓存在cache
的_buckets
中 -
散列表下标: 当第一次存储的时分,会创立具有4个空间的散列表,并将
_mask
的值置为散列表的长度减一,之后经过SEL & mask
核算出办法存储的下标值,并将办法存储在散列表中- 举个比如,假如核算出下标值为3,那么就将办法直接存储在下标为3的空间中,前面的空间会留空
-
初次办法查找: 当第一次运用办法时,
-
散列表扩容:
- 当散列表中存储的办法占有散列表长度超越3/4的时分,散列表会进行扩容操作:
- 将创立一个新的散列表而且空间扩容至原来空间的两倍
- 并重置
_mask
的值 - 终究开释旧的散列表
- 此刻再有办法要进行缓存的话,就需求从头经过
SEL & mask
核算出下标值之后在按照下标进行存储了
- 当散列表中存储的办法占有散列表长度超越3/4的时分,散列表会进行扩容操作:
-
散列表下标核算:
- 假如一个类中办法许多,其间很可能会出现多个办法的
SEL & mask
得到的值为同一个下标值 - 假如核算出来的下标有值,那么会调用
cache_next
函数往下标值-1位去进行存储 - 假如下标值-1位空间中有存储办法,而且key不与要存储的key相同,那么再到前面一位进行比较,直到找到一位空间没有存储办法或许
key
与要存储的key
相同停止 - 假如到下标0的话就会到下标为
_mask
的空间也便是最大空间处进行比较。
- 假如一个类中办法许多,其间很可能会出现多个办法的
-
非初次办法查找:
- 当要查找办法时,并不需求遍历散列表,相同经过
SEL & mask
核算出下标值,直接去下标值的空间取值即可 - 同上,假如下标值中存储的key与要查找的key不相同,就去前面一位查找。
- 这样尽管占用了少量空间,可是大大节省了时刻,也便是说其实apple是运用空间交换时刻的一种办法查找算法优化测战略。
- 当要查找办法时,并不需求遍历散列表,相同经过
经过一张图更明晰的看一下其间的流程:
4. 验证上述流程
经过一段代码演示一下 。相同运用仿照objc_class结构体
自界说一个结构体,并进行强制转化来检查其内部数据,自界说结构体在之前的文章中运用过屡次这儿不在赘述。
咱们创立Person
类承继NSObject
,Student
类承继Person
,CollegeStudent
承继Student
。三个类别离有personTest,studentTest,colleaeStudentTest
办法
经过打印断点来看一下办法缓存的进程
int main(int argc, const char * argv[]) {
@autoreleasepool {
CollegeStudent *collegeStudent = [[CollegeStudent alloc] init];
xx_objc_class *collegeStudentClass = (__bridge xx_objc_class *)[CollegeStudent class];
cache_t cache = collegeStudentClass->cache;
bucket_t *buckets = cache._buckets;
[collegeStudent personTest];
[collegeStudent studentTest];
NSLog(@"----------------------------");
for (int i = 0; i <= cache._mask; i++) {
bucket_t bucket = buckets[i];
NSLog(@"%s %p", bucket._key, bucket._imp);
}
NSLog(@"----------------------------");
[collegeStudent colleaeStudentTest];
cache = collegeStudentClass->cache;
buckets = cache._buckets;
NSLog(@"----------------------------");
for (int i = 0; i <= cache._mask; i++) {
bucket_t bucket = buckets[i];
NSLog(@"%s %p", bucket._key, bucket._imp);
}
NSLog(@"----------------------------");
NSLog(@"%p",@selector(colleaeStudentTest));
NSLog(@"----------------------------");
}
return 0;
}
咱们别离在collegeStudent
实例目标调用personTest,studentTest,colleaeStudentTest
办法处打断点检查cache
的改变。
personTest
办法调用之前:
从上图中能够发现:
-
personTest
办法调用之前,cache
中只是存储了init办法
- 上图中能够看出
init办法
刚好存储在下标为0的方位因而咱们能够看到 -
_mask
的值为3
验证咱们上述源码中提到的散列表第一次存储时会分配4个内存空间 -
_occupied
的值为1证明此刻_buckets
中只是存储了一个办法。
当collegeStudent
在调用personTest
的时分:
- 首要发现
collegeStudent类目标
的cache
中没有personTest办法
,就会去collegeStudent类目标
的办法列表中查找 - 办法列表中也没有,那么就经过
superclass指针
找到Student类目标
-
Studeng类目标
中cache
和办法列表相同没有,再经过superclass指针
找到Person类目标
- 终究在
Person类目标
办法列表中找到之后进行调用,并缓存在collegeStudent类目标
的cache
中。
履行personTest
办法之后检查cache
办法的改变:
上图中能够发现:
-
_occupied
值为2,阐明此刻personTest
办法现已被缓存在collegeStudent类目标
的cache
中
同理履行过studentTest
办法之后,咱们经过打印检查一下此刻cache
内存储的信息
上图中能够看到cache
中的确存储了 init 、personTest 、studentTest
三个办法。
那么履行过colleaeStudentTest办法
之后此刻cache
中应该对colleaeStudentTest办法
进行缓存。
前面源码提到过,当存储的办法数超越散列表长度的3/4时,体系会从头创立一个容量为原来两倍的新的散列表替代原来的散列表。
过掉colleaeStudentTest办法
,从头打印cache
内存储的办法检查:
从图中可看出:
-
_bucket
散列表扩容之后只是存储了colleaeStudentTest办法
- 而且上图中打印
SEL & _mask
位运算得出下标的值的确是_bucket
列表中colleaeStudentTest办法
存储的方位
至此现已对Class的结构及办法缓存的进程有了新的认知:
-
apple经过散列表的办法对办法进行缓存,以少量的空间节省了大量查找办法的时刻
专题系列文章
1.前常识
- 01-探求iOS底层原理|总述
- 02-探求iOS底层原理|编译器LLVM项目【Clang、SwiftC、优化器、LLVM】
- 03-探求iOS底层原理|LLDB
- 04-探求iOS底层原理|ARM64汇编
2. 基于OC言语探求iOS底层原理
- 05-探求iOS底层原理|OC的实质
- 06-探求iOS底层原理|OC目标的实质
- 07-探求iOS底层原理|几种OC目标【实例目标、类目标、元类】、目标的isa指针、superclass、目标的办法调用、Class的底层实质
- 08-探求iOS底层原理|Category底层结构、App启动时Class与Category装载进程、load 和 initialize 履行、相关目标
- 09-探求iOS底层原理|KVO
- 10-探求iOS底层原理|KVC
- 11-探求iOS底层原理|探求Block的实质|【Block的数据类型(实质)与内存布局、变量捕获、Block的品种、内存办理、Block的润饰符、循环引证】
- 12-探求iOS底层原理|Runtime1【isa详解、class的结构、办法缓存cache_t】
- 13-探求iOS底层原理|Runtime2【音讯处理(发送、转发)&&动态办法解析、super的实质】
- 14-探求iOS底层原理|Runtime3【Runtime的相关使用】
- 15-探求iOS底层原理|RunLoop【两种RunloopMode、RunLoopMode中的Source0、Source1、Timer、Observer】
- 16-探求iOS底层原理|RunLoop的使用
- 17-探求iOS底层原理|多线程技能的底层原理【GCD源码剖析1:主队列、串行队列&&并行队列、大局并发队列】
- 18-探求iOS底层原理|多线程技能【GCD源码剖析1:dispatch_get_global_queue与dispatch_(a)sync、单例、线程死锁】
- 19-探求iOS底层原理|多线程技能【GCD源码剖析2:栅门函数dispatch_barrier_(a)sync、信号量dispatch_semaphore】
- 20-探求iOS底层原理|多线程技能【GCD源码剖析3:线程调度组dispatch_group、事情源dispatch Source】
- 21-探求iOS底层原理|多线程技能【线程锁:自旋锁、互斥锁、递归锁】
- 22-探求iOS底层原理|多线程技能【原子锁atomic、gcd Timer、NSTimer、CADisplayLink】
- 23-探求iOS底层原理|内存办理【Mach-O文件、Tagged Pointer、目标的内存办理、copy、引证计数、weak指针、autorelease
3. 基于Swift言语探求iOS底层原理
关于函数
、枚举
、可选项
、结构体
、类
、闭包
、特点
、办法
、swift多态原理
、String
、Array
、Dictionary
、引证计数
、MetaData
等Swift根本语法和相关的底层原理文章有如下几篇:
- Swift5中心语法1-根底语法
- Swift5中心语法2-面向目标语法1
- Swift5中心语法2-面向目标语法2
- Swift5常用中心语法3-其它常用语法
- Swift5使用实践常用技能点
其它底层原理专题
1.底层原理相关专题
- 01-核算机原理|核算机图形烘托原理这篇文章
- 02-核算机原理|移动终端屏幕成像与卡顿
2.iOS相关专题
- 01-iOS底层原理|iOS的各个烘托结构以及iOS图层烘托原理
- 02-iOS底层原理|iOS动画烘托原理
- 03-iOS底层原理|iOS OffScreen Rendering 离屏烘托原理
- 04-iOS底层原理|因CPU、GPU资源消耗导致卡顿的原因和处理计划
3.webApp相关专题
- 01-Web和类RN大前端的烘托原理
4.跨渠道开发计划相关专题
- 01-Flutter页面烘托原理
5.阶段性总结:Native、WebApp、跨渠道开发三种计划功能比较
- 01-Native、WebApp、跨渠道开发三种计划功能比较
6.Android、HarmonyOS页面烘托专题
- 01-Android页面烘托原理
-
02-HarmonyOS页面烘托原理 (
待输出
)
7.小程序页面烘托专题
- 01-小程序结构烘托原理