看透 isa

896 阅读11分钟

前言

在写这篇博客之前,我在想要从哪里切入,才能让iOS开发者能更通俗的理解 isa。思来想去,我觉得还是从我们最熟悉的“对象”入手吧。

在Foundation层,创建对象的代码是这样的

Person *p = [[Person alloc] init];

那么你有没有想过这样一个问题?我们自定义了一个Person类,没有任何属性和方法,为什么我们可以调用 alloc init 呢 ?或许你可以脱口而出,因为Person类继承自NSObject,NSObject里有默认的实现

+ (id)alloc {
    return _objc_rootAlloc(self);
}

那为什么继承自NSOject的类就可以调用NSObject的方法呢?是不是这中间两者通过某些线索进行了关联呢?带着这个疑问我们往下看。

初识 isa

对象的本质是 结构体,这很好理解,因为OC 是 C 与 C++ 的超集。一个对象可以有多种不同数据类型的属性,那可以容纳不同数据类型的复杂结构,当然是**结构体**了。我们通过查看苹果的源码也可以佐证这一说法。

/// Represents an instance of a class.
struct objc_object {
    Class _Nonnull isa  OBJC_ISA_AVAILABILITY;
};

/// A pointer to an instance of a class.
typedef struct objc_object *id;

我们看到,对象就是这样一个结构体,且被typedef为 id 。在Foundation层 id 就表示一个对象 。

同时我们注意到在对象结构体内,有一个 Class 类型的 isa 变量,看变量类型这是一个类。对象内有一个类 ?这听起来有些奇怪;对象内有一个指向该对象类型的指针 ?这似乎还蛮符合我们以往的认知:在面向对象编程中,对象是由类创建的,对象可以通过 isa 变量找到自己所属的类。

那为什么对象需要知道自己的类呢?这主要是因为对象的信息是存储在该对象所属的类中的。

这也很容易理解,一个类可以有多个对象,如果每个对象的信息都存储在各自的本身,那随着对象的不断创建,对于内存来说是灾难级的。

既然对象的 isa 指针指向了类,那不妨也看看类的结构:

struct objc_class {
    Class _Nonnull isa  OBJC_ISA_AVAILABILITY;
    Class _Nullable super_class                             
    .......
    .......
}

类里面有个 super_class ,指向了类的父类; 同时类也有一个 isa 指针,那类的 isa 指针指向了哪里呢?

对象是按照 所定义的各个属性和方法“生产”的, 作为对象的模板,也可看成是对象。正如工厂里面的模子也是要专门制作模子的机器生产。元类 (meta class) 就是设计、管理类(class)的模板。对象是 的实例,类是 元类 的实例。

所以类的 isa 指针指向了 元类

按照这个规则,那 元类 也是对象,元类对象中也有 isa,那么元类的 isa 又指向哪里呢?总不能指向元元类吧……这样是无穷无尽的。

Objective-C语言的设计者已经考虑到了这个问题,所有元类的 isa 都指向 根元类(meta Root Class)。关于实例对象、类、元类之间的关系,苹果官方给了一张图,非常清晰的表明了三者的关系。

isa流程图

实线是 super_class 指针,虚线是 isa 指针。

    1. Root class(class) 通常是 NSObject,NSObject 是没有超类的,所以 Root class(class)的 superclass 指向 nil。
    1. 每个 Class 都有一个 isa 指针指向唯一的 Meta class
    1. Root class(meta)的 superclass 指向 Root class(class),也就是 NSObject,形成一个回路。
    1. 每个 Meta class 的 isa 指针都指向 Root class(meta)。

一个对象 可以通过 isa 找到类,根据类的 isasuper_class 找到 元类 与 父类 ,进而直到 根元类 和 根类 ,所以 对于最开始的例子 Person *p = [[Person alloc] init]; Person可以调用NSObject的方法,在这中间 isa 起到至关重要的作用。

小结:

Object-C 是基于类的对象系统。每一个对象都是一些类的实例;这个对象的 isa 指针指向它所属的类。

  • 该类描述这个 对象的数据信息 :内存分配大小(allocation size)和实例变量的类型(ivar types )与布局(layout);
  • 也描述了 对象的行为 :它能够响应的选择器(selectors)和它实现的实例方法(instance methods)。

