1. 簡介
Tagged Pointer是蘋果在64bit設(shè)備提出的一種存儲小對象的技術(shù),它具有以下特點
- Tagged Pointer指針的值不再是地址了,而是真正的值。所以,實際上它不再是一個對象了,它只是一個披著對象皮的普通變量而已。
- 它的內(nèi)存并不存儲在堆中,也不需要 malloc 和 free,不走引用計數(shù)那一套邏輯,由系統(tǒng)來處理釋放
- 在內(nèi)存讀取上有著 3 倍的效率,創(chuàng)建時比以前快 106 倍。
- 可以通過設(shè)置環(huán)境變量
OBJC_DISABLE_TAGGED_POINTERS來有開發(fā)者決定是否使用這項技術(shù)
2. 源碼
源碼是基于runtime的obj4-779.1版本
2.1 各種標(biāo)記位
#if (TARGET_OS_OSX || TARGET_OS_IOSMAC) && __x86_64__
// 64-bit Mac - tag bit is LSB
# define OBJC_MSB_TAGGED_POINTERS 0
#else
// Everything else - tag bit is MSB
# define OBJC_MSB_TAGGED_POINTERS 1 // 最高有效位
#endif
#define _OBJC_TAG_INDEX_MASK 0x7 // 0b111表示有擴展的標(biāo)記位,擴展標(biāo)記位占8位
// array slot includes the tag bit itself
#define _OBJC_TAG_SLOT_COUNT 16
#define _OBJC_TAG_SLOT_MASK 0xf // 0b1111 taggedpointer + 有擴展標(biāo)記位的mask
#define _OBJC_TAG_EXT_INDEX_MASK 0xff
// array slot has no extra bits
#define _OBJC_TAG_EXT_SLOT_COUNT 256 // 擴展標(biāo)記位能表示的個數(shù)
#define _OBJC_TAG_EXT_SLOT_MASK 0xff // 0b1111 1111
#if OBJC_MSB_TAGGED_POINTERS
# define _OBJC_TAG_MASK (1UL<<63) // 是否是tagged pointer的標(biāo)記位 1表示是 0表示不是
# define _OBJC_TAG_INDEX_SHIFT 60 // 基礎(chǔ)tag位的偏移,從2-4bit,結(jié)合_OBJC_TAG_INDEX_MASK來獲取到基礎(chǔ)tag的值
# define _OBJC_TAG_SLOT_SHIFT 60
# define _OBJC_TAG_PAYLOAD_LSHIFT 4 // LSHIFT和RSHIFT配合使用用來對數(shù)據(jù)移位混淆及恢復(fù)
# define _OBJC_TAG_PAYLOAD_RSHIFT 4
# define _OBJC_TAG_EXT_MASK (0xfUL<<60) // 1111 0000 ... 0000 0000 是否有擴展標(biāo)記位 tag位111表示有擴展標(biāo)記位
# define _OBJC_TAG_EXT_INDEX_SHIFT 52 // 擴展tag位的偏移,從5-12bit共8位,結(jié)合_OBJC_TAG_EXT_INDEX_MASK來獲取擴展tag的值
# define _OBJC_TAG_EXT_SLOT_SHIFT 52
# define _OBJC_TAG_EXT_PAYLOAD_LSHIFT 12
# define _OBJC_TAG_EXT_PAYLOAD_RSHIFT 12
#else
# define _OBJC_TAG_MASK 1UL
# define _OBJC_TAG_INDEX_SHIFT 1
# define _OBJC_TAG_SLOT_SHIFT 0
# define _OBJC_TAG_PAYLOAD_LSHIFT 0
# define _OBJC_TAG_PAYLOAD_RSHIFT 4
# define _OBJC_TAG_EXT_MASK 0xfUL
# define _OBJC_TAG_EXT_INDEX_SHIFT 4
# define _OBJC_TAG_EXT_SLOT_SHIFT 4
# define _OBJC_TAG_EXT_PAYLOAD_LSHIFT 0
# define _OBJC_TAG_EXT_PAYLOAD_RSHIFT 12
#endif
定義了很多位信息,我們需要關(guān)注的幾個:
- _OBJC_TAG_MASK :標(biāo)記位標(biāo)記該指針是否是tagged pointer
- _OBJC_TAG_INDEX_MASK :tag的值是7表示有擴展的tag位
- 其他的都是一些定義,用來通過位運算來獲取tag的值、ext tag的值的mask以及一些其他的左移右移位
2.2 如何判斷是tagged pointer
我們知道有一個標(biāo)記位來標(biāo)識指針是否是tagged pointer的
static inline bool
_objc_isTaggedPointer(const void * _Nullable ptr)
{
return ((uintptr_t)ptr & _OBJC_TAG_MASK) == _OBJC_TAG_MASK;
}
通過位運算獲取標(biāo)識位的值來確定是否是tagged pointer;需要留意的是不同的架構(gòu)標(biāo)記位不太一樣,有的是用最低位、有的使用最高位。
2.3 系統(tǒng)對tagged pointer的加密
在iOS12系統(tǒng)之前,我們發(fā)現(xiàn)是可以直接打印tagged pointer的值的,可讀性非常好,但是12之后再打印就發(fā)現(xiàn)完全看不懂了。
- (void)testCase {
NSString *stringWithFormat1 = [NSString stringWithFormat:@"y"];
[self formatedLogObject:stringWithFormat1];
}
- (void)formatedLogObject:(id)object {
if (@available(iOS 12.0, *)) {
NSLog(@"%p %@ %@", object, object, object_getClass(object));
} else {
NSLog(@"0x%6lx %@ %@", object, object, object_getClass(object));
}
}
上面的測試代碼,在12之前輸出:
0x79是ASCII對應(yīng)的y字符的值
0xa000000000000791 y NSTaggedPointerString
iOS12之后輸出:
0xcb47b8d98a2fa15f y NSTaggedPointerString
iOS12之前打印指針的值能很清晰的看到數(shù)據(jù)等信息,iOS12之后系統(tǒng)則打印的完全看不懂了,看了源代碼發(fā)現(xiàn)蘋果是做了混淆,讓我們不能直接得到值,從而避免我們?nèi)ズ苋菀拙蛡卧斐鲆粋€tagged pointer對象
蘋果是如何混淆的了
第一步:位移運算,右左橫移
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);
uintptr_t result =
(_OBJC_TAG_MASK |
((uintptr_t)tag << _OBJC_TAG_INDEX_SHIFT) |
((value << _OBJC_TAG_PAYLOAD_RSHIFT) >> _OBJC_TAG_PAYLOAD_LSHIFT));
return _objc_encodeTaggedPointer(result);
} else {
// ASSERT(tag >= OBJC_TAG_First52BitPayload);
// ASSERT(tag <= OBJC_TAG_Last52BitPayload);
// ASSERT(((value << _OBJC_TAG_EXT_PAYLOAD_RSHIFT) >> _OBJC_TAG_EXT_PAYLOAD_LSHIFT) == value);
uintptr_t result =
(_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));
return _objc_encodeTaggedPointer(result);
}
}
第二步,再來一個隨機數(shù)異或運算一下
static inline void * _Nonnull
_objc_encodeTaggedPointer(uintptr_t ptr)
{
return (void *)(objc_debug_taggedpointer_obfuscator ^ ptr); // objc_debug_taggedpointer_obfuscator 通過異或混淆一下指針的值
}
這個隨機數(shù)是在dyld加載image的時候去隨機產(chǎn)生的,每次程序加載都不一樣
// map_images -- map_images_nolock -- _read_images -- initializeTaggedPointerObfuscator
static void
initializeTaggedPointerObfuscator(void)
{
// 初始化一個taggedpointer的掩碼,每次_objc_init的時候都生成一個
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.
// 隨機一個掩碼,然后再做運算;基本上每次app啟動獲取到的都是不一樣的
arc4random_buf(&objc_debug_taggedpointer_obfuscator,
sizeof(objc_debug_taggedpointer_obfuscator));
objc_debug_taggedpointer_obfuscator &= ~_OBJC_TAG_MASK;
}
}
簡直喪心病狂啊,嘗試去hook一下,將這個值拿到或者修改成一個固定的值來好debug,最終沒搞成。
混淆之后怎么解密了
正常就是按照上面的混淆的方式,反向操作一波
第一步,異或那個固定的隨機數(shù),得到值
static inline uintptr_t
_objc_decodeTaggedPointer(const void * _Nullable ptr)
{
return (uintptr_t)ptr ^ objc_debug_taggedpointer_obfuscator; // 再次異或就得到了原始的值
}
這里舉個例子,就一目了然了;可以看到encode的時候異或一次,在decode的時候再異或一次就得到原始值了。
// 假設(shè) objc_debug_taggedpointer_obfuscator位 0010、原始數(shù)據(jù)是1001
encode:1001 ^ 0010 = 1011
decode:1011 ^ 0010 = 1001
第二步,再左右橫移回去
static inline uintptr_t
_objc_getTaggedPointerValue(const void * _Nullable ptr)
{
// ASSERT(_objc_isTaggedPointer(ptr));
uintptr_t value = _objc_decodeTaggedPointer(ptr);
uintptr_t basicTag = (value >> _OBJC_TAG_INDEX_SHIFT) & _OBJC_TAG_INDEX_MASK;
if (basicTag == _OBJC_TAG_INDEX_MASK) {
return (value << _OBJC_TAG_EXT_PAYLOAD_LSHIFT) >> _OBJC_TAG_EXT_PAYLOAD_RSHIFT;
} else {
return (value << _OBJC_TAG_PAYLOAD_LSHIFT) >> _OBJC_TAG_PAYLOAD_RSHIFT;
}
}
static inline intptr_t
_objc_getTaggedPointerSignedValue(const void * _Nullable ptr)
{
// ASSERT(_objc_isTaggedPointer(ptr));
uintptr_t value = _objc_decodeTaggedPointer(ptr);
uintptr_t basicTag = (value >> _OBJC_TAG_INDEX_SHIFT) & _OBJC_TAG_INDEX_MASK;
if (basicTag == _OBJC_TAG_INDEX_MASK) {
return ((intptr_t)value << _OBJC_TAG_EXT_PAYLOAD_LSHIFT) >> _OBJC_TAG_EXT_PAYLOAD_RSHIFT;
} else {
return ((intptr_t)value << _OBJC_TAG_PAYLOAD_LSHIFT) >> _OBJC_TAG_PAYLOAD_RSHIFT;
}
}
2.4 Tagged Pointer對象
系統(tǒng)通過3bit的標(biāo)記位來標(biāo)識tagged pointer對象的類,它的定義在objc_tag_index_t中
比如2表示是NSString、6表示是NSDate,我們知道3bit能表示的最大值是7,這個7系統(tǒng)用來預(yù)留,用來標(biāo)記是否有額外的標(biāo)記位,這樣就能支持更多的類支持tagged pointer
#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
{
// 60-bit payloads
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,
// 60-bit reserved
OBJC_TAG_RESERVED_7 = 7,
// 52-bit payloads
OBJC_TAG_Photos_1 = 8,
OBJC_TAG_Photos_2 = 9,
OBJC_TAG_Photos_3 = 10,
OBJC_TAG_Photos_4 = 11,
OBJC_TAG_XPC_1 = 12,
OBJC_TAG_XPC_2 = 13,
OBJC_TAG_XPC_3 = 14,
OBJC_TAG_XPC_4 = 15,
OBJC_TAG_NSColor = 16,
OBJC_TAG_UIColor = 17,
OBJC_TAG_CGColor = 18,
OBJC_TAG_NSIndexSet = 19,
OBJC_TAG_First60BitPayload = 0,
OBJC_TAG_Last60BitPayload = 6,
OBJC_TAG_First52BitPayload = 8,
OBJC_TAG_Last52BitPayload = 263,
OBJC_TAG_RESERVED_264 = 264
};
#if __has_feature(objc_fixed_enum) && !defined(__cplusplus)
typedef enum objc_tag_index_t objc_tag_index_t;
#endif
3. 延伸
3.1 字符串編碼
看博客發(fā)現(xiàn)很多都說字符在長度超過10的時候就不再是NSTaggedPointerString了,其實這種說法是不準(zhǔn)確的;
TaggedPointer用來表示值的位數(shù)payload是固定的,但是采用不同的字符編碼格式就能表示的位數(shù)也就不一樣了;
下面的測試?yán)樱?1位的字符串它也是TaggedPointer
NSString *test2 = [NSString stringWithFormat:@"%@", @"11111111111"];
NSLog(@"%p value:0x%lx %@ %@", test2, _objc_getTaggedPointerValue((__bridge const void *)test2), test2, object_getClass(test2));
輸出結(jié)果:
0xfa976f93575a8e75 value:0x7bdef7bdef7bdeb 11111111111 NSTaggedPointerString
關(guān)于字符串的編碼這一塊,還是挺復(fù)雜的,感興趣的可以參照這篇博客學(xué)習(xí)一下系統(tǒng)是如何在創(chuàng)建一個字符串的時候去編碼的
Tagged Pointer Strings by Mike Ash
Thus we can see that the structure of the tagged pointer strings is:
If the length is between 0 and 7, store the string as raw eight-bit characters.
If the length is 8 or 9, store the string in a six-bit encoding, using the alphabet "eilotrm.apdnsIc ufkMShjTRxgC4013bDNvwyUL2O856P-B79AFKEWV_zGJ/HYX".
If the length is 10 or 11, store the string in a five-bit encoding, using the alphabet "eilotrm.apdnsIc ufkMShjTRxgC4013"
摘自Tagged Pointer Strings by Mike Ash
3.2 iOS14對tagged pointer的優(yōu)化
在intel中tagged pointer指針的數(shù)據(jù)結(jié)構(gòu)如下:

