前言
在写这篇博客之前,我在想要从哪里切入,才能让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)。关于实例对象、类、元类之间的关系,苹果官方给了一张图,非常清晰的表明了三者的关系。
实线是 super_class 指针,虚线是 isa 指针。
-
- Root class(class) 通常是 NSObject,NSObject 是没有超类的,所以 Root class(class)的 superclass 指向 nil。
-
- 每个 Class 都有一个 isa 指针指向唯一的 Meta class
-
- Root class(meta)的 superclass 指向 Root class(class),也就是 NSObject,形成一个回路。
-
- 每个 Meta class 的 isa 指针都指向 Root class(meta)。
一个对象 可以通过 isa
找到类,根据类的 isa
和 super_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
我们以 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位 ,内存布局从低位到高位情况如下图
解释一下各存储内容的含义:
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)
最终结果显示是拿到了类信息,我们来画图分析一下这个过程,用蓝色表示内存中被保留的值,灰色表示内存中被抹除的值
-
- 起始时,完整内存的值均保留
-
- 右移3位
内存整体右移3位,那么高3位将空缺,低3位被移出isa_t内存边界(用透明度表示),所以相当于抹除。
我们只关注isa_t结构内的内存分布,不考虑边界内存的影响,简化绘图为:
- 3.左移31位
低31位被抹除
- 4.右移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进制
从低位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;
}
}
- 首先对整个bits进行赋值,传入 ISA_MAGIC_VALUE ,在arm64架构下,该值为
# define ISA_MAGIC_VALUE 0x000001a000000001ULL
将该值转换为2进制
对应 isa_t
中内存布局的位置,可以看出对 bits 赋值 就是对 nonpinter 和 magic 赋值的过程。
其次对has_cxx_dtor赋值。
最后对shifcls赋值
newisa.shiftcls = (uintptr_t)cls >> 3;
这里 ,对当前传入类进行右移3位的原因是,将cls指针后三位清除以减小内存消耗,因为指针是要按照8字节对齐的,实际后三位是没有意义的。这和 isa_t
中的内存布局没有关系,因为类可不是按照isa_t
进行内存布局的。
至此isa的赋值过程就完成了。
总结
对于 isa
,我们了解了底层原理,对其作用以及相关操作,我们会更加清晰。当然,在这里我们也要学习Apple的设计模式,试着站在开发人员的角度考虑它的设计思想。
然后你一定要熟记 isa
与 super_class
的指向流程,这真的很重要。
最后,希望在此时或者以后的某一天,你可以大胆的对它说:isa,我看透你了!