iOS底层探索之对象的本质和类的关联特性initIsa(下)

442 阅读9分钟

在开始阅读本篇博客之前,建议先去看看我的上一篇博客iOS底层探索之对象的本质和类的关联特性initIsa(上) 本篇内容主要讲下OC当中类的关联相关的initIsa

1.initIsa结构

从苹果开源的objc底层源码可以看到OC底层是通过initIsacls类进行关联的

//关联对象
    if (!zone && fast) {
        obj->initInstanceIsa(cls, hasCxxDtor);
    } else {
        // Use raw pointer isa on the assumption that they might be
        // doing something weird with the zone or RR.
        obj->initIsa(cls);
    }

command+鼠标左键进入obj->initIsa(cls)

inline void 
objc_object::initIsa(Class cls)
{
    initIsa(cls, false, false);
}

在进入initIsa(cls, false, false) initIsa

isa_t

进入isa_t isa_t 我们发现isa_t是一个联合体(union),为了能更好的深入的探索下去,我们得先来了解下联合体相关的知识。

2.联合体

我们在C语言中经常看到union,这就是联合体,也叫共用体是一种特殊的数据类型,允许你在相同的内存位置存储不同的数据类型。你可以定义一个带有多成员的共用体,但是任何时候只能有一个成员带有值。共用体提供了一种使用相同的内存位置的有效方式。

定义联合体

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

union [union tag]
{
   member definition;
   member definition;
   ...
   member definition;
} [one or more union variables];

union tag 是可选的,每个member definition是标准的变量定义,比如 int i;或者 float f;或者其他有效的变量定义。在共用体定义的末尾,最后一个分号之前,可以指定一个或多个共用体变量,这是可选的。

联合体举例

下面定义一个名为Data的共用体类型,有三个成员 i、f 和 str

union Data
{
   int i;
   float f;
   char  str[20];
};
 
int main( )
{
   union Data data;        
   data.i = 10;
   data.f = 220.5;
   strcpy( data.str, "C Programming");
   printf( "data.i : %d\n", data.i);
   printf( "data.f : %f\n", data.f);
   printf( "data.str : %s\n", data.str);
 
   return 0;
}

代码运行输出结果 代码运行结果 我们可以看到共用体if成员的值有损坏,因为最后赋给变量的值占用了内存位置,这也是 str 成员能够完好输出的原因。现在让我们再来看一个相同的实例,这次我们在同一时间只使用一个变量,这也演示了使用共用体的主要目的:

   union Data data;        

   data.i = 10;
   printf( "data.i : %d\n", data.i);
   
   data.f = 220.5;
   printf( "data.f : %f\n", data.f);
   
   strcpy( data.str, "C Programming");
   printf( "data.str : %s\n", data.str);

代码运行结果 代码运行结果

在这里,所有的成员都能完好输出,因为同一时间只用到一个成员。

对比结构体

struct Student{
	NSString *name;
	int age;
} Student;

int main( )
{
   struct Student stu;
   stu.name = @"RENO";
   stu.age = 18;
   return 0;
}

代码运行结果 代码运行结果

从代码运行的结果我们看到,结构体的所有的成员赋值都能正常输出

小结

  1. 结构体(struct)中所有变量是“共存”的
    • 优点:是海纳百川“有容乃⼤”
    • 缺点:是内存空间的分配是粗放的,不管你⽤不⽤,我系统都全给你分配。
  2. 联合体(union)中是各变量是“互斥”的,有你没我,有我没你
    • 缺点:就是不够“包容”
    • 优点:是内存使⽤更为精细灵活,也节省了内存空间

3.位域

上面👆介绍了联合体,现在我们再来了解下位域的知识,嘿嘿😋提前透露下,待会探索底层需要用到位域

什么是位域

C语言允许在一个结构体中以位为单位来指定其成员所占内存长度,这种以位为单位的成员称为“位段”或称“位域”( bit field) 。利用位段能够用较少的位数存储数据。

