從 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;
}
複製代碼
有 架構
[NSNumber alloc] 返回的是NSPlaceholderNumber,有 isa,這是個正常的 OC 對象。不對呀,若是每次 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");
複製代碼