阅读 1213

iOS 底层 - isa 的前世今生

1、前言概述

  • 本篇文章首先讲述 isa 的作用 , 实际数据结构 , 其中不同二进制位存储内容说明 , 包括 isa 优化 , 是否为 TaggedPoint .
  • 然后以引用计数为例实际探索 .
  • 最后讲述 isa 的指向 , 以及 SuperClass 的指向探索 .
  • 其中穿插了一些面试题以及涉及到的知识点 .

isa 是我们能把底层知识点串联起来最为关键的一条引线 . 通过本篇文章探索 , 对于对象的本质有更深层次的理解 .

2、isa 指针

① 概述 - 类与对象

Objective-C 是一门面向对象的编程语言。每个对象都是其 的实例 , 被称为实例对象 . 每一个对象都有一个名为 isa 的指针,指向该对象的类。

新建 Command Line 工程 , 新建一个类 LBObject , 写一个属性一个成员变量 , clang 编译.

clang -rewrite-objc main.m -o main.cpp
复制代码

打开 main.cpp . 我们看到如下 :

typedef struct objc_object LBPerson;
typedef struct {} _objc_exc_LBPerson;
#endif

extern "C" unsigned long OBJC_IVAR_$_LBPerson$_name;
struct LBPerson_IMPL {
    struct NSObject_IMPL NSObject_IVARS;
    NSString *name;
    NSString *_name;
};

struct NSObject_IMPL {
    Class isa;
};
复制代码

可以看到 , 类其实就是一个包含 isa 指针的结构体 .

来看下 NSObject 源码 :

@interface NSObject <NSObject> {
#pragma clang diagnostic push
#pragma clang diagnostic ignored "-Wobjc-interface-ivars"
    Class isa  OBJC_ISA_AVAILABILITY;
#pragma clang diagnostic pop
}
复制代码

从此可以得知 , 类也是一个对象 , 我们称之为类对象 .

源码如下 :

typedef struct objc_class *Class;

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

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
    /**/
}


/// Represents an instance of a class.
struct objc_object {
private:
    isa_t isa;
public:
    // ISA() assumes this is NOT a tagged pointer object
    Class ISA();

    // getIsa() allows this to be a tagged pointer object
    Class getIsa();
    /*...*/
}
复制代码

是不是有点傻傻分不清了 . 梳理一下 :

其指示关系如下图 .

图片引用自 : iOS 内存管理

在最初的时候 , isa 其实就是一个指针 , 起到指向的作用 , 将对象 , 类 , 以及元类连接起来 , 后来苹果针对其进行了优化 , 采用 联合体 + 位域 的方式来节省内存 与存储更多内容 .

② isa 探索

先来看下 对象的 getIsa 方法 :

#if SUPPORT_TAGGED_POINTERS
inline Class objc_object::getIsa() 
{
    if (!isTaggedPointer()) return ISA();

    uintptr_t ptr = (uintptr_t)this;
    if (isExtTaggedPointer()) {
        uintptr_t slot = 
            (ptr >> _OBJC_TAG_EXT_SLOT_SHIFT) & _OBJC_TAG_EXT_SLOT_MASK;
        return objc_tag_ext_classes[slot];
    } else {
        uintptr_t slot = 
            (ptr >> _OBJC_TAG_SLOT_SHIFT) & _OBJC_TAG_SLOT_MASK;
        return objc_tag_classes[slot];
    }
}
复制代码

这里引出一个概念 , TaggedPointer .

TaggedPointer

参考自 唐巧 - 深入理解 Tagged Pointer

  • 在开始使用 64 位机器也就是 iPhone 5S 时 , 指针对象占用 8 字节内存 .
  • 也就是说当我们存储基础数据类型 , 底层封装成 NSNumber 对象 , 也会占用 8 字节内存 , 而 32 位机器下占用 4 字节 .
  • 因此如果没有额外处理 , 在迁移到 64 位机器下时 , 会造成很大空间浪费 .