光看这文字概念,也不太能理解啊!能不能给举个栗子🌰啊!好,那么接下来我就举个栗子🌰 举个🌰

位域举例

/* 定义简单的结构体 */
struct status1{
  unsigned int widthValidated;
  unsigned int heightValidated;
};

/* 定义位域结构 */
struct status2{
  unsigned int widthValidated : 1;
  unsigned int heightValidated : 1;
};
printf( "Memory size occupied by status1 : %lu\n", sizeof(status1));
printf( "Memory size occupied by status2 : %lu\n", sizeof(status2));

当上面的代码被编译和执行时,它会产生下列结果:

代码运行结果

从运行结果我们可以得知: status1结构体占用了8 字节的内存空间,而使用位域status 2只占用 4 个字节的内存空间(冒号:后面加数字表示使用多少位),但是只有 2 位被用来存储值。如果用了 32 个变量,每一个变量宽度为 1 位,那么 status 2结构将使用 4 个字节,但只要再多用一个变量,使用了33个变量的话,那么它将分配内存的下一段来存储第 33 个变量,这个时候就开始使用 8 个字节了。

上面的举例我们只是为了记录widthheight 是否生效,其实只有TRUE/FALSE 两种情况,使用结构体的话就需要8字节64位来存储,这就大大浪费了,有点大材小用了。而使用位域,只需要2位4字节存储。

有些信息在存储时,并不需要占用一个完整的字节,而只需占几个或一个二进制位。例如在存放一个开关量时,只有0 1 两种状态,用 1二进位即可。为了节省存储空间,并使处理简便,所以C 语言提供了"位域""位段"这种数据结构。 "位域"是把一个字节中的二进位划分为几个不同的区域,并说明每个区域的位数。每个域有一个域名,允许在程序中按域名进行操作。这样就可以把几个不同的对象用一个字节的二进制位域来表示。

对于位域的定义尚有以下几点说明:

  • 一个位域存储在同一个字节中,如一个字节所剩空间不够存放另一位域时,则会从下一单元起存放该位域。也可以有意使某位域从下一单元开始。例如:
struct bs{
    unsigned a:4;
    unsigned  :4;    /* 空域 */
    unsigned b:4;    /* 从下一单元开始存放 */
    unsigned c:4
}

在这个位域定义中,a 占第一字节的 4 位,后 4 位填 0 表示不使用,b 从第二字节开始,占用 4 位,c 占用 4 位。

  • 位域的宽度不能超过它所依附的数据类型的长度,成员变量都是有类型的,这个类型限制了成员变量的最大长度: 后面的数字不能超过这个长度。
  • 位域可以是无名位域,这时它只用来作填充或调整位置。无名的位域是不能使用的。例如:
struct k{
    int a:1;
    int  :2;    /* 该 2 位不能使用 */
    int b:3;
    int c:2;
};

从以上分析可以看出,位域本质 上就是一种结构体类型,不过其成员是按二进位分配的。

位域允许用各种格式输出。

int main(){
    struct bs{
        unsigned a:1;
        unsigned b:3;
        unsigned c:4;
    } bit,*pbit;
    bit.a=1;    /* 给位域赋值(应注意赋值不能超过该位域的允许范围) */
    bit.b=7;    /* 给位域赋值(应注意赋值不能超过该位域的允许范围) */
    bit.c=15;    /* 给位域赋值(应注意赋值不能超过该位域的允许范围) */
    printf("%d,%d,%d\n",bit.a,bit.b,bit.c);    /* 以整型量格式输出三个域的内容 */
    pbit=&bit;    /* 把位域变量 bit 的地址送给指针变量 pbit */
    pbit->a=0;    /* 用指针方式给位域 a 重新赋值,赋为 0 */
    pbit->b&=3;    /* 使用了复合的位运算符 "&=",相当于:pbit->b=pbit->b&3,位域 b 中原有值为 7,与 3 作按位与运算的结果为 3(111&011=011,十进制值为 3) */
    pbit->c|=1;    /* 使用了复合位运算符"|=",相当于:pbit->c=pbit->c|1,其结果为 15 */
    printf("%d,%d,%d\n",pbit->a,pbit->b,pbit->c);    /* 用指针方式输出了这三个域的值 */
}

