本文为L_Ares个人写作,包括图片皆为个人亲自操作,以任何形式转载请表明原文出处。
前一节接触到了isa
,相信做iOS开发的大家对isa
听到的太多了,尤其是某张非常出名的runtime的流程图里面。isa
可谓是贯穿了我们的开发之路。
那么根据第一节中我们没有详细说到的isa
在对象alloc
中的初始化和绑定类与对象,开始探索isa
。
还是要使用到objc
的部分开放源码。
一、关于alloc
中isa
的初始化InitIsa
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);
}
这段代码是我们之前在将alloc
中obj
和cls
绑定的时候说到的,当时说了obj
是通过isa
和类进行的绑定,从而确定了obj
的类型,那么这个isa
怎么来的?isa
又包含了什么内容在里面?
这需要进入到initInstanceIsa
,
inline void
objc_object::initInstanceIsa(Class cls, bool hasCxxDtor)
{
ASSERT(!cls->instancesRequireRawIsa());
ASSERT(hasCxxDtor == cls->hasCxxDtor());
initIsa(cls, true, hasCxxDtor);
}
上面的ASSERT
不要管,主要的是那句initIsa
,直接进入initIsa
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
。
nonpointer
是什么?
nonpointer
: 表示是否对isa开启指针优化 。0代表是纯isa指针,1代表除了地址外,还包含了类的一些信息、对象的引用计数等。
那么我们从上面传来的是true
,所以!nonpointer
是不会走的。直接看else
。
isa_t newisa(0);
第二节中说过了,isa_t
这个是isa
的类,是个联合体。所以这里就是初始化了isa
。
然后看到下面一大堆的给isa
对象newisa
进行set,那就看一下,这都是些什么元素。
直接进入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
};
这里就找到了一个bits
,其他的呢?全都在下面的那个结构体的元素里面,直接进入这个ISA_BITFIELD
元素。
# if __arm64__
# define ISA_MASK 0x0000000ffffffff8ULL
# define ISA_MAGIC_MASK 0x000003f000000001ULL
# define ISA_MAGIC_VALUE 0x000001a000000001ULL
# define ISA_BITFIELD \
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
# define RC_ONE (1ULL<<45)
# define RC_HALF (1ULL<<18)
# 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)
# else
# error unknown architecture for packed isa
# endif
// SUPPORT_PACKED_ISA
#endif
这里就发现了isa
带有的元素,并且都是用位域来指定的大小。
我们因为用的是Mac
模拟器,所以进入的肯定是__x86_64__
,手机才是进入__arm64__
,这个就不多说了,大家应该都是清楚的。两个宏定义的元素是一样的,只不过不同的系统下,存在一点内存大小的差异。
那么看一下__x86_64__
(因为这个我们能调试,真机不好运行)objc781
的源码中,这些元素都是什么意思,nonpointer
说过了,就不再废话。
has_assoc
: 关联对象标志位,占用1位内存空间,表明是否有关联对象。
0则没有关联对象。
1则有关联对象。has_cxx_dtor
: 该对象是否有C++或Objc的析构器,占1位内存空间,
1表示有析构函数,需要做一些析构的逻辑处理。
0表示没有析构函数,可以更快的释放对象。shiftcls
: 存储类的指针的值,也就是类的地址,类的信息。在开启指针优化的前提下,也就是nonpointer
是1
的情况下。
arm64
: iOS系统,占用33位内存空间来存储类的指针。
x86_64
: MacOS系统,占用44位内存空间存储类的指针。magic
: 用于给调试器判断,占6位内存空间。判断当前对象是真的对象还是一段没有初始化的空间。weakly_refrenced
: 是否被指向或者曾经指向一个ARC的弱变量,占1位内存空间。
1表示指向或者曾经指向过
0表示没有指向,没有弱饮用的对象释放的更快速。deallocating
: 对象是否正在释放,占1位内存空间。
1表示对象正在释放
0表示对象不是正在释放的过程中。has_sidetable_rc
: 是否需要sidetable
(runtime的散列表)来存储引用计数,占1位内存空间。
1表示需要,那么这个时候对象的引用计数应该大于了10。
0表示不用。extra_rc
: 表示该对象的引用计数值。占用8位内存空间,这就很明显了,占用8位内存空间的除了char*
就那几个数值型的类型用的最多。
了解了上述这些isa
这个联合体对象带有的元素后,再看这一句
newisa.shiftcls = (uintptr_t)cls >> 3;
这就很明显了,它就是把cls
和obj
关联到一起的“元凶”。
二、验证isa把cls
和obj
关联
上面说是这么说,但是这都是根据源码含义的猜测,能不能拿出来点实际的证据证明,就是isa
把cls
和obj
关联了呢?介绍两种方法。
1. 通过shiftcls
shiftcls
上面介绍过了,说是存储类的地址的,存储类的信息的。那么就看看它和我们初始化的对象的isa
地址对不对的上了,如果对的上,那就证明初始化的对象的isa
的确是指向了类。
首先在main.m
中为JDMan
类初始化一个对象,打印它的内存地址。
我们说无论编译器优化怎么优化这个对象的属性的内存,无论它的属性内存发生了什么变化,这个对象的第一个对象一定是isa
。为什么这么说?因为这个的对象的isa
是来自于继承,根本还没有编译当前的属性列表,在你的属性列表形成之前,人家isa
就已经在那里了。
继续,
- 即然已经拿到
isa
的内存地址了,那么打印成二进制看看,一会儿正好可以和shiftcls
保存的类的信息做一下对比。所以打印它的二进制地址。
- 然后我们拿到我们目标对象,我们的目标对象不就是
cls
吗,那就拿类的地址也放着。
- 然后我们看一下
shiftcls
。
newisa.shiftcls = (uintptr_t)cls >> 3;
这里发现,shiftcls
的类型明明是uintptr_t
,那我们就把目标类也变成这个
那现在JDMan
的地址也是uintptr_t
类型的了。
-
newisa.shiftcls = (uintptr_t)cls >> 3;
表明了,类的地址,右移3位才存到shiftcls
,那就右移3位。
好了,现在类已经满足存进去的要求了,存进去以后,地址就应该是$4
所示。
-
类的计算完成了,那我们开始拿
shiftcls
。shiftcls
是在isa
里面的,上面已经说过了,所以进去看一下。
shiftcls
的前面有3位不是它的吧。因为iOS是小端啊,所以应该右面才是头,那就先右移3位,把不是shiftcls
的内容先去掉。
头上的3位不是
shiftcls
的内容去掉了,尾巴上还有17位不是shiftcls
的内容,而且刚才右移了3位,是不是应该把人家那3位找补回来才能对齐,所以这次左移17+3=20位
现在就只剩下shiftcls
的内容了,和刚才的JDMan
的内存做对比,是不是一样的?
那是不是就证明了isa
存储的shiftcls
就是我们的类。
2. 通过object_getClass
验证isa
关联了对象和类
我相信下面的这个runtime
中的函数,大家非常的熟悉。
object_getClass(对象);
我们通过一个对象找到这个对象的类,那么这是怎么找到的?还是通过isa
找到的。下面继续验证的确就是通过isa
找到的。
- 首先,从最直接的入手,就直接看
object_getClass
,即然说isa
关联了,那么源码里面必有体现。
Class object_getClass(id obj)
{
if (obj) return obj->getIsa();
else return Nil;
}
-
object_getClass
的源码中非常简单的一句,如果有obj
参数传进来,那么就返回obj->getIsa()
,那么就继续看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
,所以只看第一行就行了,因为就它是isa
。那么再进入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
}
这里走的是带掩码的return (Class)(isa.bits & ISA_MASK);
这里说下这个掩码是个什么,看英文:MASK,就是面具,面具有什么特性呢?就是脸上只能看到你的鼻子眼睛和嘴,别的地方你看不到。就是说,我想让你看到的地方你才可以看,不想让你看到的地方,你就看不到,那么就可以使用这个MASK来控制可以读取的地方。
- 这是很明显的拿
isa
的内存做了位运算,那么看一下这个ISA_MASK
多少:
# elif __x86_64__
# define ISA_MASK 0x00007ffffffffff8ULL
因为是用Mac模拟器,所以肯定用x86的。
- 那我们拿我们的目标——
JDMan
的内存,这次用lldb
打印16进制的内存。就不转换成2进制了。
- 然后我们继续拿
isa
的内存来看,根据return (Class)(isa.bits & ISA_MASK);
这句的要求,把isa
的内存和ISA_MASK
做与的位运算。
于是又被证明了isa
关联了对象与类,这个更直观。
三、结论
类和对象之间的关联,的确就是由isa
来完成的,对象中的isa
也是继承与类的isa
的。