美文网首页iOS底层探索
iOS底层探索之isa

iOS底层探索之isa

作者: loongod | 来源:发表于2020-09-10 15:59 被阅读0次

一、前置知识

1.1 C 共用体 || 联合体

共用体是一种特殊的数据类型,允许您在相同的内存位置存储不同的数据类型。您可以定义一个带有多成员的共用体,但是任何时候只能有一个成员带有值。共用体提供了一种使用相同的内存位置的有效方式。

定义
为了定义结构体,您必须使用 union 语句,方式与定义结构类似。union 语句定义了一个新的数据类型,带有多个成员。union 语句的格式如下:

 union [union tag]
    {
       member definition;
       member definition;
       ...
       member definition;
    } [one or more union variables];
// [one or more union variables] 是可选的
--
eg:
  union uData {
    int age;        // 4字节
    long height;    // 8字节
    char sex;       // 1字节
  };

现在,Data 类型的变量可以存储一个整数、一个浮点数,或者一个字符串。这意味着一个变量(相同的内存位置)可以存储多个多种类型的数据。您可以根据需要在一个共用体内使用任何内置的或者用户自定义的数据类型。

共用体占用的内存应足够存储共用体中最大的成员。例如,在上面的实例中,Data 将占用 8 个字节的内存空间,因为在各个成员中,height 所占用的空间是最大的。

访问共用体成员

        union uData u = {};
        u.height = 100;
        NSLog(@"u: %lu", sizeof(u));
        NSLog(@"age:%d, height: %ld, age: %hhd", u.age, u.height, u.sex);
        u.age = 25;
        NSLog(@"age:%d, height: %ld, age: %hhd", u.age, u.height, u.sex);
        u.sex = 'f';
        NSLog(@"age:%d, height: %ld, age: %hhd", u.age, u.height, u.sex); 

---
age:100, height: 100, age: 100
age:25, height: 25, age: 25
age:102, height: 102, age: 102

可验证最大空间为公用体中,最大的成员所占空间。也可知成员变量是访问的同一片内存空间。

1.2 C 位域

如果程序的结构中包含多个开关量,只有 TRUE/FALSE 变量,如下:

    struct
    {
      unsigned int widthValidated;
      unsigned int heightValidated;
    } status;

这种结构需要 8 字节的内存空间,但在实际上,在每个变量中,我们只存储 0 或 1。在这种情况下,C 语言提供了一中更好的利用内存空间的方式。如果您在结构内使用这样的变量,您可以定义变量的宽度来告诉编译器,您将只使用这些字节。例如,上面的结构可以重写成:

   struct
    {
      unsigned int widthValidated : 1;
      unsigned int heightValidated : 1;
    } status;

现在,上面的结构中,status 变量将占用 4 个字节的内存空间,但是只有 2 位被用来存储值。如果您用了 32 个变量,每一个变量宽度为 1 位,那么 status 结构将使用 4 个字节。

位域声明

    struct
    {
      type [member_name] : width ;
    };

下面是有关位域中变量元素的描述:

元素 描述
type 整数类型,决定了如何解释位域的值。类型可以是整型、有符号整型、无符号整型。
member_name 位域的名称。
width 位域中位的数量。宽度必须小于或等于指定类型的位宽度。

带有预定义宽度的变量被称为位域。位域可以存储多于 1 位的数,例如,需要一个变量来存储从 0 到 7 的值,您可以定义一个宽度为 3 位的位域,如下:

 struct
    {
      unsigned int age : 3;
    } Age;

上面的结构定义指示 C 编译器,age 变量将只使用 3 位来存储这个值,如果您试图使用超过 3 位,则无法完成。


二、isa探索

探索alloc 的时候,最后有个方法 initInstanceIsa 初始化 isa 并关联类。在此继续深入

inline void 
objc_object::initInstanceIsa(Class cls, bool hasCxxDtor)
{
    ASSERT(!cls->instancesRequireRawIsa());
    ASSERT(hasCxxDtor == cls->hasCxxDtor());

    initIsa(cls, true, hasCxxDtor);
}