上例程序中定义了位域结构 bs,三个位域为 a、b、c。说明了 bs 类型的变量 bit 和指向 bs 类型的指针变量 pbit。这表示位域也是可以使用指针的。

4.initIsa分析

在上面我们已经知道了,isa_t是一个联合体,里面除了有isa_t()构造方法,还有uintptr_t类型的bits,还有我们的对象cls,最最重要的是还有一个结构体成员变量ISA_BITFIELD

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);
};

这个ISA_BITFIELD不就是isa位域吗?我们在深入进去看看,到底是个什么东东???

ISA_BITFIELD

command+鼠标左键点击,进去找到了ISA_BITFIELD

#   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 unused            : 1;                                         \
      uintptr_t has_sidetable_rc  : 1;                                         \
      uintptr_t extra_rc          : 8

好家伙?直呼“好家伙”啊! 在这里插入图片描述 我的天呢?这内有乾坤啊!这是一个宏定义,分为__arm64____x86_64__两种,其中__arm64__的情况包括模拟器。 完整定义如下

# if __arm64__
// ARM64 simulators have a larger address space, so use the ARM64e
// scheme even when simulators build for ARM64-not-e.
#   if __has_feature(ptrauth_calls) || TARGET_OS_SIMULATOR
#     define ISA_MASK        0x007ffffffffffff8ULL
#     define ISA_MAGIC_MASK  0x0000000000000001ULL
#     define ISA_MAGIC_VALUE 0x0000000000000001ULL
#     define ISA_HAS_CXX_DTOR_BIT 0
#     define ISA_BITFIELD                                                      \
        uintptr_t nonpointer        : 1;                                       \
        uintptr_t has_assoc         : 1;                                       \
        uintptr_t weakly_referenced : 1;                                       \
        uintptr_t shiftcls_and_sig  : 52;                                      \
        uintptr_t has_sidetable_rc  : 1;                                       \
        uintptr_t extra_rc          : 8
#     define RC_ONE   (1ULL<<56)
#     define RC_HALF  (1ULL<<7)
#   else
#     define ISA_MASK        0x0000000ffffffff8ULL
#     define ISA_MAGIC_MASK  0x000003f000000001ULL
#     define ISA_MAGIC_VALUE 0x000001a000000001ULL
#     define ISA_HAS_CXX_DTOR_BIT 1
#     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 unused            : 1;                                       \
        uintptr_t has_sidetable_rc  : 1;                                       \
        uintptr_t extra_rc          : 19
#     define RC_ONE   (1ULL<<45)
#     define RC_HALF  (1ULL<<18)
#   endif

# elif __x86_64__
#   define ISA_MASK        0x00007ffffffffff8ULL
#   define ISA_MAGIC_MASK  0x001f800000000001ULL
#   define ISA_MAGIC_VALUE 0x001d800000000001ULL
#   define ISA_HAS_CXX_DTOR_BIT 1
#   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 unused            : 1;                                         \
      uintptr_t has_sidetable_rc  : 1;                                         \
      uintptr_t extra_rc          : 8
#   define RC_ONE   (1ULL<<56)
#   define RC_HALF  (1ULL<<7)

进都进来了,你就给我看这个啊?这个uintptr_t nonpointeruintptr_t has_assoc奇奇怪怪的是个什么玩意啊??? 在这里插入图片描述 那就耐着性子,听我一一道来!