因此 , 为了节省内存和提高执行效率,苹果提出了 Tagged Pointer 的概念。对于 64 位程序,引入 Tagged Pointer 后,相关逻辑能减少一半的内存占用,以及 3 倍的访问速度提升,100 倍的创建、销毁速度提升。

未引入 Tagged Pointer

为了存储和访问一个 NSNumber 对象,我们需要在堆上为其分配内存,另外还要维护它的引用计数,管理它的生命期 。这些都给程序增加了额外的逻辑,造成运行效率上的损失 。

引入 Tagged Pointer

由于 NSNumberNSDate 一类的变量本身的值需要占用的内存大小常常不需要 8 个字节,拿整数来说,4 个字节所能表示的有符号整数就可以达到 20 多亿 (注:2^31=2147483648,另外 1 位作为符号位),对于绝大多数情况都是可以处理的。

因此苹果将一个对象的指针拆成两部分,一部分直接保存数据,另一部分作为特殊标记,表示这是一个特别的指针,不指向任何一个地址。所以,引入了 Tagged Pointer 对象之后 , 对象的指针其实不再是传统意义上的指针 .

Tagged Pointer 作用

Tagged Pointer 特点:

  • 1️⃣ : Tagged Pointer 专门用来存储小的对象,例如 NSNumberNSDate
  • 2️⃣ : Tagged Pointer 指针的值不再是地址了,而是真正的值。所以,实际上它不再是一个对象了,它只是一个披着对象皮的普通变量而已。所以,它的内存并不存储在堆中,也不需要 mallocfree
  • 3️⃣ : 在内存读取上有着 3 倍的效率,创建时比以前快 106 倍 . ( objc_msgSend 能识别 Tagged Pointer,比如 NSNumberintValue 方法,直接从指针提取数据 )
  • 4️⃣ : 使用 Tagged Pointer 后,指针内存储的数据变成了 Tag + Data,也就是将数据直接存储在了指针中 .

那么回到 getIsa 方法中 , 当为对象类型时 , 很明显是非 isTaggedPointer . 直接来到 ISA() ;

#if SUPPORT_NONPOINTER_ISA
inline Class objc_object::ISA() 
{
    assert(!isTaggedPointer()); 
#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
}
复制代码

这里又看到一个 NONPOINTER_ISAINDEXED_ISA . INDEXED_ISA 源码如下 :

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

也就是说 64 位机器下为 1 . 那么我们来说一说 NONPOINTER_ISA .

NONPOINTER_ISA

我们已经知道对象的 isa 指针,是用来表明对象所属的类类型。 但是如果isa指针仅表示类型的话,对内存显然也是一个极大的浪费。

于是,就像 Tagged Pointer 一样,对于 isa 指针,苹果同样进行了优化。isa 指针表示的内容变得更为丰富,除了表明对象属于哪个类之外,还附加了引用计数 extra_rc,是否有被 weak 引用标志位 weakly_referenced,是否有附加对象标志位 has_assoc 等信息 , 使用的就是我们刚刚提到的 联合体 + 位域 的数据结构 .

nonpointer 就是是否进行优化的标识 , 优化之后的联合体中存储了是否优化的标识在其中的 struct 的第一个二进制位中 . ( 下面我们会仔细讲述 ) .

那么接下来 , 终于进入到 isa_t 的结构了 .

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_t

从上源码得知 isa 数据结构其实为 isa_t , 是一个联合体 ( 或者叫共用体 ,union ) .

其中 ISA_BITFIELD 宏定义在不同架构下表示如下 :

# if __arm64__
#   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
# elif __x86_64__
#   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
复制代码

首先看到 isa_t 是一个联合体的数据结构 , 联合体意味着公用内存 , 也就是说 isa 其实总共还是占用 8 个字节内存 , 共 64 个二进制位 .

