本文以NSNumber爲例,說明一個Tagged Pointer是怎樣被建立出來的。html
int main(int argc, const char * argv[]) { NSNumber *n = @3; return 0; }
能夠看到git
n並無isa,它確實不是一個OC的對象。github
進一步來看架構
int main(int argc, const char * argv[]) { // 1 NSNumber *n = [NSNumber alloc]; // 2 n = [n initWithInt:3]; return 0; }
[NSNumber alloc]返回的是NSPlaceholderNumber,有isa,這是個正常的OC對象。app
不對呀,若是每次alloc都會生成一個NSPlaceholderNumber對象,那還不如直接生成一個NSNumber對象呢,這哪有內存優化?
一個直觀的猜想是,屢次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_cpv和_NSPlaceholderValueOrNumber_cpn來作到始終返回一個NSPlaceholderNumber實例的。ui
既然有兩個全局變量,說明會有兩個實例嘍?
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自己乏善可陳[[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是什麼?
在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種。
目前,不管是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
順帶一提,__NSCFNumber並非爲NSNumber的Tagged Pointer捏造出來的,而是實際存在的NSNumber類簇下的一個私有子類。只不過runtime選擇將它做爲NSNumber的Tagged Pointer在lldb下的「畫皮」。
能夠這樣來測試它的存在
Class class = NSClassFromString(@"__NSCFNumber");