美文网首页移动开发作家群(719776724)分享专题
隐藏在 OC 中的那些暗招 &位域&联合体&am

隐藏在 OC 中的那些暗招 &位域&联合体&am

作者: CoderHG | 来源:发表于2015-12-29 22:10 被阅读147次

    如果没有听说过 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 的 01 就可以了,所以上面的 ?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 了。
    直接看一下源代码中的定义:

    image.png

    很多,也很长,但是我可以将其整理一下,去掉一些与理解无关的,如下:

    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.png

    1.3.2 arm64 真机

    image.png

    厉害了,还真的不一样。但是 按位与 之后,具体的 Class 地址就出来了,0x0000000ffffffff8 这个值不是凭空而来的。在 isa_t 中就有的:

    define ISA_MASK 0x0000000ffffffff8ULL

    就是这个值。

    通过上面的位域的定义,我们知道在 isa_t 中 shiftcls 就代表当前对象的 Class。那么我们再来验证一下这个 0x000005a1004979cf 的值:

    image.png

    0x00000001004979c8 的值:

    image.png

    我去,划红线的部分就是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:当前散列表的实际的元素大小。

    具体的数据结构已经有了,那具体应该如何的存值与取值呢?这里就涉及到不同的散列表必定有不同的一套算法,就是上面提到的 keyindex 的关系:

    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 了。

    很简单,就不用代码举例子了。

    五、思考

    最近一直在思考的一个问题: 如何才能有钱啊?????

    推荐

    Objective-C 中的对象 & isa & superclass & 元类(metaClass)

    相关文章

      网友评论

        本文标题:隐藏在 OC 中的那些暗招 &位域&联合体&am

        本文链接:https://www.haomeiwen.com/subject/dfdihttx.html