和谐学习!不急不躁!!我是你们的老朋友小青龙~
在文章iOS之alloc底层实现分析 里提到过,alloc有一个核心方法_class_createInstanceFromZone,这个方法做了3件事情:
- instanceSize:申请要开辟的内存空间大小
- calloc:根据instanceSize返回值开辟内存空间
- initInstanceIsa:将类和开辟的空间进行isa绑定
今天,我们就针对第三点的isa,做进一步的分析。
找到_class_createInstanceFromZone方法里,绑定isa的代码段:
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);
}
一步步点击进入initInstanceIsa -> 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);
...
...
}
我们看到这里有个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
};
...
...
}
我们可以看到isa_t是一个联合体位域,那么为什么它后面要跟上_t呢,说到这里,我们先引出另一个概念:nonPointerIsa。
我们都知道,64位下一个isa占用8个字节(8*8=64位),但实际上存储类的内存地址,不需要这么多存储空间,所以苹果对这个情况做了优化,并且提出了TaggedPointer和NonpointerIsa的概念:
-
TaggedPointer可以用来将一些
小数据量的值,直接写在isa里面。因为我们知道,32位可以存储的数据位2^31(约21亿),而像NSDate、NSNumber,这些类型的数据,不太会超过20亿,如果分配内存来存储、读取它们,性能上来说不如存放在栈区来的更快。 补充:为什么是2的31次方,而不是32次方,因为最高位为符号位,1代表负,0代表正,所以这里31次方,结果是2147483648。 -
NonpointerIsa表示isa存储的不只是类的内存地址,还包含了其它信息,譬如:引用计数,析构状态,被其他 weak 变量引用情况。 -
我们要研究的就是
isa_t里位域bits的内容。为了增加可读性,苹果定义了ISA_BITFIELD,我们点开它(点不进去的同学,command+B先编译一下),可以看到这样一段代码:
# elif __x86_64__
# define ISA_MASK 0x00007ffffffffff8ULL
# define ISA_MAGIC_MASK 0x001f800000000001ULL
# define ISA_MAGIC_VALUE 0x001d800000000001ULL
# define ISA_HAS_CXX_DTOR_BIT 1
# 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 unused : 1; \
uintptr_t has_sidetable_rc : 1; \
uintptr_t extra_rc : 8
# define RC_ONE (1ULL<<56)
# define RC_HALF (1ULL<<7)
为了方便阅读,我这边附上解释:
- nonpointer //标识是否为nonpointer:0代表纯指针,1 代表不止是类对象地址,isa 中包含了类信息、对象的引用计数等。
- has_assoc //是否有关联对象
- has_cxx_dtor //是否有C++的一些操作,该对象是否有 C++ 或者 Objc 的析构器,如果有析构函数,则需要做析构逻辑, 如果没有,则可以更快的释放对象
- shiftcls //对象地址,存储类指针的值。开启指针优化的情况下,在 arm64 架构中有 33 位用来存储类指针
- magic //魔数,用于调试器判断当前对象是真的对象还是没有初始化的空间
- weakly_referenced //是否有弱引用,存储对象是否被指向或者曾经指向一个 ARC 的弱变量,
没有弱引用的对象可以更快释放。
- uintptr_t deallocating //是否正在释放,标志对象是否正在释放内存
- uintptr_t has_sidetable_rc //是否在sidetable中有存储引用计数,当对象引用技术大于 10 时,则需要借用该变量存储进位
- uintptr_t extra_rc //表示该对象的引用计数值,实际上是引用计数值减 1, 例如,如果对象的引用计数为 10,那么 extra_rc 为 9。如果引用计数大于 10, 则需要使用到下面的 has_sidetable_rc
接下来,我们写点案例来证明一下:
那么,怎么通过isa找到类的内存地址呢?这里需要加上一个面具ISA_MASK值:
# if __arm64__
// ARM64 simulators have a larger address space, so use the ARM64e
// scheme even when simulators build for ARM64-not-e.
# if __has_feature(ptrauth_calls) || TARGET_OS_SIMULATOR
# define ISA_MASK 0x007ffffffffffff8ULL
# define ISA_MAGIC_MASK 0x0000000000000001ULL
# define ISA_MAGIC_VALUE 0x0000000000000001ULL
# define ISA_HAS_CXX_DTOR_BIT 0
# define ISA_BITFIELD \
uintptr_t nonpointer : 1; ...
...
# endif
由此可以得到ISA_MASK为0x007ffffffffffff8ULL,再加上之前的isa地址:
由此可以得出结论:对象通过
isa+掩码,得到类的信息。
我们继续回到上面那个objc_object::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) {
//如果是纯isa,就进入这里
newisa.setClass(cls, this);
} else {
//如果是不纯的isa,就进入这里
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会直接走setClass方法,如果是不纯的isa则需要先进行位域赋值,然后再走setClass方法。接下来我们看看setClass方法都干了什么:
inline void
isa_t::setClass(Class newCls, UNUSED_WITHOUT_PTRAUTH objc_object *obj)
{
...
...
shiftcls_and_sig = signedCls >> 3;
#elif SUPPORT_INDEXED_ISA
// Indexed isa only uses this method to set a raw pointer class.
// Setting an indexed class is handled separately.
cls = newCls;
#else // Nonpointer isa, no ptrauth
shiftcls = (uintptr_t)newCls >> 3;
#endif
}
简单来说,就是通过isa的位运算,把不是shiftcls的部分给挤出去,缺失位置补0(前面在分析isa位域的时候知道了类的信息保存在uintptr_t shiftcls)。
由于我的手机连接的是iPhone11,所以架构是arm64,找到isa位域对应代码块:
# if __arm64__
// ARM64 simulators have a larger address space, so use the ARM64e
// scheme even when simulators build for ARM64-not-e.
# if __has_feature(ptrauth_calls) || TARGET_OS_SIMULATOR
# define ISA_MASK 0x007ffffffffffff8ULL
# define ISA_MAGIC_MASK 0x0000000000000001ULL
# define ISA_MAGIC_VALUE 0x0000000000000001ULL
# define ISA_HAS_CXX_DTOR_BIT 0
# define ISA_BITFIELD \
uintptr_t nonpointer : 1; \
uintptr_t has_assoc : 1; \
uintptr_t weakly_referenced : 1; \
uintptr_t shiftcls_and_sig : 52; \
uintptr_t has_sidetable_rc : 1; \
uintptr_t extra_rc : 8
# define RC_ONE (1ULL<<56)
# define RC_HALF (1ULL<<7)
isa内存地址
左移3位,右移12位,左移9位恢复原位,就可以得到类的内存地址,下面直接上代码:
至此,成功找到了类的信息。
补充:
通过控制台打印,可以看到用64位只保存内存地址,真的很浪费。