inline void 
objc_object::initIsa(Class cls, bool nonpointer, bool hasCxxDtor) 
{ 
    ASSERT(!isTaggedPointer()); 
    
    if (!nonpointer) {
        isa = isa_t((uintptr_t)cls);
    } else {
        ASSERT(!DisableNonpointerIsa);
        ASSERT(!cls->instancesRequireRawIsa());

        isa_t newisa(0);
 
#if SUPPORT_INDEXED_ISA
        ASSERT(cls->classArrayIndex() > 0);
        newisa.bits = ISA_INDEX_MAGIC_VALUE;
        newisa.has_cxx_dtor = hasCxxDtor;
        newisa.indexcls = (uintptr_t)cls->classArrayIndex();
#else
        newisa.bits = ISA_MAGIC_VALUE;
        newisa.has_cxx_dtor = hasCxxDtor;
        newisa.shiftcls = (uintptr_t)cls >> 3;
#endif
        isa = newisa;
    }
}

查看下 SUPPORT_INDEXED_ISA 的定义

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

__ARM_ARCH_7K__: 是代表手表的宏,不满足
__arm64__: 表示64位ARM架构 满足
__LP64__: 表示指针长度为64位 满足
!__LP64__: 就不满足了

所以 SUPPORT_INDEXED_ISA 为 0

由此分析上面的主要代码为

        isa_t newisa(0);  // 声明并初始化一个isa_t
        newisa.bits = ISA_MAGIC_VALUE;
        newisa.has_cxx_dtor = hasCxxDtor;
        newisa.shiftcls = (uintptr_t)cls >> 3; // 重点,赋值了cls
        isa = newisa; // 赋值isa
2.1 isa_t结构
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_t 是一个联合体,里面有3个成员 clsbitsstruct,它们占用同一片内存区域。

然后找到 ISA_BITFIELD 的定义

# if __arm64__
#   define ISA_MASK        0x0000000ffffffff8ULL
#   define ISA_MAGIC_MASK  0x000003f000000001ULL
#   define ISA_MAGIC_VALUE 0x000001a000000001ULL
#   define ISA_BITFIELD                                                      \
      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
#   define ISA_BITFIELD                                                        \
      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)

运行环境是在mac上运行的,不是跑的真机,可以以 __x86_64__ 为例探索一下。

综上所述,所以isa_t的联合体,其中 shiftcls

  • arm64 下占33位(从第3位到35位)
  • __x86_64__ 下占44位(从第3位到46位)
union-isa_t存储.png
成员 介绍
nonpointer 表示是否对 isa 指针开启指针优化——0:纯 isa 指针;1:不止是类对象地址,isa 中包含了类信息、对象的引用计数等
has_assoc 关联对象标志位,0没有,1存在
has_cxx_dtor 该对象是否有 C++ 或者 Objc 的析构器,如果有析构函数,则需要做析构逻辑, 如果没有,则可以更快的释放对象
shiftcls 存储类指针的值,在开启指针优化的情况下,在 arm64 架构中有 33 位用来存储类指针
magic 用于调试器判断当前对象是真的对象还是没有初始化的空间
weakly_referenced 对象是否被指向或者曾经指向一个 ARC 的弱变量, 没有弱引用的对象可以更快释放
deallocating 标志对象是否正在释放内存
has_sidetable_rc 当对象引用技术大于 10 时,则需要借用该变量存储进位
extra_rc 当表示该对象的引用计数值,实际上是引用计数值减 1, 例如,如果对象的引用计数为 10,那么 extra_rc 为 9。如果引用计数大于 10, 则需要使用到下面的 has_sidetable_rc
2.2 shiftcls赋值 & 为什么右移3位(重点)
newisa打印.png

打个断点,在将要赋值 shiftcls 的时候,现在里面的所有值,都是因为 newisa.bits = ISA_MAGIC_VALUE 而来。(原因参考前置知识联合体)

