iOS底层之isa结构分析及关联类
从iOS底层之alloc、init探究这篇文章,我们可以知道,alloc
一个对象的过程,主要是计算所需内存大小cls->instanceSize
、申请内存空间calloc
、将指针与类进行关联obj->initInstanceIsa
。
💡那么指针和类是怎么关联的呢?
isa
到底是什么结构?保存了什么信息?下面来一一解惑。
int main(int argc, const char * argv[]) {
@autoreleasepool {
// insert code here...
BKPerson *objc = [BKPerson alloc];
NSLog(@"Hello, World! %@",objc);
}
return 0;
}
从alloc
的源码跟进去到关联指针和类的步骤:
if (!zone && fast) {
obj->initInstanceIsa(cls, hasCxxDtor);
} else {
// Use raw pointer isa on the assumption that they might be
// doing something weird with the zone or RR.
obj->initIsa(cls);
}
跟进obj->initInstanceIsa(cls, hasCxxDtor);
inline void
objc_object::initInstanceIsa(Class cls, bool hasCxxDtor)
{
ASSERT(!cls->instancesRequireRawIsa());
ASSERT(hasCxxDtor == cls->hasCxxDtor());
initIsa(cls, true, hasCxxDtor);
}
主要做的事情是initIsa(cls, true, hasCxxDtor);
inline void
objc_object::initIsa(Class cls, bool nonpointer, bool hasCxxDtor)
{
ASSERT(!isTaggedPointer());
if (!nonpointer) {
isa = isa_t((uintptr_t)cls);
} else {
ASSERT(!DisableNonpointerIsa);
ASSERT(!cls->instancesRequireRawIsa());
isa_t newisa(0);
#if SUPPORT_INDEXED_ISA
ASSERT(cls->classArrayIndex() > 0);
newisa.bits = ISA_INDEX_MAGIC_VALUE;
// isa.magic is part of ISA_MAGIC_VALUE
// isa.nonpointer is part of ISA_MAGIC_VALUE
newisa.has_cxx_dtor = hasCxxDtor;
newisa.indexcls = (uintptr_t)cls->classArrayIndex();
#else
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;
#endif
// This write must be performed in a single store in some cases
// (for example when realizing a class because other threads
// may simultaneously try to use the class).
// fixme use atomics here to guarantee single-store and to
// guarantee memory order w.r.t. the class index table
// ...but not too atomic because we don't want to hurt instantiation
isa = newisa;
}
}
可以看到不管!nonpointer
条件是否满足,都会生成一个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
};
这个isa_t
是一个union
联合体,Class cls
代表isa
关联的类的类型,uintptr_t bits
是一段保存着isa
指针优化、是否关联对象标志位、对象是否有析构函数、类相关信息、引用计数等信息的8
字节大小的无符号长整型数据。要知道联合体和结构体的区别是:
-
结构体(
struct
)中所有变量是“共存”的——优点是“有容乃大”, 全面;缺点是struct内存空间的分配是粗放的,不管用不用,全分配。结构体的类型大小大于等于
内部所有变量的类型大小总和,最终的类型大小是最大成员的类型大小的倍数,不足补齐。 -
联合体(
union
)中是各变量是“互斥”的——缺点就是不够“包容”; 但优点是内存使用更为精细灵活,也节省了内存空间。联合体类型的大小等于
最大成员类型大小。
就是说联合体采用内存覆盖机制
,只有一块变量存储区,只能存一个变量的值,新的成员赋值会把原本存储的成员信息替换掉。也就是Class cls
和uintptr_t bits
只能set
赋值其中一个。而内存使用的精细灵活,体现在以位域
(即二进制中每一位均可表示不同的信息)存储成员数据,也就是以计算机二进制存储的方式,位bit
为单位,用1
和0
标记数据,数据的尺寸大小是以占用多少bit
,而不是以每个数据成员数据类型的尺寸大小(多少字节byte
)存储。isa
的bits
占用的内存大小是8字节
,即64位
,可以存储足够多的信息,很大节省了内存。
isa
的bits
成员的位域,定义在isa.h
源文件中
struct {
ISA_BITFIELD; // defined in isa.h
};
ISA_BITFIELD
是一个宏定义,分别在macOS的x86_64架构
和iPhone真机的arm64架构
是这样的:
bits
的64位存储分布图:
其中存储的成员信息:
-
nonpointer
一般自定义的类都是这个类型的,而系统类才会有纯isa
指针的情况,占1
位。
0
:纯isa指针。
1
:不只是类对象地址,isa
中包含了类信息、对象的引用计数等。 -
has_assoc
关联对象标志位,0
代表没有关联对象,1
代表存在关联对象,占1
位。 -
has_cxx_dtor
该对象是否有C++
或OC
的析构器,如果有析构函数,则需要做析构逻辑,如果没有,则可以更快释放对象,占1
位。 -
shiftclx
存储类指针的值, 也就是类信息,开启指针优化的情况下,在arm64
架构中有33
位用来存储类指针,x86_64
架构中占44
位。 -
magic
用于调试器判断当前对象是真的对象还是没有初始化的空间,占6
位。 -
weakly_refrenced
是指对象是否被指向或者曾经指向一个ARC
的弱变量,没有弱引用的对象可以更快释放。 -
deallocating
标志对象是否正在释放内存。 -
has_sidetable_rc
当对象引用计数大于2
的19
次方(x86_64
架构为2
的8
次方)时,则需要存储到散列表,这时该变量值变为true
。 -
extra_rc
表示该对象的引用计数值,最大为2
的19
次方(x86_64
架构为2
的8
次方),实际上是引用计数值减1
,,如果大于最大容量,就需要取一半计数存到散列表中,真机上最多有8
张散列表存储对象引用计数,x86_64
则最多64
张,这时上面的has_sidetable_rc
值置为true
。
了解完isa
内部结构之后,我们来验证一下alloc
的过程中isa
跟类是如何关联的。
在执行BKPerson *objc = [BKPerson alloc];
时跟进到initIsa
的方法中:
可以看到
!nonpointer
条件为false,说明BKPerson
类并不是一个纯isa指针
,需要开启指针优化,所以走到下面的初始化流程。
打印出这个newisa
:
这时的isa
的成员cls
为nil
,bits
默认为0
,bits
的位域信息都是初始值0
。
往下执行
这一句是给
bits
赋值一个初始值,这是一个系统宏定义
# define ISA_MAGIC_VALUE 0x001d800000000001ULL
这时再打印newisa
,可以看到赋值后的nonpointer
已经是1了,magic
为59。
上面我们了解到magic
在x86_64
系统下的bits
位域分布在47-52位
,占据6位
,用计算器验证下这个59
是不是0x001d800000000001ULL
里的:
可以看到第一位是
1
,跟我们的打印结果一致,nonpointer
值变为1
,第47
位往后数6
位是111011
,那么59
的二进制是:59的二进制
此时此刻,可以得出结果,这magic
的59
确实是由0x001d800000000001ULL
填进去的。
再往下执行
has_cxx_dtor赋值为false,表示没有自定义的析构函数。
newisa.shiftcls = (uintptr_t)cls >> 3;
表示将cls类地址右移3位
,赋值给shiftcls
,上面我们知道x86_64
下,shiftcls
在bits
的64位
内存中占用44位
,从3-46位
。
通过打印的信息,cls = BKPerson
能看出来已经将类信息关联上指针了,也就是这个shiftcls = 536871965
这个信息保存着类的信息。
打印
cls
这个类,并手动将其地址右移3位
,可以得出536871965
,确实等于shiftcls
的数值。
cls右移3位的原因💡那么为什么要右移3位呢?而不直接赋值过去呢?
从图可以清晰解释,为什么需要右移3位?因为
bits
的成员shiftcls
在x86_64
下占据44位
,而类cls
内存存储的类信息是在第3位
到47位
,所以需要右移3位
后开始存储,存到44位
满了就停止存储。这样才能准确的存储到类的信息。
💡那我们怎么证明得出的这个类就是已经关联上了我们的对象指针?
我们将断点的堆栈回退到obj
的关联类的地方。
控制台打印这个
obj
指针,并将isa
的内存地址右移3位
,再左移20位
,再右移17位
,这时再打印地址移动之后的isa
的地址,可以看到,就是我们上面关联的类,也就是说这时对象指针和类关联上了。
这个过程可以用下图清晰表现出来:
从指针获取类信息的操作过程
获取对象的类
这个操作其实在我们日常开发中经常用到,我们通过导入#import <objc/runtime.h>
,
BKPerson *objc = [BKPerson alloc];
NSLog(@"%@", object_getClass(objc));
结果为BKPerson
。
查看这个函数的源码,
Class object_getClass(id obj)
{
if (obj) return obj->getIsa();
else return Nil;
}
继而查找getIsa()
inline Class
objc_object::getIsa()
{
if (fastpath(!isTaggedPointer())) return ISA();
extern objc_class OBJC_CLASS_$___NSUnrecognizedTaggedPointer;
uintptr_t slot, ptr = (uintptr_t)this;
Class cls;
slot = (ptr >> _OBJC_TAG_SLOT_SHIFT) & _OBJC_TAG_SLOT_MASK;
cls = objc_tag_classes[slot];
if (slowpath(cls == (Class)&OBJC_CLASS_$___NSUnrecognizedTaggedPointer)) {
slot = (ptr >> _OBJC_TAG_EXT_SLOT_SHIFT) & _OBJC_TAG_EXT_SLOT_MASK;
cls = objc_tag_ext_classes[slot];
}
return cls;
}
再查找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
}
查看if里的条件的宏定义,
# define SUPPORT_INDEXED_ISA 0
可以知道走的是这行代码return (Class)(isa.bits & ISA_MASK);
也就是取出对象的isa
里的bits
去与运算
上 ISA_MASK
。
这个宏的定义是:
我们再通过lldb
命令验证这个过程。取出对象obj
的isa地址
& ISA_MASK
,可以得出就是对象的类。
那么这个算法,其实就简化了我们上面对isa
地址的一顿左移右移操作,直接一步到位得出类。
通过计算器查看这个ISA_MASK
宏的二进制
可以看到,从第
4位
到47位
,一共44位
,都为1
,其他位都为0
,而与运算
,就是两个数只有相同位上都为1
,才会得出1
,所以这个与运算
,就是为了取出中间44位
的类信息的算法,其他位补0
,得出一个64位
的数,表示这个类。至此,我们了解了
isa
结构,及其位域的分布和成员作用,并探索了对象指针关联类的过程并验证结果。感谢阅读~