iOS:isa指针

时间:2021-7-5 作者:qvyue

一、联合体

1. 概念

联合体,是一种特殊的数据类型,其目的是节省内存。联合体内部可以定义多种数据类型,但是同一时间只能表示某一种数据类型,且所有的数据类型共享同一段内存。联合体的内存大小等于所定义的数据类型中占用内存的最大者。

2. 互斥赋值/共用内存

允许装入该“联合”所定义的任何一种数据成员,但同一时间只能表示一种数据成员,采用了覆盖的技术;

如:

union Test {
    char name;
    int age;
    long height;
};

void printUnion(union Test t) {
    printf("%cn",t.name);
    printf("%dn",t.age);
    printf("%ldn",t.height);
    printf("---------n");
}

int main(int argc, const char * argv[]) {
    union Test t;
    t.name = 'a';
    printUnion(t);
    
    t.age = 200;
    printUnion(t);
    
    t.height = 10000;
    printUnion(t);

    return 0;
}

输出结果:

a
97
97
---------
310
200
200
---------
�
10000
10000
---------

3. union所占内存长度

规则如下:

  • union 变量所占用的内存长度等于最长的成员的内存长度;

当然,union 所占内存长度也会受到内存对齐规则的限制,会区分不同架构,这里就不赘述了;

另外,联合体经常和位域一起使用。位域简而言之就是规定成员变量占有固定位数而不使用其类型所占大小,有几个特点:

  1. 位域的大小不能超过类型本身的大小;
  2. 位域单位为 bit 而不是字节;

举个例子:

struct {
    int aa;
    long bb;
    long cc;
} s1;

struct {
    int aa : 32;
    long bb : 32;
    long cc : 1;
} s2;
printf("%dn",sizeof(s1)); //24
printf("%dn",sizeof(s2)); //16

如上 s1 结构体为 4 + 8 + 8 = 20 ,然后按照 long 的 8 字节对齐为 24 字节;

而使用位域之后为 32/8 + 32/8 + 1/8 = 4 + 4 + 1 = 9 字节,然后按照 long 的 8 字节对齐为 16;

二、isa源码梳理

首先,我们肯定想直接来看看 isa ,那么尝试一下就会发现 isa 这个接口,但是会报提示:

iOS:isa指针
error

也就是说,在原来的版本中 obj 可以直接访问 isa 指针,但是现在只能通过 object_getClass 来访问;

那么 object_getClass 的源码就是突破口,来看看:

Class object_getClass(id obj)
{
    if (obj) return obj->getIsa();
    else return Nil;
}

很显然,调用了 object 的 getIsa() 接口,可是搜索 getIsa() 接口发现有两个:

iOS:isa指针
getIsa

那肯定是用宏定义区分了环境的,关键宏定义就是 SUPPORT_TAGGED_POINTERS,此时就引入了第一个问题~~~

1. Tagged Pointer

要说 Tagged Pointer ,大部分人应该度过 唐巧 – 深入理解 Tagged Pointer,这里就不再赘述了,总结一下:

  1. Tagged Pointer 针对的是 NSNumber、NSDate、NSString 等包装类型;
  2. 在 32 位架构向 64 位架构转换时,指向包装类型的指针从 4 字节扩大为 8 字节,浪费严重。Tagged Pointer 将指针分为 flag + data 两部分,不指向实际的内存地址。
  3. flag 部分标志该指针为 Tagged Pointer,data 部分包含实际数据;
  4. 因为不指向实际的内存地址,堆内存中就不会存在包装类型的真实对象,从而也不会有 malloc、dealloc 等操作,加快了运行速度;

一张图作总结:

iOS:isa指针
Tagged Pointer

2. SUPPORT_TAGGED_POINTERS

因为 Tagged Pointer 不指向实际的指针,那么在 objc 的 isa 体系下,原先直接返回 isa 指针的逻辑会出现问题。

比如 NSNumber 直接返回 isa 时,返回的是一个 Tagged Pointer 并不指向一个类对象,所以需要兼容 Tagged Pointer。而 SUPPORT_TAGGED_POINTERS 宏定义就是为了兼容这种情况,具体实现方式就是根据是否支持 Tagged Pointer 设计两个 ISA() 方法,如下:

不支持 Tagged Pointer 时:

// not SUPPORT_TAGGED_POINTERS

inline Class
objc_object::getIsa() 
{
    return ISA();
}

此时很简单,直接调用 objc_object 的 ISA 方法返回指针即可;

支持 Tagged Pointer 时:

