如果没有听说过 isa_t,只是听说过 isa 指针,那么这篇简书值得一看。
一、位域与联合体
1.1 位域
位域,这东西挺少见的,并不是 OC 特有,其实是 C 语言语法。先来看一个定义:
// 位域
struct HGBitFiled {
uintptr_t tall:1;
uintptr_t rich:1;
uintptr_t handsome:1;
};
这不是结构体么?不是的,每个成员的后面还有一个分号与一个数字呢。这就是位域。
这里的位,就是通常所说的一个 bit 位的意思,其实狠多的时候在使用 BOOL 类型的时候,没有必要弄一个 BOOL 来存储数据的,仅仅需要一个 bit 的 0 与 1 就可以了,所以上面的 ?HGBitFiled 仅仅使用 3 个bit 就代表了三个状态。
在使用的时候,与结构体的使用是一样的。接下来有一个试验(我将后面的位数稍微改了一下):
image.png
再将 tall 的值换一下,我换一种打印方式:
image.png
看懂了这两张图片,位域就差不多精通了。
1.2 联合体
1.2.1 简单的例子
其实联合体是 C 语言的语法,并非 OC 特有,我想在实际的项目开发中使用过的小伙伴不多吧。可能很多人都没有听说过。
联合体 是一种结构、也叫 共用体,在这个结构中能够不同类型的成员,但同一时间仅仅能存放当中的一种。看一个实际的例子:
// 定义联合体
union HGUnion {
int info;
char bits;
};
image.png
忽然恍然大悟,联合体即使可以有多个成员,但是在同一时间只能有其中的一种有效。对于一个联合体来说,他的长度就是其成员长度最长的那个,比如上面的例子就是 int 的长度,因为 int 的长度比 char 的长。
通过上面的例子,我们还能发现在一个联合体中的数据是共用的,对于长度不一的数据,他们是右对齐的规律来取数据的。
1.2.2 与位域结合
变成了这样:
// 定义联合体
union HGUnion {
int info;
char bits;
// 位域
struct {
uintptr_t tall:1;
uintptr_t rich:2;
uintptr_t handsome:3;
};
};
多么奇怪的语法,这里要注意的是,联合体重的位域,是一个只读的。
代表当前成员每个位置的值。比如:
image.png
没有什么可以解释的,所有的精髓都在图片中。那么问题来了,联合体到底有什么作用呢?
1.2.3 isa_t
通常我们定义一个数据结构,比如一个类。我们希望这个类的实例对象的数据结构必须都是一样,比如一个 Person, 都有手,有脚,都要吃饭。但是有的场景是不用这样的,同一个数据结构可能需要分成不同的场景,说得很抽象,本来想单独的举个例子的,发现不好找,所以就直接说 OC 中的 isa 吧。
在说到 isa,这里有一个问题,如果别人问题: OC 中的 isa 是什么?你是否会回答:是一个对象的类型,也就是 Class。如果这样回答的话,对方必然会立马给你一个鄙视的远光。其实在很久很久之前,isa 就已经不是一个 Class 类型了,是一个联合体 isa_t 了。
直接看一下源代码中的定义:
很多,也很长,但是我可以将其整理一下,去掉一些与理解无关的,如下:
union isa_t
{
Class cls;
uintptr_t bits;
#if SUPPORT_PACKED_ISA
# 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)
};
// SUPPORT_PACKED_ISA
#endif
};
对于一个联合体来说它有多少种功能,它就会有多个成员。联合体主要使用的不是定义的成员,而是成员中具体位的意义。
比如以上的 isa_t 这个联合体中有两个成员:cls 与bits。意思就是在某些情况下 cls 的值是有效的,有的时候 bits 是有效的,在以上代码中的 SUPPORT_PACKED_ISA 有值的时候, bits 就是有效的。这是后来苹果对 isa 的一个优化,主要是在 isa 中添加了很多的信息,已经不仅仅包含 当前对象的类型,还包括比如是否被若指针引用过,是否被关联过,等等。
具体 isa_t 中的位的意义,其实都是在位域中给出,具体的描述可以见下图:
来自小码哥
这里面其实还有很多东西是需要介绍的,在这里就一切从简,更多详细的也可以参考这篇简书 Objective-C 中的对象,在这里直接说说如何在 isa_t 中查看信息当前对象对应的 Class。
1.3 从 isa_t 中提取对应对象的 Class
在上面已经简单的描述过,现在的 isa 指针已经不是简单的 Class 地址了,还有其它丰富的内容。那么应该如何从中提取这些信息呢?先从联合体 isa_t 入手分析。isa_t 中有两个成员 Class 与 bits,说明这两个成员有两种场景,一种是直接就是一个 Class 的值,另一种却是 bits。这里直接给出答案:在模拟器环境下,isa 的值就是 Class 的类型地址,在真机 arm64的环境下 bits 的值才有效。
先来看一下具体的两个实验:
1.3.1 模拟器
image.png1.3.2 arm64 真机
image.png厉害了,还真的不一样。但是 按位与 之后,具体的 Class 地址就出来了,0x0000000ffffffff8 这个值不是凭空而来的。在 isa_t 中就有的:
define ISA_MASK 0x0000000ffffffff8ULL
就是这个值。
通过上面的位域的定义,我们知道在 isa_t 中 shiftcls 就代表当前对象的 Class。那么我们再来验证一下这个 0x000005a1004979cf 的值:
0x00000001004979c8 的值:
我去,划红线的部分就是33位,还真是一模一样。由于 在 isa_t 中的前3位是其它的信息,所以对 shiftcls (Class) 来说都应该是 0。这也是为什么 Class 的地址为什么最后的16进制的值,只可能是0或者8的原因。是不是有点像MS题,有的MS 官真的很蛋疼,闲得没事找事干。关于这种MS题我曾经遇到过一个类似的,大家可以参考一下:Block MS题,15年遇到的问题,17年MS被问到,但是最后一问没有回答上来。
其实关于上面除 shiftcls 以外的值,可以自行研究。
二、散列表
2.1 简单概念
散列表,也叫哈希表。具体的定义就不做介绍,在 OC 中的方法缓存的实现原理使用的就是散列表。在 OC 中,一个方法被调用之后是会对当前的方法进行缓存的,那具体是怎么缓存的呢。答案就是使用散列表(哈希表)机制。
散列表的数据结构其实就是一个数组,但是散列表在数组的结构上会对取值与存值由所不同。在数组中我们要找出具体特征(key)的值,是需要通过编列的方法一个一个的 对特征(key)对比,找到与之对应的,然后直接返回。但是散列表就不一样了,是通过 特征(key)直接计算出当前的值的 index,然后直接取出来:
index=f(key)
得到 index 之后,就直接在散列表中取出对应的值即可。
2.2 OC 缓存机制
那具体是如何实现的呢?简单的看一下苹果的源代码,粗略的进行一下分析(代码有删减,去掉一些复杂的逻辑):
这个结构体 暂且可以理解成 OC 中方法的结构:
struct bucket_t {
cache_key_t _key;
IMP _imp;
};
_key:可以理解成 SEL。
_imp:就是一个方法对应的实现。
其次是一个缓存结构体:
struct cache_t {
bucket_t *_buckets;
mask_t _mask;
mask_t _occupied;
};
在这个结构体中,有三个成员:
_buckets:就是一个散列表。
_mask:当前散列表的容量大小 - 1,比如容量是4,那么这个是就是3。
_occupied:当前散列表的实际的元素大小。
具体的数据结构已经有了,那具体应该如何的存值与取值呢?这里就涉及到不同的散列表必定有不同的一套算法,就是上面提到的 key 与 index 的关系:
index=f(key)
其实苹果在这一步的实现也很简单:
index = _key & _mask;
通过一个 key(sel)与当前缓存的 _mask 做一个 按位与 的操作,直接得出一个对应的 index, 所得到的这个 index 肯定是比 _mask 还要小的一个数值。
很容易能想到,可能不同的 _key 按位与 出来的结果是一样的,这种情况怎么办,其实当得到这个 index 值之后,还有一个操作,取出这个位置的 bucket,然后与这个 bucket 的 _key 做对比,如果不相等,说明不是我们想要的,然后会自动的将 index 的减一之后再对比,具体的源代码如下(主要看 arm64 部分):
#if __arm__ || __x86_64__ || __i386__
// objc_msgSend has few registers available.
// Cache scan increments and wraps at special end-marking bucket.
#define CACHE_END_MARKER 1
static inline mask_t cache_next(mask_t i, mask_t mask) {
return (i+1) & mask;
}
#elif __arm64__
// objc_msgSend has lots of registers available.
// Cache scan decrements. No end marker needed.
#define CACHE_END_MARKER 0
static inline mask_t cache_next(mask_t i, mask_t mask) {
return i ? i-1 : mask;
}
#else
#error unknown architecture
#endif
不管是对存值还是取值,思路都是一样的。但是在存值的时候,还会出现一个问题:当前的缓存中已经是最大容量了,怎么办?那接下来的事情就是扩容。
比如当前的 _occupied = _mask + 1;, 那么就要考虑扩容了。在散列表中扩容的思路通常都是这样,通过一定的机制加大 _mask 的值,将现有的 _occupied 清零,并清空所有的缓存,感觉还是挺暴力的。
2.3 小总结
以上简单的介绍了一下概念以及粗略的介绍了一下 OC 方法的缓存机制。其实所有的散列表的逻辑都是类似的,只是 key 与 index 的映射关系会有所不同,已经 index 相同的采用机制有所不同。
从上面的介绍也能看出,其实散列表主要解决的就是快速的在一个列表中取值。在其中会消耗一些不必要的内存空间,这也是一个经典的以空间换时间的例子。
所以一定要记住,如果一旦有人问你:在 OC 中的方法调用步骤,不要忘记了方法有缓存。首先肯定是去缓存中取,取不到才会有消息发送、动态解析与消息转发这些步骤。
三、isEqual&hash
直接看图片:
image.png
一个是协议方法、一个是协议属性,它们即分离又有着千丝万理的关系。 先看一个简单的例子:
image.png
巧了,hash 的值就是当前对象的地址值。是的,系统就是这么搞的。
其实 isEqual 与hash 还是很好理解的,通常都是使用 NSSet 来做介绍,网上很多。主要的逻辑是,每次将元素添加到 Set 的时候,首先调用对象的 hash 值来做对比,如果 hash 不一样,那么添加就成功。如果两个 hash 值一样,那么还会调用其 isEqual: 方法进行对比,如果不相等相等则添加成功,如果相等就添加成功。所以将一个元素添加到 Set 首先判断 hash ,其次判断 isEqual: 方法,只有两个都相等的情况下才不能添加成功。
所以一旦重写了 isEqual: 的情况,还需要考虑一下是否需要重写 hash 方法的情况。比如有一个 Persion 类,通过其 id 就能判断是否为同一个人,那么就需要重写 isEqual:,如果仅仅是一个对比的话,到这就结束了。如果还需要将不同的 Persion 实例对象都放到一个 Set 中的话,就肯定要重写 hash 了。
很简单,就不用代码举例子了。
五、思考
最近一直在思考的一个问题: 如何才能有钱啊?????
网友评论