而上述不同架构的宏定义中定义的位域就是 64 个二进制位中 , 每个位置存储的是什么内容 .

  • 由于联合体的特性 , cls , bits 以及 struct 都是 8 字节内存 , 也就是说他们在内存中是完全重叠的 .
  • 实际上在 runtime 中,任何对 struct 的操作和获取某些值,如 extra_rc,实际上都是通过对 bits 做位运算实现的。
  • bitsstruct 的关系可以看做 : bits 向外提供了操作 struct 的接口,而 struct 本身则说明了 bits 中各个二进制位的定义。

以获取有无关联对象来举例 :

可以直接使用 isa.has_assoc , 也就是点语法直接访问 bits 中第二个二进制位中的数据 . ( arm 64 架构中 )

因此 , bitsstruct 的关系理解清楚以后 , 我们 isa 其实就有两种情况 , cls 或者是 bits , 也就是我们刚刚所提到的 nonPointer_isa 与否 , 两种情况完美验证 . 如下图 :

参照 arm64 架构下 , ISA_BITFIELD . 我们来看看每个字段都存储了什么内容 , 以便更深刻的理解对象的本质 .

#   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
复制代码
-成员- 含义
nonpointer 1bit 标志位 - 1 ( 奇数 )表示开启了isa优化,0 ( 偶数 ) 表示没有启用isa优化
has_assoc 1bit 标志位 - 表明对象是否有关联对象。没有关联对象的对象释放的更快 , 关联对象可以参考 Category底层原理
has_cxx_dtor 1bit 标志位 - 表明对象是否有C++或ARC析构函数。没有析构函数的对象释放的更快 , 参考 OC 对象的创建流程 中有详细叙述对象释放完整流程
shiftcls 33bit 存储类指针的值。开启指针优化的情况下,在 arm64 架构中有 33 位用来存储类指针。
magic 6bit 用于调试器判断当前对象是真的对象还是没有初始化的空间 , 固定为 0x1a
weakly_referenced 1bit 标志位 - 用于表示该对象是否被别ARC对象弱引用或者引用过。没有被弱引用的对象释放的更快
deallocating 1bit 标志位 - 用于表示该对象是否正在被释放
has_sidetable_rc 1bit 标志位 - 用于标识是否当前的引用计数过大 ( 大于 10 ) ,无法在 isa 中存储,则需要借用sidetable来存储
extra_rc 19bit 实际上是对象的引用计数减 1 . 比如,一个 object 对象的引用计数为7,则此时 extra_rc 的值为 6

以上就是 arm64 架构下 isa 每一个位置所存储的内容 , x86 架构下存储数据不变 , 只是占据位有所不同 , 就不重复讲述了 .

那我们接下来以 extra_rc 为例来探索一下其存储和获取的过程 .

isa 实战演练 - 引用计数探索 - extra_rc

首先再来看一下这张图 , 我们如何能拿到 isa_t

我们普通创建一个对象 , 其实就是一个指向这个类的指针 , 例如 :

NSObject * obj = [[NSOibect alloc] init];
复制代码

分析过程 :

  • 1️⃣ : 那么 obj 就是一个 NSObject * 类型 . 而 NSObject 是一个 Class isa , 也就是 objc_class *
  • 2️⃣ : 换句话说 , obj == objc_class ** . 而 objc_class 继承于 objc_object . 也就是说 objc_class 的首地址其实就是 objc_object
  • 3️⃣ : 由于 objc_object 内部就是一个 isa_t . 因此 obj == objc_class ** 可以替换成 obj == objc_class **
  • 4️⃣ : 当 isa 开启优化时 , 也就是说 isa 不再是一个指针 , 而是我们之前讲的联合体 . 因此改为 obj == isa_t *

综上 , 我们得到结论 , obj 就是一个 指向 isa_t 的指针 .

接下来我们来实际演练一遍 .

