iOS底层-包罗万象的isa

2,654 阅读7分钟
 欢迎阅读iOS底层系列(建议按顺序)
iOS底层 - alloc和init探索
iOS底层 - 包罗万象的isa
iOS底层 - 类的本质分析
iOS底层 - cache_t流程分析
iOS底层 - 方法的本质和查找流程分析

1.本文概述

  • 本文主要从底层分析isa的作用,isa的数据结构,isa内各个位置内实际存储的内容,以及isa在是否是nonpointer下的区别
  • isa的走位图,SuperClass的指向

备注:isa是串联对象和类的重要线索,了解isa,能对对象的本质,类方法的走向等有更深刻的理解


2.isa的作用

        上篇说到,alloc在开辟空间后也初始化了isa,从而把对象和类关联起来。所以对于对象来说,isa的基础作用就是和类进行绑定,告诉系统对象的归属。但是大部分nonpointerisa不仅仅只是做指向,其内部还存储了大量的信息。

这里引入了一个nonpointer的概念,简单说明下:

早期调用isa可以直接返回类,后来苹果为了优化内存,使其内部增加了及其丰富的信息,并且增加了isa_mask,不让直接获取类。有优化的就是提到的nonpointer,也是本文研究的重点。

从初始化isa的源码,来验证下这个说法

objc_object::initIsa(Class cls, bool nonpointer, bool hasCxxDtor) { 
    assert(!isTaggedPointer()); 
    if (!nonpointer) {
        isa.cls = 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;
    }
}

首先断言判断

assert(!isTaggedPointer());

如果是TaggedPointer,后面就不执行,也就没有isa这个概念了。

这里引入了一个TaggedPointer的概念:

        早期64位系统时,当我们存储基础数据类型 , 底层封装成 NSNumber 对象 , 也会占用 8字节内存 , 32位机器占用4字节。为了存储和访问一个 NSNumber 对象,需要在堆上分配内存,另外还要维护它的引用计数,管理它的生命期 。这些都给程序增加了额外的逻辑,造成运行效率上的损失 。因此如果没有额外处理 , 会造成很大空间浪费 .

因此苹果引入了TaggedPointer,当对象为指针为TaggedPointer类型时,指针的值不是地址了,而是真正的值,直接优化了存储,提升了获取速度。

TaggedPointer的特点

  • 专门用来存储小对象,例如NSNumber和部分NSString
  • 指针不在存储地址,而是直接存储对象的值。所以,它不是一个对象,而是一个伪装成对象的普通变量。内存也不在堆,而是在栈,由系统管理,不需要mallocfree
  • 在内存读取上有着3倍的效率,创建时比以前快106倍。(少了malloc流程,获取时直接从地址提取值)

回到源码来,

if (!nonpointer) {isa.cls = cls;}

这里验证了,未开启isa指针优化时,isa直接和类关联,无后续操作;当开启优化时,先初始化了isa_t

isa_t newisa(0);

,然后对内部属性赋值,最后通过shiftcls类关联

newisa.shiftcls = (uintptr_t)cls >> 3;

这可以说明isa_t就是isa真正的结构。


3.isa 的数据结构

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的底层是isa_tisa_t的结构是联合体+位域

  • 联合体的大小取决于内部最大的元素的大小,所以isa的大小为8字节。
  • 联合体内部的的元素在内存中的互相覆盖的,所以cls,bits是不会同时存在的。

回过头看,nonpointer!nonpointer是只能二选一的,苹果就是利用这种互斥关系,把isa的结构定义成联合体。追求内存极致优化的苹果显然不满足于此,在联合体内又增加了位域的结构来使isa包罗万象。

  • 8字节即为64个二进制位。每个二进制位都定义了存储的内容,即为位域。

看下ISA_BITFIELD宏定义的每个二进制位存储的内容(这里采用x86架构下的结构,每种架构都有细微的差别,但所包含的内容是一样的,只是某些内容存储的长度不一致)

#   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

说明下各个存储位代表的意思:

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。

实际演练验证一下,初始化两个对象

CJPerson *object = [CJPerson alloc];
NSLog(@"object = %p", object);        

CJPerson *object1 = [CJPerson alloc];                
objc_setAssociatedObject(object1, @"object1", object1, OBJC_ASSOCIATION_RETAIN_NONATOMIC);        
NSLog(@"object1 = %p", object1);

lldb打印出它们各自的isa内容


可以看出,第一位是一样的,因为都是nonpointer , 唯一的区别就是在第二位关联对象标志位。其他位置同理,有个比较特殊的就是shiftcls,上文提到,早期isa可以直接获取类,现在需要一个isa_mask来间接获取。

看下object_getClass底层,有一段这样的代码,也是通过isa_mask来获取isa的指向

return (Class)(isa.bits & ISA_MASK);

x86中的isa_mask

#   define ISA_MASK        0x00007ffffffffff8ULL

转为二进制

0000 0000 0000 0000 0111 1111 1111 1111
1111 1111 1111 1111 1111 1111 1111 1000

这就很明显了,从第3位开始,后面的44位存储着shiftcls的信息,刚好和上面给出的x86isa的存储内容架构吻合。直接将对象的  isa & isa_mask之后,就会得到对象的内存地址,也就是isa的指向。


3.isa的走位图

这是苹果官方给出的isasuperclass的走位图。



举个例子验证下,用上面CJPerson实例出来的对象lldb打印下isa的走位


x/4gx打印的是对象在内存中从首地址开始,连续存储的4个8字节的内容地址,x/5gxx/6gx依此类推。
p/tp/op/dp/x分别代表二进制、八进制、十进制和十六进制打印 。


因为isa是对象中的第一个元素,所以x/4gx打印出来的第一个地址就是isa,在用isa&isa_mask就得到isa的指向,依次类推



在分别po打印下地址



得出结论:

  • 对象的isa指向类。
  • 类的isa指向和类同名的元类,但是地址不同。
  • 元类的isa指向根元类。
  • 根元类的isa指向自己。


总结:
isa是串联对象,类,元类和根元类的重要线索,采用联合体加位域的数据结构使有限的空间充分利用,存储了丰富的信息


以上就是关于isa的探索,后续继续更新类的底层结构,方法转发,block,锁,多线程等底层探索,还有应用程序加载,启动优化,内存优化等相关知识点,敬请关注。