id类型源码解析
iOS中任何变量都有明确定义属于哪种类型,对象指针也是如此,属于id类型。id其实是结构体struct objc_object
类型的指针.
struct objc_object {
Class _Nonnull isa OBJC_ISA_AVAILABILITY;
};
/// A pointer to an instance of a class.
typedef struct objc_object *id;
复制代码
而结构体struct objc_object
仅仅有一个Class类型的成员变量isa
。Class又是struct objc_class
结构体类型。 所以,id
就是指向struct objc_object {objc_class *isa}
。
typedef struct objc_class *Class;
struct objc_class {
Class _Nonnull isa OBJC_ISA_AVAILABILITY;
#if !__OBJC2__
Class _Nullable super_class OBJC2_UNAVAILABLE;
const char * _Nonnull name OBJC2_UNAVAILABLE;
long version OBJC2_UNAVAILABLE;
long info OBJC2_UNAVAILABLE;
long instance_size OBJC2_UNAVAILABLE;
struct objc_ivar_list * _Nullable ivars OBJC2_UNAVAILABLE;
struct objc_method_list * _Nullable * _Nullable methodLists OBJC2_UNAVAILABLE;
struct objc_cache * _Nonnull cache OBJC2_UNAVAILABLE;
struct objc_protocol_list * _Nullable protocols OBJC2_UNAVAILABLE;
#endif
} OBJC2_UNAVAILABLE;
复制代码
到此为止,我们无法再深入理解下面的具体结构,因为Xcode中仅仅开放了objc的9个头文件,并且代码太过老旧。我们需要从苹果的opensource下载一份比较新的来看,不幸的是苹果提供的源码因为缺少依赖和一些头文件,无法编译成功,这里有一份Github上可以编译和调试的源码OjbcDeubg。本文后续的objc源码全部基于objc750.1版本。
我们可以看到struct objc_object中isa已经变为isa_t
类型,并且是私有成员不能从外部直接访问。
struct objc_object {
private:
isa_t isa;
public:
// ISA() assumes this is NOT a tagged pointer object
Class ISA();
// getIsa() allows this to be a tagged pointer object
Class getIsa();
......
}
复制代码
isa_t
的结构类型是一个联合体,联合体是把几种不同类型的变量存放到同一段内存单元中。也就是使用覆盖技术,几个变量互相覆盖。这种几个不同的变量共同占用一段内存的结构。所以isa_t
的成员变量可能是Class cls;
,也可能是uintptr_t bits;
,也可能是struct { ISA_BITFIELD; // defined in isa.h };
。
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
};
# 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
复制代码
所以,从现在来看,struct objc_objectd中的isa并不一定指向的是Class,可能是一个无符号整数,也可能是一个结构体位域,关于结构体位域的相关知识可以参考这篇文章。 我们之前有听说过对象的指针指向类类型么?现在证明这个结论是错误的。对象指针可能是指向类类型,也可能仅仅是一个无符号整数,或是一个结构体位域。
如何不浪费64bit,用好isa的内存空间?
在2013年9月,苹果推出了iPhone5s配备了 64 位架构的处理器。64位CPU下,指针所占用的位数为8个字节64位。一个内存地址实际上用不了64位去存储,一般32位即可存储一个20亿的数(2^31=2147483648,另外1位作为符号位)。所以,苹果把isa根据需要进行了区分,苹果提出了TaggedPointer和NonpointerIsa。对于小对象采用TaggedPointet方式来存放其值。对于占用内存比较大的对象采用NonpointerIsa来把isa按位使用,一部分用来存放实际的对象地址,一部分存放附加的其他信息。
TaggedPoint
对于NSDate
、NSNumber
这样的小对象存储的值,绝大多数情况并不会大于20亿这个量级。如果采用指针、堆内存的方式,那势必会造成内存的浪费和性能损耗。苹果采用将value值直接存储在isa_t
中的uintptr_t bits;
上,并且用一些特殊标识来标明此isa是TaggedPoint类型的。这样用isa就存储了值,而不需要在堆上分配内存再去存储值。要知道堆内存的分配、释放及访问,要比栈内存慢很多的。
NonpointerIsa
isa其实并不单单是一个指针,实际上只有33位用于存储对象地址。其余位用来存储一些特殊的值。
uintptr_t nonpointer : 1; //标识是否为nonpointer
uintptr_t has_assoc : 1; //是否有关联对象
uintptr_t has_cxx_dtor : 1; //是否有C++的一些操作
uintptr_t shiftcls : 33; //对象地址
uintptr_t magic : 6; //魔数
uintptr_t weakly_referenced : 1; //是否有弱引用
uintptr_t deallocating : 1; //是否正在释放
uintptr_t has_sidetable_rc : 1; //是否在sidetable中有存储引用计数
uintptr_t extra_rc : 19 //在当前isa中存储的引用计数值
复制代码
强调一点,很多教程讲MRC中引用计数是通过sidetable来存储,但其实isa_t中已经拿出了19位来存储对象的引用计数,这个引用计数的值已足够大,大多数情况19位已经够存储其引用计数值,所以并不需要使用sidetable来存储额外超过的引用计数值。
源码解读TaggedPoint
inline Class objc_object::getIsa()
{
if (!isTaggedPointer()) return ISA();
uintptr_t ptr = (uintptr_t)this;
if (isExtTaggedPointer()) {
uintptr_t slot =
(ptr >> _OBJC_TAG_EXT_SLOT_SHIFT) & _OBJC_TAG_EXT_SLOT_MASK;
return objc_tag_ext_classes[slot];
} else {
uintptr_t slot =
(ptr >> _OBJC_TAG_SLOT_SHIFT) & _OBJC_TAG_SLOT_MASK;
return objc_tag_classes[slot];
}
}
复制代码
现在的iOS已经无法直接访问obj_object
的isa,在获取isa
的时候需要调用getIsa()
方法。此方法中首先判断是否为taggedPonter,如果不是则调用ISA()
方法,是的话则判断是否为ExtTaggedPointer
,然后再按位与操作得到class类型。所有支持TaggedPointer的class被保存在全局数据变量OBJC_EXPORT Class _Nullable objc_debug_taggedpointer_ext_classes[]
中。
在判断是否为ExtTaggedPointer
时,调用了_objc_decodeTaggedPointer
方法,在要获取slot
时要先让ptr
与objc_debug_taggedpointer_obfuscator
进行按位异或操作,然后拿异或后的值与_OBJC_TAG_EXT_MASK
进行与操作来判断是否是ExtTaggedPointer
。
_objc_decodeTaggedPointer(const void * _Nullable ptr)
{
return (uintptr_t)ptr ^ objc_debug_taggedpointer_obfuscator;
}
复制代码
网上有很多教程是这样的
int main(int argc, const char * argv[]) {
@autoreleasepool {
// insert code here...
int a = 0;
NSObject *obj = [[NSObject alloc] init];
NSNumber *num0 = @0;
NSNumber *num1 = @1;
NSNumber *num2 = @2;
NSNumber *num3 = @8;
NSNumber *num4 = @0xFFFFFFFFFFFF;
NSLog(@"%p, %p, %p, %p, %p", num0, num1, num2, num3, num4);
}
return 0;
}
复制代码
因为num0、num1、num2、num3
的值没有超过32位的存储空间,所以通过打印其地址可以看到0,1,2,3
的值是直接保存到地址里的。但遗憾的是,大家试一下发现都是错误的。
0x59aa0169f2f5214b, 0x59aa0169f2f5204b, 0x59aa0169f2f5234b, 0x59aa0169f2f5294b, 0x5955fe960d0ade5b
复制代码
从这段输出中,并不能发现什么规律。
就是因为上文提到了objc_debug_taggedpointer_obfuscator
,苹果对TaggedPointer的值进行了按位异或混淆。在ios应用启动装载的过程中装载objc时会调用到initializeTaggedPointerObfuscator
中,这里生成了一个随机数objc_debug_taggedpointer_obfuscator
,所有的TaggedPointer都用此随机数进行了混淆,所以我们看不出规律来。
static void
initializeTaggedPointerObfuscator(void)
{
if (sdkIsOlderThan(10_14, 12_0, 12_0, 5_0, 3_0) ||
// Set the obfuscator to zero for apps linked against older SDKs,
// in case they're relying on the tagged pointer representation.
DisableTaggedPointerObfuscation) {
objc_debug_taggedpointer_obfuscator = 0;
} else {
// Pull random data into the variable, then shift away all non-payload bits.
arc4random_buf(&objc_debug_taggedpointer_obfuscator,
sizeof(objc_debug_taggedpointer_obfuscator));
objc_debug_taggedpointer_obfuscator &= ~_OBJC_TAG_MASK;
}
}
复制代码
幸运的是,我们拿到了源代码。所以可以把此随机数改为0。
static void
initializeTaggedPointerObfuscator(void)
{
if (sdkIsOlderThan(10_14, 12_0, 12_0, 5_0, 3_0) ||
// Set the obfuscator to zero for apps linked against older SDKs,
// in case they're relying on the tagged pointer representation.
DisableTaggedPointerObfuscation) {
objc_debug_taggedpointer_obfuscator = 0;
} else {
// Pull random data into the variable, then shift away all non-payload bits.
arc4random_buf(&objc_debug_taggedpointer_obfuscator,
sizeof(objc_debug_taggedpointer_obfuscator));
objc_debug_taggedpointer_obfuscator &= ~_OBJC_TAG_MASK;
}
#warning write by test
objc_debug_taggedpointer_obfuscator = 0;
objc_debug_taggedpointer_obfuscator &= ~_OBJC_TAG_MASK;
}
复制代码
然后我们在run一次代码,执行结果如下
0x27, 0x127, 0x227, 0x827, 0xffffffffffff37
复制代码
第一位的0x27
等于0x027
,这样我们才真切的感受到TaggedPointer真的是将value值直接保存到了isa中。
源码解读NonpointerIsa
MRC时代,我们是可以直接获取引用计数的。ARC下无法直接获取,但NSObject中还是对我们暴露了retainCount
函数,我们去追踪retainCount
做了哪些事情。
inline uintptr_t
objc_object::rootRetainCount()
{
if (isTaggedPointer()) return (uintptr_t)this;
sidetable_lock();
isa_t bits = LoadExclusive(&isa.bits);
ClearExclusive(&isa.bits);
if (bits.nonpointer) {
uintptr_t rc = 1 + bits.extra_rc;
if (bits.has_sidetable_rc) {
rc += sidetable_getExtraRC_nolock();
}
sidetable_unlock();
return rc;
}
sidetable_unlock();
return sidetable_retainCount();
}
复制代码
retainCount
首先判断是否为taggeePointer
,如果是则直接返回isa
,不是的话判断是否为nonpointer
,是的话,要先从bits
中即我们上文提到的结构体位域中取出extra_rc
中存储的引用计数,然后根据bits
中的has_sidetable_rc
来断定是否有sidetable中存储该对象的引用计数。如果有则加上sidetable中存储的引用计数,无则直接返回bits.extra_rc
。
至此,我们也分析出nonPointerISA中,isa并不是一个纯粹的指向class,而是还携带了一些附加信息。
总结
iOS的内存管理中,并不是仅仅用到散列表SideTables来存储引用计数,而是针对不同的对象及CPU位数做了特殊的事情。在64位CPU下,有TaggedPointer来存储小对象,有NonPonterISA这种非指针型isa来管理引用计数等,还有散列表来存储超过范围的引用计数和弱引用关系。