iOS开发者一定知道每个实例对象都有一个isa指针,其中存储着对象的类信息。今天我们就来探究下isa是如何保存类的信息的。
通过objc的源码可以找到我们在调用alloc方法创建实例对象的时候有初始化isa的操作,其初始化代码如下
inline void
objc_object::initIsa(Class cls, bool nonpointer, UNUSED_WITHOUT_INDEXED_ISA_AND_DTOR_BIT bool hasCxxDtor)
{
ASSERT(!isTaggedPointer());
isa_t newisa(0);
if (!nonpointer) {
newisa.setClass(cls, this);
} else {
ASSERT(!DisableNonpointerIsa);
ASSERT(!cls->instancesRequireRawIsa());
#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
# if ISA_HAS_CXX_DTOR_BIT
newisa.has_cxx_dtor = hasCxxDtor;
# endif
newisa.setClass(cls, this);
#endif
newisa.extra_rc = 1;
}
isa = newisa;
}
由上述代码可知,isa其实是isa_t类型的结构体,那么isa_t又是什么呢?
union isa_t {
isa_t() { }
isa_t(uintptr_t value) : bits(value) { }
uintptr_t bits;
private:
// Accessing the class requires custom ptrauth operations, so
// force clients to go through setClass/getClass by making this
// private.
Class cls;
public:
#if defined(ISA_BITFIELD)
struct {
ISA_BITFIELD; // defined in isa.h
};
bool isDeallocating() {
return extra_rc == 0 && has_sidetable_rc == 0;
}
void setDeallocating() {
extra_rc = 0;
has_sidetable_rc = 0;
}
#endif
void setClass(Class cls, objc_object *obj);
Class getClass(bool authenticated);
Class getDecodedClass(bool authenticated);
};
从isa_t的定义中引出了一个新的数据类型--联合体
。我们来看下联合体和结构体有什么区别:
结构体
结构体是把不同的数据组合成一个整体,其变量是共存的,不管变量是否使用,在创建时都会分配内存。
-
优点:
存储容量大,包容性强
,且成员之间不会相互影响
。 -
缺点:所有变量都会分配内存,
内存消耗较大
。
联合体
联合体也是由不同的数据类型组成,但其变量是互斥
的,所有的成员共占一段内存,而且联合体采用了内存覆盖技术,同一时刻只能保存一个成员变量的值
,如果对新的成员赋值,就会将原来的成员的值覆盖掉。
-
优点:所有成员共用一段内存,使
内存的使用更为精细灵活
,同时也节省了内存空间
。 -
缺点:
包容性差
。
我们再回过来看isa_t的定义,其定义了两个变量一个为bits,另一个为cls
,同时如果定义了ISA_BITFIELD
的情况下,定义了位域结构体
以便我们精确的操作isa_t中的不同位的数据。ISA_BITFIELD的定义根据不同的cpu进行了不同的定义,定义的字段和内容大同小异,只是不同的字段占用的位数量不一样,本文仅以x86_64为例说明:
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 unused : 1; \
uintptr_t has_sidetable_rc : 1; \
uintptr_t extra_rc : 8
如上所示就是ISA_BITFIELD在x86_64下的定义,其中:
-
nonpointer
:存储于第0位,标识是否为优化isa标志
。0代表不是优化的isa,此isa是一个纯指向类或者原类的指针;1标识是优化的isa,此isa中包含类信息,对象的引用计数等。我们现在创建的对象中的isa基本上都是优化后的isa
。 -
has_assoc
:存储于第1位,关联对象标志位
。对象含有或者曾经含有关联引用。0表示没有;1表示有。没有关联引用的对象可以更快的释放内存。 -
has_cxx_dtor
:存储于第3位,析构函数标志位
,如果有析构函数,则需进行析构逻辑,如果没有,则可以更快速的释放对象。 -
shiftcls
:存储于第3-46位,存储类的信息,这个是我们主要关注的内容
。 -
magic
:存储于第47-52位,判断对象是否初始化完成
,是调试器判断当前对象是真的对象还是没有初始化的空间。 -
weakly_referenced
:存储于第53位,标识对象是否被指向或者曾经被指向一个ARC的弱引用变量
。如果没有弱引用的对象可以更快的被释放。 -
unused
:存储于第54位。 -
has_sidetable_rc
:存储于第55位,判断该对象的引用计数是否过大
,如果过大则需要其他散列表来进行存储。 -
ISA_BITFIELDextra_rc
:存储于第56-63位,存放该对象引用计数值-1的结果
。
了解了isa的数据类型之后我们看下objc是如何设置isa的呢?根据objc源码我们找到了设置isa的具体方法initIsa,代码如下:
inline void
objc_object::initIsa(Class cls, bool nonpointer, UNUSED_WITHOUT_INDEXED_ISA_AND_DTOR_BIT bool hasCxxDtor)
{
ASSERT(!isTaggedPointer());
isa_t newisa(0);
if (!nonpointer) {
newisa.setClass(cls, this);
} else {
ASSERT(!DisableNonpointerIsa);
ASSERT(!cls->instancesRequireRawIsa());
#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
# if ISA_HAS_CXX_DTOR_BIT
newisa.has_cxx_dtor = hasCxxDtor;
# endif
newisa.setClass(cls, this);
#endif
newisa.extra_rc = 1;
}
isa = newisa;
}
在实际开发中,我们创建的类基本上都是nonpointer的,故此处isa的创建会走else中的代码,在此处打上断点后我们一步一步跟断点。
ISA_MAGIC_VALUE
经过ISA_MAGIC_VALUE
初始化newisa.bit
之后,我们看到newisa中bits的值变为了8303511812964353
,而ISA_MAGIC_VALUE转为10进制数也是8303511812964353,在计算器中输入后可以看到2进制计数法中每一位的情况,如下所示
在newisa
的位信息中,除了nonpointer为1与计算器中的第0位的1相符之外,magic被置为了59
,同时计算器中从第47位开始(即magic的首位)显示为110111
,此时我们将59输入计算器中发现59的二进制计数正是110111,与bit中的值相等
。
下一步是setClass,我们进入setClass方法后发现设置shiftcls
的代码就一行。
shiftcls = (uintptr_t)newCls >> 3;
此处将class右移了3位
存入了shiftcls中,这是因为class信息的最后4位始终是0
,即无效位,为了匹配shiftcls是从第3位开始存储的,便去除了最右边三位无效位存入了shiftcls中
。
在setClass之后shiftcls的信息变成了536875037,那么此时shiftcls中存储的是否是SLPerson的类信息呢?
我们通过以下几种方式来看下:
- 我们刚刚说了class的最后三位是0,所以我们只需要将isa中除了shiftcls的其它位置0便可以得到正确的类信息,即我们可以采取
右移3位,左移20位,再右移17位
的方式将其余位全部置0。
(lldb) p newisa.bits >> 3
(uintptr_t) $53 = 1037939513495581
(lldb) p $53 << 20
(uintptr_t) $54 = 562954278797312
(lldb) p $54 >> 17
(uintptr_t) $55 = 4295000296
(lldb) po $55
SLPerson
运算过程
通过如上打印可以看到我们将其余位全部置0后得到的信息就是SLPerson。
这是我们直接通过位运算来还原了isa中类的信息,那么系统又是如何获取类的信息的呢?
- (Class)class {
return object_getClass(self);
}
Class object_getClass(id obj)
{
if (obj) return obj->getIsa();
else return Nil;
}
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;
}
inline Class
objc_object::ISA(bool authenticated)
{
ASSERT(!isTaggedPointer());
return isa.getDecodedClass(authenticated);
}
inline Class
isa_t::getDecodedClass(bool authenticated) {
#if SUPPORT_INDEXED_ISA
if (nonpointer) {
return classForIndex(indexcls);
}
return (Class)cls;
#else
return getClass(authenticated);
#endif
}
inline Class
isa_t::getClass(MAYBE_UNUSED_AUTHENTICATED_PARAM bool authenticated) {
#if SUPPORT_INDEXED_ISA
return cls;
#else
uintptr_t clsbits = bits;
clsbits &= ISA_MASK;
return (Class)clsbits;
#endif
}
# define ISA_MASK 0x00007ffffffffff8ULL
从源码可知,获取class的方法最终就是一行代码return bits & ISA_MASK
,我们将ISA_MASK
放入计算器中发现就是一个3-46位为1,其余位为0的二进制数
。与bits按位进行&运算最终的结果和我们进行位移运算是一样的~
网友评论