看透isa

2020-09-11  本文已影响0人  卖馍工程师

前言

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

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

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

那么你有没有想过这样一个问题?我们自定义了一个Person类,没有任何属性和方法,为什么我们可以调用 allocinit 呢 ?或许你可以脱口而出,因为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 指针。

一个对象 可以通过 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内存布局情况

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

如上,优化之后的 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) 

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

起始时isa_t内存占满.png

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

右移3位

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

低31位被抹除

左移31位 右移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,我看透你了!

上一篇 下一篇

猜你喜欢

热点阅读