今天的主题是探索isa
的结构 在此之前我们需要先了解下什么是联合体
构造数据类型的方式有以下两种 :
-
结构体
(struct
) -
联合体
(union
,也称为共用体
)
结构体
结构体是指把不同的数据组合成一个整体
,其变量是共存的,变量不管是否使用,都会分配内存。
缺点:所有属性都分配内存,比较
浪费内存
,假设有4
个int成员,一共分配了16
字节的内存,但是在使用时,你只使用了4字节,剩余的12字节就是属于内存的浪费优点:存储
容量较大
,包容性强
,且成员之间不会相互影响
联合体
联合体也是由不同的数据类型组成
,但其变量是互斥
的,所有的成员共占
一段内存。而且共用体采用了内存覆盖技术
,同一时刻只能保存一个成员的值,如果对新的成员赋值,就会将原来成员的值覆盖
掉
缺点:,包容性弱
优点:所有成员共用一段内存,使内存的使用更为精细灵活,同时也节省了内存空间
两者的区别
内存占用情况
结构体的各个成员会
占用不同的内存
,互相之间没有影响
共用体的所有成员
占用同一段内存
,修改一个成员会影响其余所有成员内存分配大小
结构体内存 >= 所有成员占用的
内存总和
(成员之间可能会有缝隙)共用体占用的内存等于
最大
的成员占用的内存
现在我们已经大致了解什么是联合体,下面进行isa
探索,上代码
int main(int argc, const char * argv[]) {
@autoreleasepool {
// insert code here...
Person *objc = [Person 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); // isa 初始化
} else {
ASSERT(!DisableNonpointerIsa);
ASSERT(!cls->instancesRequireRawIsa());
isa_t newisa(0); // isa 初始化
#if SUPPORT_INDEXED_ISA // !nopointmenter 执行的流程 ,即isa通过cls定义
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 //bits 时执行的流程
newisa.bits = ISA_MAGIC_VALUE;//bist进行赋值 为0x001d800000000001ULL
// 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
类型使用联合体的原因也是基于内存优化的考虑,这里的内存优化是指在isa指针中通过char + 位域
(即二进制中每一位均可表示不同的信息)的原理实现。通常来说,isa指针占用的内存大小是8
字节,即64位,已经足够存储很多的信息了,这样可以极大的节省内存,以提高性能
从isa_t
的定义中可以看出:
提供了两个成员,cls
和bits
,由联合体的定义所知,这两个成员是互斥
的,也就意味着,当初始化isa
指针时,有两种初始化方式
通过cls
初始化,bits
无默认值
通过bits
初始化,cls
有默认值
还提供了一个结构体定义的位域,用于存储类信息及其他信息,结构体的成员ISA_BITFIELD
,这是一个宏定义
,有两个版本 __arm64__
(对应ios 移动端) 和__x86_64__
(对应macOS),以下是它们的一些宏定义,如下图所
其中存储的成员信息:
-
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
是如何跟类
进行关联的
上面 initIsa
方法的源码实现我们可以发现逻辑主要分为两部分
- 通过 cls 初始化 isa
- 通过 bits 初始化 isa
运行源码 断点如图 打印newisa
信息
-继续执行,到newisa.bits = ISA_MAGIC_VALUE;
下一行,表示为isa的bits成员赋值,重新执行lldb命令p newisa
,得到的结果如下
对比发现数据发生变化,可以看到赋值后的nonpointer
已经是1了,magic
为59。
- 其中
magic
是59
是由于将isa指针地址转换为二进制,从47
(因为前面有4
个位域,共占用47
位,地址是从0开始)位开始读取6位,再转换为十进制,如下图所示
此时此刻,可以得出结果,这magic的59确实是由0x001d800000000001ULL填进去的。
继续执行 过newisa.shiftcls = (uintptr_t)cls >> 3;
这一步 打印newisa
-
shiftcls
由0
变成了536871965
即为(uintptr_t)cls >> 3
为什么在shiftcl
s赋值时需要类型强转
?
因为内存的存储不能存储字符串
,机器码只能识别0 、1
这两种数字,所以需要将其转换为uintptr_t
数据类型,这样shiftcls
中存储的类信息才能被机器码理解, 其中uintptr_t是long
为什么需要右移3位
?
主要是由于shiftcls
处于isa
指针地址的中间
部分,前面还有3
个位域,为了不影响前面的3
个位域
的数据,需要右移将其抹零
进一步验证类与指针对象
相关联
- 控制台打印这个
obj
指针,并将isa
的内存地址右移3位
,再左移20位
,再右移17位
,这时再打印地址移动之后的is
a的地址,可以看到,就是我们上面关联的类,也就是说这时对象指针和类关联上了, 如图
- 控制台打印这个
- 通过
isa & ISA_MSAK
如图
- 通过