TL.DR
本文以NSNumber为例,说明一个Tagged Pointer是怎样被创建出来的。
从代码开始
没有isa
int main(int argc, const char * argv[])
{
NSNumber *n = @3;
return 0;
}
可以看到
n并没有isa,它确实不是一个OC的对象。
NSPlaceholderNumber
进一步来看
int main(int argc, const char * argv[])
{
// 1
NSNumber *n = [NSNumber alloc];
// 2
n = [n initWithInt:3];
return 0;
}
[NSNumber alloc]返回的是NSPlaceholderNumber,有isa,这是个正常的OC对象。
不对呀,如果每次alloc都会生成一个NSPlaceholderNumber对象,那还不如直接生成一个NSNumber对象呢,这哪有内存优化?
一个直观的猜测是,多次alloc返回的NSPlaceholderNumber是同一个实例。
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;
}
结果
NSPlaceholderNumber是哪来的
[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_cpv和_NSPlaceholderValueOrNumber_cpn来做到始终返回一个NSPlaceholderNumber实例的。
既然有两个全局变量,说明会有两个实例喽?
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;
}
的确如此
_NSCFNumber,最终的Tagged Pointer
NSPlaceholderNumber本身乏善可陈[[NSPlaceholderNumber initWithInt:]](https://github.com/NSFish/Pri...
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是什么?
LSB
在Advances in Objective-C中,是将地址的最后一位置为1,来与正常的指针区分开。
这里要说明一下,为什么正常的指针,最后一位会都是0呢?
OC中的[NSObject alloc]最终调用的是C标准库中的malloc,它所返回的地址通是16的整数。
16在二进制下最后4位都是0,用最后一位是否为1来识别Tagged Pointer非常合理。
实际试验一下,打开Xcode的GuardMalloc,可以看到Console的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种。
MSB
目前,不论是x86_64还是ARM 64,都没有充分使用64位。就ARM 64而言,目前仅仅使用了后48位。
所以,Tagged Pointer的标志位也是可以放在最前面的,即MSB。
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
回顾一下整个过程
- [NSNumber alloc]返回NSPlaceholderNumber
- [NSPlaceholderNumber initWithXXX:]调用了CFNumberCreate
- CFNumberCreate调用了objc_debug_taggedpointer_obfuscator(或_objc_makeTaggedPointer)
- _objc_makeTaggedPointer根据运行设备的架构,拼接出一个地址并返回
顺带一提,__NSCFNumber并不是为NSNumber的Tagged Pointer捏造出来的,而是实际存在的NSNumber类簇下的一个私有子类。只不过runtime选择将它作为NSNumber的Tagged Pointer在lldb下的“画皮”。
可以这样来测试它的存在
Class class = NSClassFromString(@"__NSCFNumber");
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。