// SUPPORT_TAGGED_POINTERS
inline Class
objc_object::getIsa() 
{
    if (fastpath(!isTaggedPointer())) return ISA();

    extern objc_class OBJC_CLASS_$___NSUnrecognizedTaggedPointer;
    uintptr_t slot, ptr = (uintptr_t)this;
    Class cls;

    slot = (ptr >> _OBJC_TAG_SLOT_SHIFT) & _OBJC_TAG_SLOT_MASK;
    cls = objc_tag_classes[slot];
    if (slowpath(cls == (Class)&OBJC_CLASS_$___NSUnrecognizedTaggedPointer)) {
        slot = (ptr >> _OBJC_TAG_EXT_SLOT_SHIFT) & _OBJC_TAG_EXT_SLOT_MASK;
        cls = objc_tag_ext_classes[slot];
    }
    return cls;
}

如上代码,第一句就先判断了该指针是否是 Tagged Pointer 类型的指针,如果不是仍然调用 ISA() 方法寻找指针,如果是,则进入了 Tagged Pointer 相关的处理逻辑;

fastpath 宏定义是编译器特性,意思是告诉编译器括号中的代码很高概率为 1,即不是 Tagged Pointer;和 fastpath 对应的是 slowpath。通过 fastpath 这种方式,编译器在编译过程中,会将可能性更大的代码紧跟着前面的代码,从而减少指令跳转带来的性能上的下降;

至此,ISA() 方法之前的逻辑已经理清楚了,总结一下逻辑吧:

iOS:isa指针
ISA()

接下来看看 ISA() 方法……

3. 架构区分

搜索 objc_object::ISA 也会该方法发现存在两个,而影响其逻辑的宏定义为 SUPPORT_NONPOINTER_ISA,该宏定义的定义逻辑是:

#if !SUPPORT_INDEXED_ISA  &&  !SUPPORT_PACKED_ISA
#   define SUPPORT_NONPOINTER_ISA 0
#else
#   define SUPPORT_NONPOINTER_ISA 1
#endif

不关注 SUPPORT_PACKED_ISA,来看看 SUPPORT_INDEXED_ISA 的定义:

#if __ARM_ARCH_7K__ >= 2  ||  (__arm64__ && !__LP64__)
#   define SUPPORT_INDEXED_ISA 1
#else
#   define SUPPORT_INDEXED_ISA 0
#endif

objc 源码中全局搜索 __ARM_ARCH_7K__ 会发现:

iOS:isa指针
__ARM_ARCH_7K__

也就是说这一段代码是为了判断架构是否为 armv7k 或者是 arm64_32,查阅官方文档可以发现:

iOS:isa指针
armv7k 和 arm64_32

也就是说:

  1. arm64_32 是指 watchOS 上 指针为 4 字节的 arm64 架构下;
  2. armv7k 是指 armv7 在 32 位系统上的变体,也是用于 watchOS;

由此,可以发现 arm64_32 和 armv7k 都是针对 WatchOS的,所以,可以做出总结,对于 iOS 设备而言:

  1. SUPPORT_INDEXED_ISA = 0;
  2. SUPPORT_NONPOINTER_ISA = 1;

所以,后面看代码就会比较清晰了~~~

4. 源码分析

ISA() 函数的代码为:

inline Class
objc_object::ISA(bool authenticated)
{
    ASSERT(!isTaggedPointer());
    return isa.getDecodedClass(authenticated);
}

继续看 isa.getDecodedClass

inline Class
isa_t::getDecodedClass(bool authenticated) {
#if SUPPORT_INDEXED_ISA
    if (nonpointer) {
        return classForIndex(indexcls);
    }
    return (Class)cls;
#else
    return getClass(authenticated);
#endif
}

因为 SUPPORT_INDEXED_ISA = 0,所以直接看 getClass() 方法:

inline Class
isa_t::getClass(MAYBE_UNUSED_AUTHENTICATED_PARAM bool authenticated) {
#if SUPPORT_INDEXED_ISA
    return cls;
#else

    uintptr_t clsbits = bits;

#   if __has_feature(ptrauth_calls)
#       if ISA_SIGNING_AUTH_MODE == ISA_SIGNING_AUTH
    // Most callers aren't security critical, so skip the
    // authentication unless they ask for it. Message sending and
    // cache filling are protected by the auth code in msgSend.
    if (authenticated) {
        // Mask off all bits besides the class pointer and signature.
        clsbits &= ISA_MASK;
        if (clsbits == 0)
            return Nil;
        clsbits = (uintptr_t)ptrauth_auth_data((void *)clsbits, ISA_SIGNING_KEY, ptrauth_blend_discriminator(this, ISA_SIGNING_DISCRIMINATOR));
    } else {
        // If not authenticating, strip using the precomputed class mask.
        clsbits &= objc_debug_isa_class_mask;
    }
#       else
    // If not authenticating, strip using the precomputed class mask.
    clsbits &= objc_debug_isa_class_mask;
#       endif

#   else
    clsbits &= ISA_MASK;
#   endif

    return (Class)clsbits;
#endif
}