注意:

  • arm64isa_t 中后 19 位为 extra_rc .
  • extra_rc 实际存储为引用计数减 1 .
  • iOS 小端模式 , 读取二进制位时从右往左读 .
@interface ViewController ()
@end

@implementation ViewController

- (void)viewDidLoad {
    [super viewDidLoad];
    NSObject *obj =[NSObject alloc];
    NSLog(@"isa_t = %p", *(void **)(__bridge void*)obj);
}
复制代码

注意使用真机 , 使用 arm64 架构下的 bits .

  • 运行打印如下 :

实际上 obj 对象此时引用计数为 1 , 预期一致 .


接下来修改代码如下 :

@interface ViewController ()
@property(nonatomic, strong) NSObject *obj1;
@property(nonatomic, strong) NSObject *obj2;
@property(nonatomic, weak) NSObject *weakRefObj;
@end

@implementation ViewController

- (void)viewDidLoad {
    [super viewDidLoad];
    NSObject *obj =[NSObject alloc];
    _obj1 = obj;
    NSObject *tempObj = obj;
    NSLog(@"isa_t = %p", *(void **)(__bridge void*)obj);
}
复制代码
  • 运行打印如下 :

引用计数显示为 2 , 实际为 3 - 1 = 2 . 符合预期 .


最后添加代码如下 :