每个 Object-C 类也是一个对象,它的 isa 指针指向元类,元类是关于类对象的描述,就像类是普通实例对象的描述一样。

一个元类是根元类的实例;根元类是它自身的实例。

isa 指针链以一个环结束:实例指向类-指向元类-指向根元类-到自身。元类的 isa 指针并不重要,因为在现实世界中,没人会向元类对象发送消息。

总之, isa 很棒~ 很重要~

isa的优化

随着Apple公司的发展,iPhone 不断更新迭代,技术不断提升,底层源码也是在不断优化的。64位架构CPU问世,Apple更新优化了许多地方,其中就包括 isa 的结构。

/// Represents an instance of a class.
struct objc_object {

private:
    isa_t isa;

     ..........太多  以下省略
     ..........
}
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
}

在 objc2.0 中,所有的对象都会包含一个 isa_t 类型的结构体。同时,因为 objc_class 继承自 objc_object,所以所有的类也包含这样一个 isa。在优化之前,isa 只是一个指向类或元类的指针,而优化之后,采取了联合体结构,同样是占用8字节空间,但存储了更多的内容。

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.h 中,这样做的目的是为了区分不同架构

isa_t中的struct

深入 isa

我们以 arm64 架构为例,则 isa_t 可以表示成如下所示的代码

(以下内容探讨如不特殊说明,默认均是以 arm64 架构为例)

#   define ISA_MASK        0x0000000ffffffff8ULL
#   define ISA_MAGIC_MASK  0x000003f000000001ULL
#   define ISA_MAGIC_VALUE 0x000001a000000001ULL

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

    Class cls;
    uintptr_t bits;
    
    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
     }
};

isa_t 是一个联合体,这里所占空间为 8字节,共64位 ,内存布局从低位到高位情况如下图

isa_t内存布局情况

解释一下各存储内容的含义:

  • nonpointer(存储在第0字节) : 是否为优化isa标志。0代表是优化前的isa,一个纯指向类或元类的指针;1表示优化后的isa,不止是一个指针,isa中包含类信息、对象的引用计数等。现在基本上都是优化后的isa。

  • has_assoc (存储在第1个字节): 关联对象标志位。对象含有或者曾经含有关联引用,0表示没有,1表示有,没有关联引用的可以更快地释放内存(dealloc的底层代码有体现)。

  • has_cxx_dtor(存储在第2个字节): 析构函数标志位,如果有析构函数,则需进行析构逻辑,如果没有,则可以更快速地释放对象(dealloc的底层代码有体现)。

  • shiftcls :(存储在第3-35字节)存储类的指针,其实就是优化之前 isa 指向的内容。在arm64架构中有33位用来存储类指针。x86_64架构有44位。

  • magic(存储在第36-41字节):判断对象是否初始化完成, 是调试器判断当前对象是真的对象还是没有初始化的空间。

  • weakly_referenced(存储在第42字节):对象被指向或者曾经指向一个 ARC 的弱变量,没有弱引用的对象可以更快释放(dealloc的底层代码有体现)。

  • deallocating(存储在第43字节):标志对象是否正在释放内存。

  • has_sidetable_rc(存储在第44字节):判断该对象的引用计数是否过大,如果过大则需要其他散列表来进行存储。

  • extra_rc(存储在第45-63字节。):存放该对象的引用计数值减1后的结果。对象的引用计数超过 1,会存在这个里面,如果引用计数为 10,extra_rc 的值就为 9。

如上,优化之后的 isa,保留了优化之前类的指针(shiftcls),所以依然可以通过isa找到对应的类,在类中通过super_class找到父类,这对于 isa 的指向图的部分是一样子。同时还包含了更多其他的内容,这个设计和 taggedpointer 有些类似,把内存用到极致。

接下来我们做一些有趣的事情:

定义一个继承自 NSObject 的类 Person,不添加任何属性与方法等,保证它是刚刚创建出来的样子。