这段代码比较复杂,但是其实也就三部分:

  1. SUPPORT_INDEXED_ISA 的判断,因为 iOS 下必定是 0,直接忽略;
  2. __has_feature(ptrauth_calls) 逻辑;
  3. clsbits &= ISA_MASK;

说说第二点,__has_feature 是编译器特性,而 ptrauth_calls 指的是 Pointer Authentication Codes,简称PACs,在 A12 中被引入,即从 iphone X/XR/XS 开始被引入。

PACs 功能简单来说,因为 64 位架构下,指针基本用不满,所以在高位存储一个指针签名,使用时进行验证防止被篡改,具体可以参阅为什么 arm64e 的指针地址有空余支持 PAC?,这里就不赘述了。

总之,PACs 技术也会影响到 ISA 指针的寻址逻辑。略过 PACs 相关的代码,isa 的寻址逻辑就非常清晰了,就是两行代码:

inline Class
isa_t::getClass(MAYBE_UNUSED_AUTHENTICATED_PARAM bool authenticated) {
    uintptr_t clsbits = bits;
    clsbits &= ISA_MASK;
}

即:取出 bits 然后使用 ISA_MASK 进行掩码处理得到 isa 真实地址;

5. 结果验证

通过源码已经得出结论,那么接下来进行验证。

跑在真机 iphone7 iOS12 上,不是 iphoneX 及 A12 以上,所以 ISA_MASK0x0000000ffffffff8ULL,测试代码如下:

int main(int argc, char * argv[]) {
    NSObject *obj1 = [NSObject new];
    NSObject *obj2 = [NSObject new];

    Class cls1 = object_getClass(obj1);
    Class cls2 = object_getClass(obj2);

    NSLog(@"cls1:%@",cls1);
    NSLog(@"cls2:%@",cls2);
}

实验结果如下:

iOS:isa指针
obj1

再来看看 obj2:

iOS:isa指针
obj2

如上图,obj2 存储的指针和 obj1 为同一个,那都不用 MASK 了,肯定是同一个。

上述的 x/x4w 等操作涉及到大小端问题,就不再赘述了~~~

三、isa 结构体详解

objc4-818.2 中 isa 结构体源码:

union isa_t {
    isa_t() { }
    isa_t(uintptr_t value) : bits(value) { }

    uintptr_t bits;

private:
    // Accessing the class requires custom ptrauth operations, so
    // force clients to go through setClass/getClass by making this
    // private.
    Class cls;

public:
#if defined(ISA_BITFIELD)
    struct {
        ISA_BITFIELD;  // defined in isa.h
    };

    bool isDeallocating() {
        return extra_rc == 0 && has_sidetable_rc == 0;
    }
    void setDeallocating() {
        extra_rc = 0;
        has_sidetable_rc = 0;
    }
#endif

    void setClass(Class cls, objc_object *obj);
    Class getClass(bool authenticated);
    Class getDecodedClass(bool authenticated);
};

这里需要注意下 在 objc4-818.2 中,cls 被设置成了 private。看注释也可以很清楚的看到,因为 ptrauth 的原因,cls 已经不允许被直接访问了,操作 isa 指针,必须通过 setClass/getClass 方法;

其实在之前版本中,不涉及到 ptrauth,所以 isa 的联合体代码很简单,暂不研究 PACs 技术相关的实现,所以直接来看看 arm64 且 A12(iphoneX/XR/XS) 版本以下的源码 :

union isa_t {
    isa_t() { }
    isa_t(uintptr_t value) : bits(value) { }

    Class cls;
    uintptr_t bits;
#if defined(ISA_BITFIELD)
    struct {
        ISA_BITFIELD;  // defined in isa.h
    };
#endif
};

拎出 ISA_BITFIELD,使用旧版本的代码正式进入 isa 中 struct 的分析:

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 unused            : 1;
    uintptr_t has_sidetable_rc  : 1;
    uintptr_t extra_rc          : 19
}

1. nonpointer

如果 nonpointer 为0,代表 raw isa,也就是没有结构体的部分,访问对象的 isa 会直接返回一个指向 cls 的指针,也就是在 iPhone 迁移到 64 位系统之前时 isa 的类型。

