在我们iOS开发进行lldb调试的时候,经常会在控制台看到isa的存在,那么本文就来分析一下isa的结构。
在分析isa之前,我们还需要了解一下联合体和位域的的结构:
联合体:简单来说联合体的作用就是使不同类型的变量存放到同一段内存单元中,也称共用体。
位域:把一个字节中的二进制位划分为几个不同的区域。
介绍完联合体和位域之后,我们来了解一下联合体位域这个结构:
我们知道对象的内存开辟决定于属性,如果对于一个属性是int类型,那么4*4 = 16字节 * 8位 = 128位
,占用的内存比较大,是一种浪费的现象。
那么我们怎么节省内存?
那就出现了联合体位域结构,请看代码:
// 联合体
union {
char bits;
// 位域
struct { // 0000 1111
char front : 1;
char back : 1;
char left : 1;
char right : 1;
};
} _direction;
上面的代码就是一个联合体位域结构,我们通过对每个属性的get,set方法对属性进行存储。在联合体中,有一个属性是bits,bits的每个位置代表_direction每个属性占用的大小。类似于0000 1111
,用后四位分别代表_direction四个属性。
而使用了联合体位域的使用,大大的减少了内存空间的使用,只占用了4个内存。
那么回到isa,我们知道isa占用内存大小为8个字节,那么内存为:8*8=64
,存储的当前类的信息,64位存一个类的信息,那是搓搓有余的;那么一个类的信息中,并不只有isa,还有NONPOINTER_ISA,shiftcls等很多信息,我们没有必要去定义而外的信息,来增加类的负担,所以就直接堆放在isa里面,来加快和优化整个性能。
接下来我们来看一下isa的源码(我下载的objc4-781源码),找到initIsa(cla)
方法并点击进去;
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;
}
}
从源码可以知道,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
};
isa_t是一个联合体,这个联合体提供了两个初始化方法,还有两个属性:bits,cls,我们根据初始化的方法可以了解到,cls和bits是一种互斥
的关系,这也是联合体的一种特性
,就是说cls和bits只能有一个能进行set方法,下一个set会对上一个set进行覆盖;而get方法就不能在这种情况。
而根据isa_t的源码分析可以得知,当cls有值时,并不会触发ISA_BITFIELD
结构体的赋值,而当bits有值时,就会触发结构体进行赋值。(关于ISA_BITFIELD结构体的代码,源码有很多不同的架构代码,例如arm64,x86_64等不同的代码,本文以x86_64架构源码进行讲解)
# elif __x86_64__
# define ISA_MASK 0x00007ffffffffff8ULL
# define ISA_MAGIC_MASK 0x001f800000000001ULL
# define ISA_MAGIC_VALUE 0x001d800000000001ULL
# 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
# define RC_ONE (1ULL<<56)
# define RC_HALF (1ULL<<7)
我在这边解释一下isa中的各个属性的解释:
nonpointer:表示是否对 isa 指针开启指针优化;
0:纯isa指针,
1:不止是类对象地址,isa 中包含了类信息、对象的引用计数等
has_assoc:关联对象标志位,0没有,1存在
has_cxx_dtor:该对象是否有 C++ 或者 Objc 的析构器,
如果有析构函数,则需要做析构逻辑, 如果没有,则可以更快的释放对象
shiftcls:存储类指针的值。开启指针优化的情况下,在 arm64 架构中有 33 位用来存储类指针。
magic:用于调试器判断当前对象是真的对象还是没有初始化的空间
weakly_referenced:志对象是否被指向或者曾经指向一个 ARC 的弱变量,没有弱引用的对象可以更快释放。
deallocating:标志对象是否正在释放内存has_sidetable_rc:当对象引用技术大于 10 时,则需要借用该变量存储进位
结构体中的信息,都是类中的信息;
因此,从initIsa源码可以得知,当nonpointer为假时,他是使用isa_t((uintptr_t)cls);对isa进行初始化,而为真时,就会通过newisa(0)对isa_t进行初始化赋值,通过对newisa打印,可以得到一下结果:
(lldb) p newisa
(isa_t) $0 = {
cls = 0x00000001000020f8
bits = 4294975736
= {
nonpointer = 0
has_assoc = 0
has_cxx_dtor = 0
shiftcls = 536871967
magic = 0
weakly_referenced = 0
deallocating = 0
has_sidetable_rc = 0
extra_rc = 0
}
}
以上代码结果为程序运行到isa_t newisa(0);
后一步产生的结果,继续往下执行,会对newisa的bits进行赋值:newisa.bits = ISA_MAGIC_VALUE;(ISA_MAGIC_VALUE=0x001d800000000001ULL)
,这时候再对newisa打印结果:
(lldb) p newisa
(isa_t) $1 = {
cls = 0x001d800000000001
bits = 8303511812964353
= {
nonpointer = 1
has_assoc = 0
has_cxx_dtor = 0
shiftcls = 0
magic = 59
weakly_referenced = 0
deallocating = 0
has_sidetable_rc = 0
extra_rc = 0
}
}
得到的新的结果中,magic=59,这又是怎么得到的呢?
打开计算机,对ISA_MAGIC_VALUE值进行转换,查看二进制,在第47之后,有一个111011
值。
那么我们再用计算器计算59的值:
可以看到,最终转换的二进制结果一样是111011
,一摸一样;这个59就是这样来的。
那么继续执行代码:newisa.shiftcls = (uintptr_t)cls >> 3;
这一步是对shiftcls进行赋值。
然后将newisa赋值给isa,控制台打印显示的结果为:
(lldb) p newisa
(isa_t) $2 = {
cls = Person
bits = 8303516107940073
= {
nonpointer = 1
has_assoc = 0
has_cxx_dtor = 0
shiftcls = 536871965
magic = 59
weakly_referenced = 0
deallocating = 0
has_sidetable_rc = 0
extra_rc = 0
}
}
类的信息是存在shiftcls中的,那么如何获取类信息呢?
继续执行代码,当代码跳转到了obj->initInstanceIsa(cls, hasCxxDtor);
之后,通过一下代码获取我们需要的值:
(lldb) po obj
<Person: 0x1007350b0>
(lldb) x/4gx 0x1007350b0
0x1007350b0: 0x001d8001000020e9 0x0000000000000000
0x1007350c0: 0x566265574b575b2d 0x7261686320776569
(lldb) p 0x001d8001000020e9
(long) $1 = 8303516107940073
(lldb) p 0x001d8001000020e9 & 0x00007ffffffffff8
(long) $2 = 4294975720
(lldb) po 0x001d8001000020e9 & 0x00007ffffffffff8
Person
从上面的代码可以了解到,在x86_64中有一个ISA_MASK 0x00007ffffffffff8ULL
宏定义,通过得到的对person的isa的值与上ISA_MASK,就能得到类信息。
下面我来详细解释一下系统的这个算法是如何实现的,以x86_64架构为例:在这个结构体中,shiftcls的范围在4-47,那么我们来还原一下这个过程:
(lldb) p obj
(Person *) $0 = 0x000000010180d150
(lldb) x/4gx 0x000000010180d150
0x10180d150: 0x001d8001000020e9 0x0000000000000000
0x10180d160: 0x61636f4c534e5b2d 0x6e61506e65704f6c
(lldb) p/x 0x001d8001000020e9 >> 3
(long) $1 = 0x0003b0002000041d
(lldb) p/x 0x0003b0002000041d<<20
(long) $2 = 0x0002000041d00000
(lldb) p/x 0x0002000041d00000>>17
(long) $3 = 0x00000001000020e8
(lldb) p/x cls
(Class) $4 = 0x00000001000020e8 Person
(lldb)
在上面的代码中,先右移三位,再左移20,最后一步移动17位那是因为我们要还原shiftcls原来的位置:4-47。