Person *p = [[Person alloc] init];

以16进制格式化打印4段内存情况

(lldb) x/4gx p
0x10201f950: 0x001d8001000024dd 0x0000000000000000
0x10201f960: 0x0000000000000000 0x0000000000000000
(lldb) 

因为Person继承自NSObject,默认有一个 isa,所以 0x001d8001000024dd 就是 isa_t 结构 ,我们将这个值 右移3位,左移31位,再右移28位,看看得到什么?

(lldb) x/4gx p
0x10201f950: 0x001d8001000024dd 0x0000000000000000
0x10201f960: 0x0000000000000000 0x0000000000000000
(lldb) po 0x001d8001000024dd >> 3
1037939513492635

(lldb) po 1037939513492635 << 30
562951189692416

(lldb) po 562951189692416 >> 27
Person

(lldb) 

最终结果显示是拿到了类信息,我们来画图分析一下这个过程,用蓝色表示内存中被保留的值,灰色表示内存中被抹除的值

    1. 起始时,完整内存的值均保留

起始时isa_t内存占满.png

    1. 右移3位

内存整体右移3位,那么高3位将空缺,低3位被移出isa_t内存边界(用透明度表示),所以相当于抹除。

右移3位

我们只关注isa_t结构内的内存分布,不考虑边界内存的影响,简化绘图为:

  • 3.左移31位

低31位被抹除

左移31位

  • 4.右移28位

右移28位

最终内存中被保留的内容 仅剩第3到第35字节的,对应前面所讲的 isa_t 内存布局情况,刚好是 shiftcls 的数据信息。所以我们上面的操作可以取到Person类信息。

我们再看一下apple的开发人员是怎么取类的信息的呢?

inline Class 
objc_object::ISA() 
{
 
    return (Class)(isa.bits & ISA_MASK);
}

通过 isa中的 bits & ISA_MASK

看看 ISA_MASK 是什么?

#   define ISA_MASK        0x0000000ffffffff8ULL

将它转换成2进制

ISA_MASK的二进制形式

从低位3开始到35位为1,其他位均为0。所以 & ISA_MASK 就相当于保留第3-35位数据,抹除其他位数据。依然是取 shiftcls

isa 的初始化

了解了isa的结构,我们来看一下isa的初始化(去除一些宏定义,断言以及条件判断等,我们直接将代码减少到它执行的代码)

inline void 
objc_object::initIsa(Class cls, bool nonpointer, bool hasCxxDtor) 
{ 
        isa_t newisa(0);

        newisa.bits = ISA_MAGIC_VALUE;
        // isa.magic is part of ISA_MAGIC_VALUE
        // isa.nonpointer is part of ISA_MAGIC_VALUE
        newisa.has_cxx_dtor = hasCxxDtor;
        newisa.shiftcls = (uintptr_t)cls >> 3;

        isa = newisa;
    }
}

  1. 首先对整个bits进行赋值,传入 ISA_MAGIC_VALUE ,在arm64架构下,该值为
#   define ISA_MAGIC_VALUE 0x000001a000000001ULL

将该值转换为2进制

ISA_MAGIC_VALUE转换为2进制

对应 isa_t 中内存布局的位置,可以看出对 bits 赋值 就是对 nonpinter 和 magic 赋值的过程。

  1. 其次对has_cxx_dtor赋值。

  2. 最后对shifcls赋值

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

这里 ,对当前传入类进行右移3位的原因是,将cls指针后三位清除以减小内存消耗,因为指针是要按照8字节对齐的,实际后三位是没有意义的。这和 isa_t 中的内存布局没有关系,因为类可不是按照isa_t 进行内存布局的。

至此isa的赋值过程就完成了。

总结

对于 isa ,我们了解了底层原理,对其作用以及相关操作,我们会更加清晰。当然,在这里我们也要学习Apple的设计模式,试着站在开发人员的角度考虑它的设计思想。

然后你一定要熟记 isasuper_class 的指向流程,这真的很重要。

最后,希望在此时或者以后的某一天,你可以大胆的对它说:isa,我看透你了!