- 通過最低位來標(biāo)記是否是tagged pointer
- 3位tag來標(biāo)記數(shù)據(jù)的class
- 擴展的8位來表示更多的類型的tagged pointer
在arm中tagged pointer指針的數(shù)據(jù)結(jié)構(gòu)如下:
iOS13系統(tǒng):

為什么要做這樣的改變了?

主要是針對objc_msgSend的調(diào)用的優(yōu)化,在intel的結(jié)構(gòu)下,判斷tagged pointer和nil需要分2個分支去判斷;而如果將最高位設(shè)置位1了,那么只需要做一次判斷就可以判斷對象是否是正常指針了。
if (ptrValue <= 0) // is tagged or nil其他情況就是正常的指針
iOS14:
iOS14跟iOS13的區(qū)別則將tag放在了最后三位;tagged pointer的標(biāo)記位還是保持在最高位

為什么要這樣做了?
蘋果的解釋是:
- ARM有個特性就是dyld會忽略指針的前8bit(這是由于ARM的Top Byte Ignore特性);
- 這樣布局數(shù)據(jù)之后,圖中的payload就跟普通指針的payload是一毛一樣了,也就是tagged pointer的payload(有效負載位)有包含一個正常的指針的能力;
- 這使得tagged pointer具備了引用二進制文件中的常量數(shù)據(jù)的能力,例如字符串或其他數(shù)據(jù)結(jié)構(gòu),這減少了dirty memory的使用
看到這里我只能說蘋果牛逼666