NONPOINTER_ISA

  1. nonpointer0位,表示是否对isa指针开启指针优化0:isa指针,1:不止是类对象地址,isa包含了类信息、对象的引用计数等。
  2. has_assoc1位,表示关联对象标志位,0:没有,1:有。
  3. has_cxx_dtor2位,表示该对象是否有C++或者Objc的析构器,如果有析构函数,则需要做析构逻辑,如果没有,则可以更快的释放对象。
  4. shiftclsx86架构中占用3~46位,表示存储类指针的值。开启指针优化的情况下,在arm64架构中占用3~35位。
  5. magicx86架构中占用47~52位,在arm64架构中占用36~41位,用于调式器判断当前对象是真的对象还是没有初始化的空间。
  6. weakly_referencedx86架构中占用53位,在arm64架构中占用42位,标志对象是否被指向或者曾经指向一个ARC的弱变量,没有弱引用的对象可以更快释放。
  7. unusedx86架构中占用54位,在arm64架构中占用43位,标志对象是否正在释放内存。
  8. has_sidetable_rcx86架构中占用55位,在arm64架构中占用44位,表示当对象引用计数大于10时,则需要借用该变量存储进位
  9. extra_rcx86架构中占用56~63位,在arm64架构中占用45~63位,当表示该对象的引用计数值时,实际上是引用计数值减1,例如:如果对象的引用计数为10,那么extra_rc9,如果引用计数大于10,则需要使用到has_sidetable_rc

为了更直观的理解ISA_BITFIELD 请看下图:

ISA_BITFIELD内存分布 由上面得知OC底层通过运用联合体位域,大大优化了ISA,这就是著名的NONPOINTER_ISA,这样就充分利用了内存的使用。

isa关联对象过程还原

先看下面这个代码,在NSLog处打上断点

 JPStudent *stu = [JPStudent alloc];
 NSLog(@"%@",stu);

当代码执行到NSLog,控制台lldb调试

(lldb) x/4gx stu
0x10070d140: 0x011d8001000080e9 0x0000000000000000
0x10070d150: 0x0000000000000000 0xd4cbf20b4a85bce1

我们看到stu对象的指针地址是0x011d8001000080e9(isa),最高位是0,说明64位并没有使用满。 我们都知道对象是通过指针地址关联到类的,那么我们看看类的地址

(lldb) p/x JPStudent.class
(Class) $1 = 0x00000001000080e8 JPStudent

从控制台看到,类的地址占用位数更少了,那到底对象和类是怎么关联的呢???

ISA_MASK

在上面的源码中我们看到有一个ISA_MASK的宏定义,ISA_MASK = 0x007ffffffffffff8ULL

#     define ISA_MASK        0x007ffffffffffff8ULL

这个ISA_MASK,就是ISA面具,和网络中的子网掩码差不多,单独存在没有什么实际的意义。

比如我想露出眼睛👀或者鼻子👃,那么我就需要遮住脸部的其他地方,把需要露出来的地方露出来,那我戴上一个面具🎭就可以达到这种目的。

那么我们现在知道了ISA0x011d8001000080e9ISA_MASK 0x007ffffffffffff8ULL让它两个作与(&)操作,看看结果 在这里插入图片描述 还有谁???看到没有?得到的结果是一模模一样样!

对象stu通过ISA得到(cls),因为ISA是不纯的,里面还包含了其他信息,所以必须通过掩码(ISA_MASK),得到类的信息。

ISA位运算

除了上面的通过掩码得到,也可以通过运算得到,因为我们只要找到shiftcls就可以,在前面的介绍中我们已经知道shiftcls是占中间的44位。

具体操作如下

(lldb) x/4gx stu
0x10070d140: 0x011d8001000080e9 0x0000000000000000
0x10070d150: 0x0000000000000000 0xd4cbf20b4a85bce1
(lldb) p/x 0x011d8001000080e9 >> 3
(long) $6 = 0x0023b0002000101d
(lldb) p/x 0x0023b0002000101d << 20
(long) $7 = 0x0002000101d00000
(lldb) p/x 0x0002000101d00000 >> 17
(long) $8 = 0x00000001000080e8
(lldb) p/x JPStudent.class
(Class) $9 = 0x00000001000080e8 JPStudent
(lldb) 

具体操作请看下面的图 isa位移过程

🌹请收藏+关注,评论 + 转发,以免你下次找不到我,哈哈😁🌹

🌹欢迎大家留言交流,批评指正,互相学习😁,提升自我🌹