洋仔的博客 洋仔的博客
首页
  • 个人心法总结

    • 价值心法
  • 技术文档
  • GitHub技巧
  • Nodejs
  • 博客搭建
  • iOS基础知识
  • 前端
  • 学习
  • 面试
  • 心情杂货
  • 实用技巧
  • 投资体系
  • 毛选
  • 友情链接
关于
收藏
  • 分类
  • 标签
  • 归档
GitHub (opens new window)

洋仔

奋斗的小青年
首页
  • 个人心法总结

    • 价值心法
  • 技术文档
  • GitHub技巧
  • Nodejs
  • 博客搭建
  • iOS基础知识
  • 前端
  • 学习
  • 面试
  • 心情杂货
  • 实用技巧
  • 投资体系
  • 毛选
  • 友情链接
关于
收藏
  • 分类
  • 标签
  • 归档
GitHub (opens new window)
  • 技术文档

  • GitHub技巧

  • Nodejs

  • 博客搭建

  • iOS基础知识

    • iOS底层相关

      • OC基础之常见知识
      • OC基础之属性关键字
      • OC基础之底层系列
      • OC反射机制系列
      • 分类与扩展
      • load跟initialize
      • isa指针是什么
        • isa指针是什么?
        • 什么是联合体?
        • 结构体 isa_t
        • isa 指针的作用与元类
        • 什么是元类(meta-class)?
        • 元类的类是什么?
        • 类和元类的继承
        • 为什么要设计metaclass
        • 类对象和元类对象分别是什么,他们之间有什么区别?
      • 引用计数管理
      • 字典系列
    • Runloop系列

    • Runtime系列

    • 内存管理系列

    • Block系列

    • 线程系列

    • KVC跟KVO系列以及通知中心

    • UI系列

    • 离屏渲染系列

    • 组件化系列跟架构

    • OC跟webview交互系列

    • 持久化系列

    • APP编译系列

    • APP性能优化系列

    • cocoapods系列

    • swift系列

    • Git系列

    • 网络相关

    • 三方库系列

    • 系统原理

    • 总结系列

    • 算法系列

    • 数据结构系列

  • 前端

  • 技术
  • iOS基础知识
  • iOS底层相关
洋仔
2024-09-19
目录

isa指针是什么

# isa指针是什么?

isa指针保存着指向类对象的内存地址,类对象全局只有一个,因此每个类创建出来的对象都会默认有一个isa属性,保存类对象的地址,也就是class,通过class就可以查询到这个对象的属性和方法,协议等;

当 ObjC 为一个对象分配内存,初始化实例变量后,在这些对象的实例变量的结构体中的第一个就是 isa。(isa 存储该对象信息,例如引用计数器,弱引用表等)

  • isa指针用来维护 “对象” 和 “类” 之间的关系,并确保对象和类能够通过isa指针找到对应的方法、实例变量、属性、协议等;

  • 在 arm64 架构之前,isa就是一个普通的指针,直接指向objc_class,存储着Class、Meta-Class对象的内存地址。instance对象的isa指向class对象,class对象的isa指向meta-class对象;

  • 从 arm64 架构开始,对isa进行了优化,用nonpointer表示,变成了一个共用体(union)结构,还使用位域来存储更多的信息。将 64 位的内存数据分开来存储着很多的东西,其中的 33 位才是拿来存储class、meta-class对象的内存地址信息。要通过位运算将isa的值& ISA_MASK掩码,才能得到class、meta-class对象的内存地址。

// objc.h
struct objc_object {
    Class isa;  // 在 arm64 架构之前
};

// objc-private.h
struct objc_object {
private:
    isa_t isa;  // 在 arm64 架构开始
};

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

    Class cls;
    uintptr_t bits;

#if SUPPORT_PACKED_ISA

    // extra_rc must be the MSB-most field (so it matches carry/overflow flags)
    // nonpointer must be the LSB (fixme or get rid of it)
    // shiftcls must occupy the same bits that a real class pointer would
    // bits + RC_ONE is equivalent to extra_rc + 1
    // RC_HALF is the high bit of extra_rc (i.e. half of its range)

    // future expansion:
    // uintptr_t fast_rr : 1;     // no r/r overrides
    // uintptr_t lock : 2;        // lock for atomic property, @synch
    // uintptr_t extraBytes : 1;  // allocated with extra bytes

