从 SF 慢慢把文章搬过来。。。html
segmentfault.com/a/119000001…git
本文以 NSNumber 为例,说明一个 Tagged Pointer 是怎样被建立出来的。github
int main(int argc, const char * argv[])
{
NSNumber *n = @3;
return 0;
}
复制代码
能够看到 n 并无 isa,它确实不是一个 OC 的对象。segmentfault
int main(int argc, const char * argv[])
{
// 将 alloc 和 init 拆开来看
NSNumber *n = [NSNumber alloc];
n = [n initWithInt:3];
return 0;
}
复制代码
有 架构
不对呀,若是每次 alloc 都会生成一个 NSPlaceholderNumber 对象,那还不如直接生成一个 NSNumber 对象呢,这哪有内存优化?app
一个直观的猜想是,屡次 alloc 返回的 NSPlaceholderNumber 是同一个实例。ide
int main(int argc, const char * argv[])
{
NSNumber *a = [NSNumber alloc];
NSNumber *b = [NSNumber alloc];
NSNumber *c = [NSNumber alloc];
NSNumber *d = [NSNumber alloc];
NSNumber *e = [NSNumber alloc];
NSNumber *f = [NSNumber alloc];
NSNumber *h = [NSNumber alloc];
return 0;
}
复制代码
结果 函数
[NSNumber alloc] 最终调用了 [NSNumber allocWithZone:]测试
NSNumber *__cdecl +[NSNumber allocWithZone:](NSNumber_meta *self, SEL a2, _NSZone *a3)
{
NSNumber *result; // rax
if ( (NSNumber_meta *)__NSNumberClass == self )
result = (NSNumber *)_NSPlaceholderValueOrNumber((__int64)&__NSNumberClass, 1);
else
result = (NSNumber *)NSAllocateObject(self, 0LL, a3);
return result;
}
复制代码
这里值得关注的是 _NSPlaceholderValueOrNumber(,)优化
__int64 __usercall _NSPlaceholderValueOrNumber@<rax>(__int64 a1@<rax>, char a2@<dil>)
{
__int64 result; // rax
void *v3; // rax
__int64 v4; // rcx
void *v5; // rax
__int64 v6; // rax
__int64 v7; // [rsp-8h] [rbp-10h]
// cpv,我猜是 const pointer value
v7 = a1;
result = _NSPlaceholderValueOrNumber_cpv;
if ( !_NSPlaceholderValueOrNumber_cpv )
{
v3 = _objc_msgSend(&OBJC_CLASS___NSPlaceholderValue, "self", v7);
result = NSAllocateObject(v3, 0LL, 0LL);
// _NSPlaceholderValueOrNumber_cpv 应该是个全局变量
// 也就是说 NSPlaceholderValue 实例只会有一个
_NSPlaceholderValueOrNumber_cpv = result;
}
//相应地 cpn 应该是 const pointer number..
v4 = _NSPlaceholderValueOrNumber_cpn;
if ( !_NSPlaceholderValueOrNumber_cpn )
{
v5 = _objc_msgSend(&OBJC_CLASS___NSPlaceholderNumber, "self", v7);
v6 = NSAllocateObject(v5, 0LL, 0LL);
v4 = v6;
// 同理 NSPlaceholderNumber 实例只会有一个
_NSPlaceholderValueOrNumber_cpn = v6;
result = _NSPlaceholderValueOrNumber_cpv;
}
if ( a2 ) //a2 为 true 则使用cpn,即返回 NSPlaceholderNumber 实例
{
result = v4;
}
// btw,这几兄弟的继承关系是这样的:NSPlaceholderNumber -> NSPlaceholderValue -> NSNumber -> NSValue
return result;
}
复制代码
原来是经过全局变量 _NSPlaceholderValueOrNumber_cpn 来作到始终返回一个 NSPlaceholderNumber 实例的。 代码中还有一个相似的全局变量 _NSPlaceholderValueOrNumber_cpv,用来返回 NSPlaceholderValue 实例。
int main(int argc, const char * argv[])
{
NSValue *v1 = [NSValue alloc];
NSValue *v2 = [NSValue alloc];
NSValue *v3 = [NSValue alloc];
NSNumber *n1 = [NSNumber alloc];
NSNumber *n2 = [NSNumber alloc];
NSNumber *n3 = [NSNumber alloc];
return 0;
}
复制代码
的确如此
那么 NSPlaceholderNumber 是如何最终返回一个 Tagged Pointer 的呢?
NSPlaceholderNumber 自己乏善可陈,[NSPlaceholderNumber initWithInt:]只是一层皮
NSPlaceholderNumber *__cdecl -[NSPlaceholderNumber initWithInt:](NSPlaceholderNumber *self, SEL a2, int a3)
{
int v4; // [rsp+Ch] [rbp-4h]
v4 = a3;
// 其它的 initWithXXX: 等方法也都是调用 CFNumberCreate,惟一的区别是指定的数据长度不一样
return (NSPlaceholderNumber *)CFNumberCreate(kCFAllocatorDefault, 3LL, &v4);
}
复制代码
真正的执行者是 CFNumberCreate,它的代码很长,这里只取生成 Tagged Pointer 的部分
// a1: 分配器类型,从NSPlaceholderNumber那传过来的是kCFAllocatorDefault
// a2: 数值长度
// a3: 数值
// a1: 分配器类型,从NSPlaceholderNumber那传过来的是kCFAllocatorDefault
// a2: 数值长度
// a3: 数值
unsigned __int64 __fastcall CFNumberCreate(__objc2_class **a1, __int64 a2, unsigned int *a3) {
// ...
if ( __CFTaggedNumberClass
&& (kCFAllocatorSystemDefault == v4
|| (!v4 || (__objc2_class **)kCFAllocatorDefault == v4)
&& kCFAllocatorSystemDefault == (__objc2_class **)CFAllocatorGetDefault())
&& __CFNumberCaching != 2 )
{
switch ( __CFNumberTypeTable[a2] & 0x1F )
{
case 1:
*(_QWORD *)&v5 = *(char *)v3;
return objc_debug_taggedpointer_obfuscator ^ (__CFNumberCanonicalTypeIndex[__CFNumberTypeTable[a2] & 7] | 16LL * *(_QWORD *)&v5 & 0xFFFFFFFFFFFFFF0LL | 0xB000000000000000LL);
case 2:
*(_QWORD *)&v5 = *(signed __int16 *)v3;
return objc_debug_taggedpointer_obfuscator ^ (__CFNumberCanonicalTypeIndex[__CFNumberTypeTable[a2] & 7] | 16LL * *(_QWORD *)&v5 & 0xFFFFFFFFFFFFFF0LL | 0xB000000000000000LL);
case 3:
*(_QWORD *)&v5 = (signed int)*v3;
return objc_debug_taggedpointer_obfuscator ^ (__CFNumberCanonicalTypeIndex[__CFNumberTypeTable[a2] & 7] | 16LL * *(_QWORD *)&v5 & 0xFFFFFFFFFFFFFF0LL | 0xB000000000000000LL);
case 4:
v5 = *(double *)v3;
goto LABEL_21;
case 5:
v6 = _mm_cvtsi32_si128(*v3);
*(_QWORD *)&v5 = (unsigned int)(signed int)*(float *)v6.m128i_i32;
if ( *(float *)v6.m128i_i32 != (float)SLODWORD(v5) )
goto LABEL_23;
v7 = *(_QWORD *)&v5 == 0LL;
v8 = _mm_cvtsi128_si32(v6) < 0;
break;
case 6:
*(_QWORD *)&v5 = (unsigned int)(signed int)*(double *)v3;
if ( *(double *)v3 != (double)SLODWORD(v5) )
goto LABEL_23;
v7 = *(_QWORD *)&v5 == 0LL;
v8 = *(_QWORD *)v3 < 0;
break;
default:
goto LABEL_23;
}
// ...
}
复制代码
这里最重要的调用无疑是 objc_debug_taggedpointer_obfuscator。 遗憾的是,在目前开源的 objc4-723 中的实现和 macOS 10.13 上的 /usr/lib/libobjc.A.dylib 中均没有找到这个函数。
做为代替,取 objc4-723 中构造 Tagged Pointer 的部分
// Create a tagged pointer object with the given tag and value.
// Assumes the tag is valid.
// Assumes tagged pointers are enabled.
// The value will be silently truncated to fit.
static inline void * _Nonnull
_objc_makeTaggedPointer(objc_tag_index_t tag, uintptr_t value)
{
// PAYLOAD_LSHIFT and PAYLOAD_RSHIFT are the payload extraction shifts.
// They are reversed here for payload insertion.
// assert(_objc_taggedPointersEnabled());
if (tag <= OBJC_TAG_Last60BitPayload) {
// assert(((value << _OBJC_TAG_PAYLOAD_RSHIFT) >> _OBJC_TAG_PAYLOAD_LSHIFT) == value);
return (void *)
(_OBJC_TAG_MASK |
((uintptr_t)tag << _OBJC_TAG_INDEX_SHIFT) |
((value << _OBJC_TAG_PAYLOAD_RSHIFT) >> _OBJC_TAG_PAYLOAD_LSHIFT));
} else {
// assert(tag >= OBJC_TAG_First52BitPayload);
// assert(tag <= OBJC_TAG_Last52BitPayload);
// assert(((value << _OBJC_TAG_EXT_PAYLOAD_RSHIFT) >> _OBJC_TAG_EXT_PAYLOAD_LSHIFT) == value);
return (void *)
(_OBJC_TAG_EXT_MASK |
((uintptr_t)(tag - OBJC_TAG_First52BitPayload) << _OBJC_TAG_EXT_INDEX_SHIFT) |
((value << _OBJC_TAG_EXT_PAYLOAD_RSHIFT) >> _OBJC_TAG_EXT_PAYLOAD_LSHIFT));
}
}
复制代码
经过位操做把 value 和标志位拼起来,最终返回的就是 _NSCFNumber。很直观,不过
这个 OBJC_TAG_Last60BitPayload 和 OBJC_TAG_First52BitPayload 是什么?
Apple 在 Advances in Objective-C 的说明中,是将地址的最后一位置为 1,来与正常的指针区分开的。
这里要说明一下,为何正常的指针,最后一位会都是 0 呢?
OC 中的 [NSObject alloc] 最终调用的是 C 标准库中的 malloc,它所返回的地址是 16 的整数。16 在二进制下最后 4 位都是 0,用最后一位是否为 1 来识别 Tagged Pointer 很是合理。
实际试验一下,打开 Xcode 的 GuardMalloc,能够看到 log
GuardMalloc[debug-objc-38327]: Allocations will be placed on 16 byte boundaries.
复制代码
剩下的 3 位,能够用于区分不一样类型的 Tagged Pointer,好比 NSTaggedPointerNumber、NSTaggedPointerDate 等等。在 objc4-723 中,有
#if __has_feature(objc_fixed_enum) || __cplusplus >= 201103L
enum objc_tag_index_t : uint16_t
#else
typedef uint16_t objc_tag_index_t;
enum
#endif
{
OBJC_TAG_NSAtom = 0,
OBJC_TAG_1 = 1,
OBJC_TAG_NSString = 2,
OBJC_TAG_NSNumber = 3,
OBJC_TAG_NSIndexPath = 4,
OBJC_TAG_NSManagedObjectID = 5,
OBJC_TAG_NSDate = 6,
OBJC_TAG_RESERVED_7 = 7,
// ...
};
复制代码
恰好 8 种。
目前,不论 是x86_64 仍是 ARM 64,都没有充分使用 64 位。就 ARM 64 而言,目前仅仅使用了后 48 位。 因此,Tagged Pointer 的标志位也是能够放在最前面的,即 MSB。
OC Runtime 实际上支持 4 种放置方式
#if __has_feature(objc_fixed_enum) || __cplusplus >= 201103L
enum objc_tag_index_t : uint16_t
#else
typedef uint16_t objc_tag_index_t;
enum
#endif
{
// ...
OBJC_TAG_First60BitPayload = 0,
OBJC_TAG_Last60BitPayload = 6,
OBJC_TAG_First52BitPayload = 8,
OBJC_TAG_Last52BitPayload = 263,
// ...
};
#if __has_feature(objc_fixed_enum) && !defined(__cplusplus)
typedef enum objc_tag_index_t objc_tag_index_t;
#endif
复制代码
相应地,mask 也不同
#if TARGET_OS_OSX && __x86_64__
// 64-bit Mac - tag bit is LSB
// macOS下,始终用最小位做为标志位
# define _OBJC_TAG_MASK 1UL
#else
// Everything else - tag bit is MSB
// 剩下的只有ARM64了。ARM64只用到64位中的48位,高位全是0,此时mask就要反过来。
// 这种状况下,理论上可以支持的Tagged Pointer的类就多了不少。
# define _OBJC_TAG_MASK (1UL<<63)
#endif
复制代码
顺带一提,__NSCFNumber 并非为 NSNumber 的Tagged Pointer 捏造出来的,而是实际存在的 NSNumber 类簇下的一个私有子类。只不过 OC Runtime 选择将它做为 NSNumber 的 Tagged Pointer 在 lldb 下的“画皮”。
能够这样来测试它的存在
Class class = NSClassFromString(@"__NSCFNumber");
复制代码