好的,重点来了,为什么赋值 shiftcls 的时候要右移3位?

cls 是一个 Class 对象,注意是对象,点进去看一下定义

typedef struct objc_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
    // 省略 ...
}

再继续看 cache_tclass_data_bits_t 发现都是结构体,然后看里面的成员变量,大部分都是 uintptr_t 类型的,查看定义

typedef unsigned long           uintptr_t;

根据内存对齐原则,可知 Class 肯定是 8 字节对齐的,同样的,cls的指向地址(也既开始地址)肯定是 8 的倍数, 转换成二进制后,低三位肯定是 000

继续上图的打印:

(lldb) p (uintptr_t)cls
(uintptr_t) $1 = 4294980728
(lldb) p/t (uintptr_t)cls
(uintptr_t) $2 = 0b0000000000000000000000000000000100000000000000000011010001111000
(lldb) 

再联想联合体的说明,共用内存,可见苹果设计优化节省内存的良苦用心
赋值 shiftcls 的时候既没有改变 cls 的值,也最大的优化了内存使用。

我先开始到分析到这的时候,还是有疑问,存的时候是这样,但是取的时候呢?取的时候前三位不是 000 啊,,取的时候前三位确实不是 000,包括后面的几位。但是苹果在取的时候,又做了操作,接着往下分析

那赋值了 shiftcls 是不是就证明了我们关联了类呢?通常通过 x/4gx 打印实例的时候,第一个输出的 8 字节到底是不是 isa 呢?

2.3 object_getClass 验证isa是否存的是class
        LGPerson *p = [LGPerson alloc];
        // #import <malloc/malloc.h>
        // Returns the class of an object.
        id tp = object_getClass(p); 

可在 objcruntime 里面的 objc-class.mm 源码中查到

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

objc_object

inline Class 
objc_object::getIsa() 
{    // oc对象可认为isTaggedPointer为false
    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;
}

会调用 ISA()

inline Class 
objc_object::ISA() 
{
    ASSERT(!isTaggedPointer()); 
  // 上面分析过 SUPPORT_INDEXED_ISA = 0
#if SUPPORT_INDEXED_ISA
    if (isa.nonpointer) {
        uintptr_t slot = isa.indexcls;
        return classForIndex((unsigned)slot);
    }
    return (Class)isa.bits;
#else
    return (Class)(isa.bits & ISA_MASK);
#endif
}

最终返回的是 (Class)(isa.bits & ISA_MASK)
__x86_64__# define ISA_MASK 0x00007ffffffffff8ULL

知道计算规则后,咱们验证下

验证isa指向class.png
(lldb) x/4gx p
0x10075eab0: 0x001d800100003445 0x0000000000000000
0x10075eac0: 0x0000000000000000 0x0000000000000000
(lldb) p tp
(id) $1 = 0x0000000100003440
(lldb) p 0x001d800100003445 & 0x00007ffffffffff8ULL
(unsigned long long) $2 = 4294980672
(lldb) p (Class)(0x001d800100003445 & 0x00007ffffffffff8ULL)
(Class) $3 = LGPerson
(lldb) p/x 4294980672
(long) $4 = 0x0000000100003440

总结

  1. 先打印p的内存,取前8字节,也就是 isa
  2. 打印 tp,是p实例的Class对象。
  3. isaISA_MASK 相与。得到值用 p/x 16进制打印,验证和 tp 一样。
  4. p (Class)(0x001d800100003445 & 0x00007ffffffffff8ULL) 强转类型输出也是LGPerson类。
  5. 对应 newisa.shiftcls 赋值时右移三位,0x00007ffffffffff8 (__x86_64__) 转成2进制,可发现 0~247~63 位都是0,中间 3~46 的44位为1,所以取的就是 shiftcls
  6. 可验证上面所述

参考

C语言中文版:https://wiki.jikexueyuan.com/project/c/c-bit-fields.html

相关文章

网友评论

    本文标题:iOS底层探索之isa

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