# if __arm64__  // 在 __arm64__ 架构下
#   define ISA_MASK        0x0000000ffffffff8ULL  // 用来取出 Class、Meta-Class 对象的内存地址
#   define ISA_MAGIC_MASK  0x000003f000000001ULL
#   define ISA_MAGIC_VALUE 0x000001a000000001ULL
    struct {
        uintptr_t nonpointer        : 1;  // 0:代表普通的指针,存储着 Class、Meta-Class 对象的内存地址
                                          // 1:代表优化过,使用位域存储更多的信息
        uintptr_t has_assoc         : 1;  // 是否有设置过关联对象,如果没有,释放时会更快
        uintptr_t has_cxx_dtor      : 1;  // 是否有C++的析构函数(.cxx_destruct),如果没有,释放时会更快
        uintptr_t shiftcls          : 33; // 存储着 Class、Meta-Class 对象的内存地址信息
        uintptr_t magic             : 6;  // 用于在调试时分辨对象是否未完成初始化
        uintptr_t weakly_referenced : 1;  // 是否有被弱引用指向过,如果没有,释放时会更快
        uintptr_t deallocating      : 1;  // 对象是否正在释放
        uintptr_t has_sidetable_rc  : 1;  // 如果为1,代表引用计数过大无法存储在 isa 中,那么超出的引用计数会存储在一个叫 SideTable 结构体的 RefCountMap(引用计数表)散列表中
        uintptr_t extra_rc          : 19; // 里面存储的值是对象本身之外的引用计数的数量,retainCount - 1
#       define RC_ONE   (1ULL<<45)
#       define RC_HALF  (1ULL<<18)
    };
......  // 在 __x86_64__ 架构下
};
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52

如果isa非nonpointer,即 arm64 架构之前的isa指针。由于它只是一个普通的指针,存储着Class、Meta-Class对象的内存地址,所以它本身不能存储引用计数,所以以前对象的引用计数都存储在一个叫SideTable结构体的RefCountMap(引用计数表)散列表中。

如果isa是nonpointer,则它本身可以存储一些引用计数。从以上union isa_t的定义中我们可以得知,isa_t中存储了两个引用计数相关的东西:extra_rc和has_sidetable_rc。

  • extra_rc:里面存储的值是对象本身之外的引用计数的数量,这 19 位如果不够存储,has_sidetable_rc的值就会变为 1;extra_rc占了19位,可以存储的最大引用计数:$2^{19}-1+1=524288$,超过它就需要进位到SideTables

    extra_rc:表示该对象的引用计数值。extra_rc只是存储了额外的引用计数,实际的引用计数公式:实际引用计数 = extra_rc + 1。这里占了8位,所以理论上可以存储的最大引用计数是:2^8 - 1 + 1 = 256(arm64CPU架构下的extra_rc占19位,可存储的最大引用计数为2^19 - 1 + 1 = 524288)。

  • has_sidetable_rc:如果为 1,代表引用计数过大无法存储在isa中,那么超出的引用计数会存储SideTable的RefCountMap中,SideTables是一个Hash表,根据对象地址可以找到对应的SideTable,SideTable内包含一个RefcountMap,根据对象地址取出其引用计数,类型是size_t。
    它是一个unsigned long,最低两位是标志位,剩下的62位用来存储引用计数。我们可以计算出引用计数的理论最大值:$2^{62+19}=2.417851639229258e24$。

    其实isa能存储的524288在日常开发已经完全够用了,为什么还要搞个Side Table?我猜测是因为历史问题,以前cpu是32位的,isa中能存储的引用计数就只有$2^{7}=128$。因此在arm64下,引用计数通常是存储在isa中的。

所以,如果isa是nonpointer,则对象的引用计数存储在它的isa_t的extra_rc中以及SideTable的RefCountMap中。

注: 有一些对象比较小则会使用 TaggedPointer技术,不使用isa

isa本质是一个isa_t的类型,那isa_t是一个联合体位域结构

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
};
1
2
3
4
5
6
7
8
9
10
11
12

# 什么是联合体?

当多个数据需要共享内存或者多个数据每次只取其一时,可以利用联合体(union),利用union可以用相同的存储空间存储不同型别的数据类型,从而节省内存空间。

采用这种结构的原因也是基于内存优化的考虑(即二进制中每一位均可表示不同的信息)。通常来说,isa指针占用的内存大小是8字节,即64位,已经足够存储很多的信息了,这样可以极大的节省内存,以提高性能。

# 结构体 isa_t

isa_t 是一个 union 类型的结构体,其中的 isa_t、cls、 bits 还有结构体共用同一块地址空间。而 isa 总共会占据 64 位的内存空间, 8 字节(决定于其中的结构体)

struct {
   uintptr_t nonpointer        : 1;
   uintptr_t has_assoc         : 1;
   uintptr_t has_cxx_dtor      : 1;
   uintptr_t shiftcls          : 44;
   uintptr_t magic             : 6;
   uintptr_t weakly_referenced : 1;
   uintptr_t deallocating      : 1;
   uintptr_t has_sidetable_rc  : 1;
   uintptr_t extra_rc          : 8;
};
1
2
3
4
5
6
7
8
9
10
11
  • nonpointer:表示是否对 isa 指针开启指针优化,0:纯isa指针,1:不⽌是类对象地址,isa 中包含了类信息、对象的引⽤计数等。 如果该实例对象启用了Non-pointer,那么会对isa的其他成员赋值,否则只会对cls赋值。