- (void)viewDidAppear:(BOOL)animated{
    [super viewDidAppear:animated];
    NSLog(@"3. obj isa_t = %p", *(void **)(__bridge void*)_obj1);
    _obj2 = _obj1;
    NSLog(@"4. obj isa_t = %p", *(void **)(__bridge void*)_obj1);
    _weakRefObj = _obj1;
    NSLog(@"5. obj isa_t = %p", *(void **)(__bridge void*)_obj1);
    NSObject *attachObj = [[NSObject alloc] init];
    objc_setAssociatedObject(_obj1, "attachKey", attachObj, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
    NSLog(@"6. obj isa_t = %p", *(void **)(__bridge void*)_obj1);
}
复制代码

分别在 NSLog 断点打印并查看 isa_t 结果如下 .

引用计数 , 弱引用标识 , 关联对象标识 均符合预期 .

最后 , 来看下实际 isa 指向内容 ( 在 isa_t 3 - 36 位中存储为 shiftcls 指针 , 相当于 isa 未优化时的 NONPOINTER_ISA 指针 )

isa 在 lldb 中 , 获取 isa_t 中某一段位置的数据 , 直接通过 宏定义中提供好的 mask 可以快速获取指定位置存储的值 .

#   define ISA_MASK        0x0000000ffffffff8ULL
#   define ISA_MAGIC_MASK  0x000003f000000001ULL
#   define ISA_MAGIC_VALUE 0x000001a000000001ULL
复制代码

通过以上探索 , 我们很清楚 runtimeisa 的具体结构 . 那么接下来 , 我们来探索一下 isa 的指向 , 这个问题也已经是面试常客的地位了 .

在探索 isa 指向之前 , 我们需要知道这个问题 :

③ 面试题 class , objc_getClass 与 object_getclass

classobjc_getClassobject_getclass 方法有什么区别 ?

( 这个问题来自 阿里、字节:一套高效的iOS面试题 这篇文章 , 刚好在写探索 isa 文章 , 就一起来解答一下 ) .

先上源码 .

+ (Class)class {
    return self;
}

- (Class)class {
    return object_getClass(self);
}
复制代码
Class object_getClass(id obj)
{
    if (obj) return obj->getIsa();
    else return Nil;
}

Class objc_getClass(const char *aClassName)
{
    if (!aClassName) return Nil;

    // NO unconnected, YES class handler
    return look_up_class(aClassName, NO, YES);
}
复制代码

getIsa 方法上面粘贴过 , 我们就不沾了 . 其实就是获取 isa 指向 .

1️⃣、 class 方法 .

  • 当调用者为实例对象时 , 返回 isa 指向也就是类对象 ( 也就是 - class ) .
  • 当调用者为类对象时 , 返回自身 ( 也就是 + class ) .

2️⃣、object_getClass方法.

  • object_getClass 其实就是获取 isa 指向 .

    也就是说 当需要获取元类时 , 则需要使用类对象调用 object_getClass .

写法如下 :

void test(){
    NSObject *obj =[NSObject alloc];
    // NSObject类
    Class class = object_getClass(obj);
    // NSObject元类
    Class metaClass = object_getClass(class);
}
复制代码

3️⃣、objc_getClass 方法

  • 这个方法传入参数为字符串 , 其实就是根据字符串获取到这个类对象 .

写法如下 :

Class objcClass = objc_getClass("NSObject");
复制代码

了解了这几个函数以后 , 我们就开始探索 isa 的指向了 .

提示 : ( 在探索前 , 请对 OC类对象/实例对象/元类 有详细了解 )

④ isa 指向探索

isa 走位流程图 , 图片引用自官方文档

代码准备

新建一个 LBSuperClass 类继承于 NSObject , 一个 LBSubClass 继承于 LBSuperClass , 创建代码如下 .

NSObject * object = [NSObject alloc];
LBSuperClass * superClass = [LBSuperClass alloc];
LBSubClass * subClass = [LBSubClass alloc];
复制代码

三个对象创建完加断点 , 运行代码.

提示 :

  • lldb 命令

    • x/4g 代表打印对象首地址开始连续 48 字节内存地址内容 . X/5g , x/6g 以此类推 , 就免去因为小端模式 x 打印必须从右往左读的问题 .

    • p/tp/op/dp/x分别代表二进制、八进制、十进制和十六进制打印 .

  • 通过前面探索 , 我们知道对象首地址前 8 个字节 , 其实就是 isa .

  • 要使用真机跑 , 模拟器不开启 isa 优化 .

lldb 调试探索

调试结果如下 :

结论 1️⃣ : 实例对象的 isa 指向类对象 .

结论 2️⃣ : 类对象的 isa 指向元类对象 . ( 注意 , 元类对象的地址与类对象不同 )

结论 3️⃣ : 根元类 ( NSObject 的元类 ) 指向自己 .

结论 4️⃣ : 元类的 isa 指向根源类 .

⑤ superClass 指向探索

提示 :

对象中第 8 - 16 位置存储的是 superClass 指针地址 .

struct objc_class : objc_object {
    // Class ISA; 继承与父类 objc_object
    Class superclass;
    cache_t cache;             // formerly cache pointer and vtable
    class_data_bits_t bits;
}
复制代码

结论 1️⃣ : 子类继承父类 ( 好像等于没说 , 忽略.. ) .

结论 2️⃣ : NSObject 的元类 ( 根源类 ) 的父类指向 NSObject 类对象 .

完美验证了上述 isa 流程指示图 .

3、总结

  • isa 是连接实例对象 , 类与元类的重要桥梁 .
  • isa64 位机器开始引入 TaggedPointer , 与 isa 的优化 ( NONPOINTER_ISA ) , 使用联合体 + 位域的模式 , 来存储更多内容 , 取值方式也变成使用掩码 mask 位运算获取真实 cls .
  • isa 指向 :
    • 实例对象的 isa 指向类对象 .
    • 类对象的 isa 指向元类对象 . ( 注意 , 元类对象的地址与类对象不同 , 名称相同 ) .
    • 根元类 ( NSObject 的元类 ) 指向自己 .
    • 元类的 isa 指向根源类 .
  • superClass 指向 :
    • 子类 superClass 指向父类 .
    • 根源类 superClass 指向 NSObject 类 .

至此 , isa 的相关知识点我们已经探索完毕了 . 后续继续更新类的结构探索 , KVC , KVO , RunLoop 等底层探索 , 敬请关注 .