如果为1,代表它不是指针,是已经优化的isa,关于类的信息存储在shiftcls中。

这里肯定恒为 1,为 0 也不会有这个 struct 了;

上例中可以看到 nonpointer 为 1:

iOS:isa指针
nonpointer

2. has_assoc

是否有关联对象,详见iOS:关联对象;

3. has_cxx_dtor

iOS:析构流程

4. shiftcls

真正存储类对象的位置;

其实从这里就可以看出来ISA_MASK 在 A12 以下的 arm64 架构下,为什么为 0x0000000ffffffff8ULL

iOS:isa指针
ISA_MASK

如上图,值为 1 的比特位正好为第 3 到 第 36 位,也就是 shiftcls 对应的位置;

那为什么这里使用 33 位就能表示一个 objc 中的指针呢?x86_64 位下为什么为 44 个呢?

5. magic

用于调试器判断当前对象是真的对象还是没有初始化的空间 , 固定为 0x1a

6. weakly_referenced

是否存在弱引用,即是否有被 __weak 修饰的对象引用;

7. unused

未知,应该是做保留位用;

8. has_sidetable_rc

是否有 sidetable;

extra_rc 在溢出时,会新建一个 sidetable,并将一半的引用计数器转移到 sidetable 中。这个标志位如果存在,则表明计算引用计数器时需要考虑到 sidetable 中的值;

9. extra_rc

其意义为:对象的引用计数 ;

这里需要注意,extra_rc 的含义是区分版本的:

  1. objc4-818.2:extra_rc = 引用计数器
  2. objc4-818.2 之前:extra_rc = 真实引用计数器 – 1(真实引用计数器 = extra_rc + 1);

objc4-818.2 中的源码:

iOS:isa指针
rootRetainCount

objc4-756.2 中的源码:

iOS:isa指针
rootRetainCount

与之匹配的,在 objc4-818.2 的 objc_object::initIsa() 方法中,将 extra_rc 设置为了 1;

接下来,看个实例,运行在 iOS12 真机上:

iOS:isa指针
extra_rc和weakly_referenced

如上图,代码是 818.2 之前的版本,obj1 的引用计数器很明显为 3,所以 extra_rc = 3 – 1 = 2;

在来看看 obj2 是不是 1 进行对比确认:

iOS:isa指针
obj1

如图,obj2 的 extra_rc = 1-1 = 0,且没有被 __weak 对象引用,所以 weakly_referenced 为 0;

至此,isa 的基础知识全部梳理完毕,接下来研究下 sidetable 的详细实现,一张图总结:

iOS:isa指针
isa结构体

(注:图片来自“style_月月”的博客)

三、补充

1. sidetable 的使用

详见(iOS:SideTable)[https://www.jianshu.com/p/e0b03d2be843];

2. isa和superClass指向

这玩意讲太多,都讲烂了,相关的面试题也比较多,这里就不赘述了。关系如图:

iOS:isa指针
isa和superClass指向

这里需要重点关注的是:

  1. 所有元类对象的 isa 指针都指向跟元类;
  2. 根元类(Meta_NSObject)的 superClass 指针指向跟类对象(NSObject);
  3. 跟类对象(NSObject)的 superClass 指向为nil;

由以上三点延伸出来的面试题就不再赘述了,真忘记了,看源码或者写代码做测试是最直接的,死记硬背一些面试题,个人感觉没什么太大意义~~~

实例对象中未存储 superClass,其信息存储在类对象/元类中。使用 super 关键字进行方法调用时,本质是调用 super_objcMsgSend()

四、疑问

1. shiftcls 为什么占 33 和 44 位?

根据 isa 的 shiftcls 中的注释可知,不同架构下存在 MACH_VM_MAX_ADDRESS ,即虚拟内存的最大地址,这个 33 位 和 44 位的设计就是根据这个最大地址来设计的。

粗略计算了 MACH_VM_MAX_ADDRESS 和 33/44 位最大值的对比,shiftcls 仍然无法完全覆盖最大地址,猜测虚拟内存最大的几位还有其他作用,所以这里知道 shiftcls 所占位数和不同架构洗啊的虚拟内存最大值有关即可,不再深究~~~

2. objc4-818.2 中为什么要改变 extra_rc 的计算逻辑?

存疑~~

声明:本文内容由互联网用户自发贡献自行上传,本网站不拥有所有权,未作人工编辑处理,也不承担相关法律责任。如果您发现有涉嫌版权的内容,欢迎发送邮件至:qvyue@qq.com 进行举报,并提供相关证据,工作人员会在5个工作日内联系你,一经查实,本站将立刻删除涉嫌侵权内容。