是否关闭Non-pointer目前有这么几个判断条件,这些都可以在runtime源码objc-runtime-new.m中找到逻辑。

 1:包含swift代码;
  2:sdk版本低于10.11;
  3:runtime读取image时发现这个image包含__objc_rawisa段;
  4:开发者自己添加了OBJC_DISABLE_NONPOINTER_ISA=YES到环境变量中;
  5:某些不能使用Non-pointer的类,GCD等;
  6:父类关闭。
1
2
3
4
5
6
  • 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。

# isa 指针的作用与元类

Objective-C 中类也是一个对象。

因为在 Objective-C 中,对象的方法并没有存储于对象的结构体中(如果每一个对象都保存了自己能执行的方法,那么对内存的占用有极大的影响)。

当实例方法被调用时,它要通过自己持有的 isa 来查找对应的类,然后在这里的 class_data_bits_t 结构体中查找对应方法的实现。同时,每一个 objc_class 也有一个指向自己的父类的指针 super_class 用来查找继承的方法。

类方法的实现又是如何查找并且调用的呢?这时,就需要引入元类来保证无论是类还是对象都能通过相同的机制查找方法的实现。

让每一个类的 isa 指向对应的元类,这样就达到了使类方法和实例方法的调用机制相同的目的:

  • 实例方法调用时,通过对象的 isa 在类中获取方法的实现
  • 类方法调用时,通过类的 isa 在元类中获取方法的实现

# 什么是元类(meta-class)?

Objective-C 的一个类也是一个对象。这意味着你可以发送消息给一个类。

NSStringEncoding defaultStringEncoding = [NSString defaultStringEncoding];
1

在这个示例里,defaultStringEncoding被发送给了NSString类。

之所以能成功是因为 Objective-C 中每个类本身也是一个对象。如上面所看到的,这意味着类结构也必须以一个isa指针开始,从而可以和objc_object在二进制层面兼容,之后这个结构的下一字段必须是一个指向父类的指针(对于基类则为nil)。

正如我上周展示的,定义一个Class有很多种方式,取决于你的运行时库版本,但有一点,它们都以isa字段开始,并且仅跟着一个superclass字段

typedef struct objc_class *Class;
struct objc_class {
    Class isa;
    Class super_class;
    /* followed by runtime specific details... */
};
1
2
3
4
5
6

为了调用Class里的方法,该Class的isa指针也必须指向一个包含了该Class方法列表的Class。

这就引出了元类的定义:元类是Class的类。

简单来说就是:

  • 当你给对象发送消息时,消息是在寻找这个对象的类的方法列表;
  • 当你给类发消息时,消息是在寻找这个类的元类的方法列表。

元类是必不可少的,因为它存储了类的类方法。每个类都必须有独一无二的元类,因为每个类都有独一无二的类方法。

# 元类的类是什么?

元类,就像之前的类一样,它也是一个对象。你也可以调用它的方法。自然的,这就意味着他必须也有一个类。

所有的元类都使用根元类(继承体系中处于顶端的类的元类)作为他们的类。这就意味着所有NSObject的子类(大多数类)的元类都会以NSObject的元类作为他们的类

根据这个规则,所有的元类使用根元类作为他们的类,根元类的元类则就是它自己。也就是说基类的元类的isa指针指向他自己

# 类和元类的继承

类用super_class指针指向了父类,同样的,元类用super_class指向类的super_class的元类。

说的更拗口一点就是,根元类把它自己的基类设置成了super_class。

在这样的继承体系下,所有实例、类以及元类都继承自一个基类。

这意味着对于继承于NSObject的所有实例、类和元类,他们可以使用NSObject的所有实例方法,类和元类可以使用NSObject的所有类方法

# 为什么要设计metaclass

metaClass是单一职责和扩展性: instance的信息由Class所有; Class的信息则由metaClass所有;

否则类方法,实际方法都在同一个流程中,类对象、元类对象能够复用消息发送流程机制;

  • 根据消息接受者的isa指针找到metaclass(因为类方法存在元类中。如果调用的是实例方法,isa指针指向的是类对象。)
  • 进入CacheLookup流程,这一步会去寻找方法缓存,如果缓存命中则直接调用方法的实现,如果缓存不存在则进入objc_msgSend_uncached流程。

# 类对象和元类对象分别是什么,他们之间有什么区别?

实例对象可以通过isa指针找到它的类对象,类对象存储实例方法列表等信息。类对象可以通过isa指针找到它的元类对象,从而可以访问类方法列表等相关信息

类对象或是元类对象都是objc_class数据结构的,objc_class由于继承自objc_object,所以他们都有isa指针,所有实例可以找到类,类可以找到元类

编辑 (opens new window)
上次更新: 2024/10/23, 23:26:17
load跟initialize
引用计数管理

← load跟initialize 引用计数管理→

最近更新
01
数组
10-25
02
数组双指针系列之对撞指针
10-25
03
数组双指针系列之快慢指针
10-25
更多文章>
Theme by Vdoing | Copyright © 2019-2024 Evan Xu | MIT License
  • 跟随系统
  • 浅色模式
  • 深色模式
  • 阅读模式