一、環(huán)境介紹
-
mac版本:Mac Mojave 10.14 -
objc版本:objc runtime 750
二、為什么要使用TaggedPointer?
以前我們初始化一個對象(64位為例),開發(fā)的代碼如下
NSNumber *number2 = [NSNumber numberWithInteger:2];
此時的內(nèi)存圖如下

可以看到我就想存一個2用掉了24個字節(jié),由于我們的NSNumber和NSDate對象的值一般不需要8個字節(jié),4個字節(jié)的長度2^31=2147483648可以表達的數(shù)量已經(jīng)達到了20多億了,為了不造成內(nèi)存的浪費,想到將指針的值(8個字節(jié))進行拆分,一部分表示數(shù)據(jù),一部分用來表示是一個特殊的指針,他不執(zhí)行任何對象,這就是TaggedPointer技術(shù),這樣指針 = Data + Tag,那么我們的存一個數(shù)字只需要8個字節(jié)就夠了。
三、一個簡單的例子
3.1 版本新特性
NSNumber *number1 = @1;
NSNumber *number2 = @2;
NSNumber *number3 = @3;
NSNumber *numberFFFF = @(0xFFFF);
NSLog(@"number1 pointer is %p", number1);
NSLog(@"number2 pointer is %p", number2);
NSLog(@"number3 pointer is %p", number3);
NSLog(@"numberffff pointer is %p", numberFFFF);
輸出結(jié)果卻是這個樣子的
number1 pointer is 0x19ec25e574ba1459
number2 pointer is 0x19ec25e574ba1759
number3 pointer is 0x19ec25e574ba1659
numberffff pointer is 0x19ec25e57445ea59
這個地址有點特殊,研究了一下,發(fā)現(xiàn)原來是在10_14以后蘋果對TaggedPointer進行了混淆,文件objc-runtime-new.m寫到
static void
initializeTaggedPointerObfuscator(void)
{
if (sdkIsOlderThan(10_14, 12_0, 12_0, 5_0, 3_0) ||
DisableTaggedPointerObfuscation) {
objc_debug_taggedpointer_obfuscator = 0;
} else {
arc4random_buf(&objc_debug_taggedpointer_obfuscator,
sizeof(objc_debug_taggedpointer_obfuscator));
objc_debug_taggedpointer_obfuscator &= ~_OBJC_TAG_MASK;
}
}
混淆的代碼也很簡單,類似這種加入加密前的數(shù)據(jù)是a,加密后的數(shù)據(jù)為b,
那么:
加密:b = a ^ objc_debug_taggedpointer_obfuscator,
解密: a = b ^ objc_debug_taggedpointer_obfuscator.
這里利用了異或的特性,源碼如下:
static inline void * _Nonnull
_objc_encodeTaggedPointer(uintptr_t ptr)
{
return (void *)(objc_debug_taggedpointer_obfuscator ^ ptr);
}
static inline uintptr_t
_objc_decodeTaggedPointer(const void * _Nullable ptr)
{
return (uintptr_t)ptr ^ objc_debug_taggedpointer_obfuscator;
}
所以要想知道0x19ec25e574ba1459是什么意思,還是要知道objc_debug_taggedpointer_obfuscator值,這是個隨機值,要想獲取這個值:
方法一:通過斷點來獲取

通過lldb指令讀取
(lldb) p/x objc_debug_taggedpointer_obfuscator
(uintptr_t) $0 = 0x19ec25e574ba157e
方法二: 看來runtime源碼知道objc_debug_taggedpointer_obfuscator是個全局變量,只要在我們用的地方申明一下即可
extern uintptr_t objc_debug_taggedpointer_obfuscator;
通過NSLog打印就可以了
NSLog(@"%lx",objc_debug_taggedpointer_obfuscator);
為了方便查看,簡單寫了一個方法,用來解開混淆
uintptr_t _objc_decodeTaggedPointer_(id ptr) {
NSString *p = [NSString stringWithFormat:@"%ld",ptr];
return [p longLongValue] ^ objc_debug_taggedpointer_obfuscator;
}
3.2 真實的地址
NSNumber *number1 = @1;
NSNumber *number2 = @2;
NSNumber *number3 = @3;
NSNumber *numberFFFF = @(0xFFFF);
NSLog(@"number1 pointer is %p---真實地址:==0x%lx", number1,_objc_decodeTaggedPointer_(number1));
NSLog(@"number2 pointer is %p---真實地址:==0x%lx", number2,_objc_decodeTaggedPointer_(number2));
NSLog(@"number3 pointer is %p---真實地址:==0x%lx", number3,_objc_decodeTaggedPointer_(number3));
NSLog(@"numberffff pointer is %p---真實地址:==0x%lx", numberFFFF,_objc_decodeTaggedPointer_(numberFFFF));
輸出
number1 pointer is 0xfda27e12be89be71---真實地址:==0x127
number2 pointer is 0xfda27e12be89bd71---真實地址:==0x227
number3 pointer is 0xfda27e12be89bc71---真實地址:==0x327
numberffff pointer is 0xfda27e12be764071---真實地址:==0xffff27
會發(fā)現(xiàn),不管運行多少次,都是以27結(jié)尾,我們有理由相信,蘋果貢獻了1個字節(jié)(8個bit)來標(biāo)識這是個特殊的指針,最后1個字節(jié)用來標(biāo)識,這個類指針,判斷是否是TaggedPointer不同平臺判斷的方式不一樣,但對我們理解根本不影響
static inline bool
_objc_isTaggedPointer(const void * _Nullable ptr)
{
return ((uintptr_t)ptr & _OBJC_TAG_MASK) == _OBJC_TAG_MASK;
}
-
mac平臺最后一個為1; -
iPhone和模擬器,為最高位是1。
那么剩下的7個字節(jié)是不是都用來存放數(shù)據(jù)呢?
3.3 TaggedPointer存儲的數(shù)字的最大值
NSNumber *numberF13 = @(0xFFFFFFFFFFFFF);
NSNumber *numberF13_1 = @(0x1FFFFFFFFFFFFF);
NSNumber *numberF13_3 = @(0x3FFFFFFFFFFFFF);
NSNumber *numberF13_7 = @(0x7FFFFFFFFFFFFF);
NSNumber *numberF14 = @(0xFFFFFFFFFFFFFF);
NSLog(@"numberF13 pointer is %p---真實地址:==0x%lx", numberF13,_objc_decodeTaggedPointer_(numberF13));
NSLog(@"numberF13_1 pointer is %p---真實地址:==0x%lx", numberF13_1,_objc_decodeTaggedPointer_(numberF13_1));
NSLog(@"numberF13_3 pointer is %p---真實地址:==0x%lx", numberF13_3,_objc_decodeTaggedPointer_(numberF13_3));
NSLog(@"numberF13_7 pointer is %p---真實地址:==0x%lx", numberF13_7,_objc_decodeTaggedPointer_(numberF13_7));
NSLog(@"numberF14 pointer is %p---真實地址:==0x%lx", numberF14,_objc_decodeTaggedPointer_(numberF14));
輸出如下
number1 pointer is 0x20f9850034a2e631---真實地址:==0x127
number2 pointer is 0x20f9850034a2e531---真實地址:==0x227
number3 pointer is 0x20f9850034a2e431---真實地址:==0x327
numberffff pointer is 0x20f98500345d1831---真實地址:==0xffff27
numberF13 pointer is 0x2f067affcb5d1821---真實地址:==0xfffffffffffff37
numberF13_1 pointer is 0x3f067affcb5d1821---真實地址:==0x1fffffffffffff37
numberF13_3 pointer is 0x1f067affcb5d1821---真實地址:==0x3fffffffffffff37
numberF13_7 pointer is 0x5f067affcb5d1821---真實地址:==0x7fffffffffffff37
numberF14 pointer is 0x102500210
從輸出可以看出,到numberF14地址已經(jīng)是真正的oc對象的地址了,說明有效存儲位置有56位,所以TaggedPointer所能表達的數(shù)字范圍為[0 2^65)。
四、思考:你會如何實現(xiàn)NSString的TaggedPointer?
我們現(xiàn)在想做的事情就是如何利用指針來存儲我們的字符數(shù)據(jù),而指針的大小就是8個字節(jié),一共64位,如何利用這個64位呢?由NSNumber的靈感,可以使用低1位來表示是TaggedPointer類型,其他三位來表示具體哪個類的,對于字符串,需要存儲它的長度,再讓出4位,還剩下56位,從而問題轉(zhuǎn)為如何利用這個56位。
計算機中存儲的就是0和1,對于字符串的編碼有ASCII和非ASCII:
-
ASCII是利用一個字節(jié)的大小表示字符的,一共是128個(最高位都為0); - 后面為了統(tǒng)一編碼出現(xiàn)了
Unicode編碼,Unicode是規(guī)定了符號的二進制代碼,沒有規(guī)定如何存儲,具體如何存儲的,后來就出現(xiàn)了,UTF-16(字符用兩個字節(jié)或四個字節(jié)表示)、UTF-32(字符用四個字節(jié)表示)和UTF-8(最常用的,兼容了ASCII)
對于非ASCII:
- 如果是
UTF-32編碼的,要想包含所有Unicode,需要4個字節(jié),那么最多也只能保存1個字符,沒有任何意義; - 如果是
UTF-16編碼的,要想包含所有Unicode,也需要4個字節(jié),最少也需要2個字節(jié),按最少的算,那么56位,也只能放3個16為的字符,還是很少; - 如果是
UTF-8,如果撇開ASCII的話,那么也是最多需要4個字節(jié),最少2個字節(jié),56位還是最多放3個字節(jié)。
對于非ASCII我們貌似沒有找到一個好的方案來存儲,那么我們要實現(xiàn)TaggedPointer的話,是不是可以不考慮非ASCII的情況,畢竟在實際場景,我們用到ASCII的場景的幾率還是比非ASCII大的多,對于非ASCII的還是交給開辟控件的方式。
對于ASCII:
如果我們不考慮非ASCII的話,那么有以下方案可以用來存儲數(shù)據(jù):
- 方案一: 使用
8位存儲一個字符,這也是默認計算機存儲ASCII的方式,由于占用一個字節(jié),那么這種方式56位可以放7個字節(jié); - 方案二: 使用
7位存儲一個字符,ASCII其實真正存儲數(shù)據(jù)的是7位,如果是用7位表示一個字符的話,那么最多可以放8個字節(jié),比方案一多出一個字節(jié); - 方案三: 使用
6位存儲,有人可能想6位怎么可能,存儲ASCII最少也得7位啊,6怎么存儲,是的,直接存是不行的,但是我們可以不直接存字符,而是提供一個表格,存索引。ASCII一共有128個,但是我們常用的根本就沒有那么多,那么我們可以不可以選出一些常用的來作為我們的可選值 ?6位的話,最多可以存儲2^ 6 = 64個不同的字符,所以肯定是不能滿查找ASCII集合,但是,我們可以找來常見的64個字符比如[a-zA-z0-9./_-],這里就有66個了,再從這個66個里面取出2個不常用的就可以了,這樣的話我們就可以存儲9個字節(jié)了; - 方案四: 使用
5位存儲,這種的話我們的查找范圍就縮小為了2^5 = 32個,也就是我們要在方案三的基礎(chǔ)上在找出更加常用的32個字符,這種方案可以存儲11個字符; - 方案五: 使用
4位存儲,那范圍就是2^4 = 16個,這種感覺行也行,但是范圍太小了 - 更少的想想不大可能了
下面看下蘋果是如何實現(xiàn)的
五、對于NSString蘋果是如何使用TaggedPointer的?
5.1 現(xiàn)象
添加測試如下測試代碼
NSMutableString *imutable = [NSMutableString string];
NSString *immutable;
char c = 'a';
do {
[imutable appendFormat: @"%c", c++];
immutable = [imutable copy];
NSLog(@"源地址:%p 真實地址:0x%lx %@ %@", immutable,_objc_decodeTaggedPointer_(immutable), immutable, object_getClass(immutable));
} while(((uintptr_t)immutable & 1) == 1);
輸出,這里我省去了源地址,因為這里打印了類的類型更直觀寫
真實地址:0x6115 a NSTaggedPointerString
真實地址:0x626125 ab NSTaggedPointerString
真實地址:0x63626135 abc NSTaggedPointerString
真實地址:0x6463626145 abcd NSTaggedPointerString
真實地址:0x656463626155 abcde NSTaggedPointerString
真實地址:0x66656463626165 abcdef NSTaggedPointerString
真實地址:0x6766656463626175 abcdefg NSTaggedPointerString
真實地址:0x22038a01169585 abcdefgh NSTaggedPointerString
真實地址:0x880e28045a54195 abcdefghi NSTaggedPointerString
真實地址:0xf9eb5f3ca3c376e0 abcdefghij __NSCFString
前面提到過最后一個字節(jié)低4位標(biāo)志是TaggedPointer信息,高4位存放字符串的長度,所以最后一個數(shù)字5是標(biāo)志位,倒數(shù)一個數(shù)字就是字符串的長度。
從上面的輸出可以看出:
- 當(dāng)字符串的長度
<=7的時候,蘋果是直接存儲的字符ASCII值,a的ASCII值是61,b是62...。 - 當(dāng)字符串長度大于
7的時候具體如何做的,我們通過逆向CoreFoundation.framework來查看
5.2 hopper -> length
先來看下length方法,看看是不是和我們猜測的一樣

翻譯一下就是
rdi = self ^ *_objc_debug_taggedpointer_obfuscator; // 解密得到真實地址
if ((di & 14 ) == 14) { 也就是//0b1110 我們的字符串的是5(0x0101),所以走else了
rax = (di >> 11) & 0xf;
} else {
rax =(di >> 4 ) & 0xf;
}
再簡化一下就是
======
rax = (di >> 4 ) & 0xf
已經(jīng)很顯然了,就是拿低1字節(jié)的高4位的值,證明了我們的猜想。
5.3 hopper -> characterAtIndex
蘋果是如何將字符轉(zhuǎn)成NSTaggedPointerString的,不是很好查,但是我們可以反向思考,通過取數(shù)據(jù)來反推如何存的,

下面開始簡化該偽代碼,如果你覺得不想看,可以直接跳到第四次簡化開始看。
___stack_chk_guard是為了安全加的,不考慮,前面分析過((((r8 ^ rdi) & 0xe) == 0xe ? 0x1 : 0x0) << 0x3 | 0x4)在這里等價于0x4,arg2就是傳進來的index
5.3.1 第一次簡化
unsigned short -[NSTaggedPointerString characterAtIndex:](void * self, void * _cmd, unsigned long long arg2) {
r12 = index;
rbx = self >> 0x4 & 0xf;
r8 = self >> 0x4 >> 0x4;
if (rbx >= 0x8) {
rdx = rbx;
if (rbx < 0xa) {
do {
*(int8_t *)(rbp + rdx + 0xffffffffffffffc7) = *(int8_t *)((r8 & 0x3f) + _sixBitToCharLookup);
rdx = rdx - 0x1;
r8 = r8 >> 0x6;
} while (rdx != 0x0);
}
else {
do {
*(int8_t *)(rbp + rdx + 0xffffffffffffffc7) = *(int8_t *)((r8 & 0x1f) + _sixBitToCharLookup);
rdx = rdx - 0x1;
r8 = r8 >> 0x5;
} while (rdx != 0x0);
}
}
rax = *(int8_t *)(rbp + r12 + 0xffffffffffffffc8) & 0xff;
return rax;
}
繼續(xù)分析這段代碼
-
self >> 0x4 & 0xf;其實就是字符串的length -
self >> 0x4 >> 0x4;其實就是字符串的開始位置 -
0xffffffffffffffc7其實是-0x39 = -57的補碼,0xffffffffffffffc7是-0x38 = -56的補碼
5.3.2 第二次簡化
unsigned short -[NSTaggedPointerString characterAtIndex:](void * self, void * _cmd, unsigned long long arg2) {
rbx = length;
r8 = self >> 0x8;
if (rbx >= 0x8) {
if (length < 0xa) {
do {
*(int8_t *)(rbp - 57 + rdx) = *(int8_t *)((r8 & 0x3f) + _sixBitToCharLookup);
rdx = rdx - 0x1;
r8 = r8 >> 0x6;
} while (rdx != 0x0);
}
else {
do {
*(int8_t *)(rbp - 57 + rdx) = *(int8_t *)((r8 & 0x1f) + _sixBitToCharLookup);
rdx = rdx - 0x1;
r8 = r8 >> 0x5;
} while (rdx != 0x0);
}
}
rax = *(int8_t *)(rbp - 56 + index) & 0xff;
return rax;
}
-
bp其實就是棧指針,這里使用bp說明是通過bp來操控??臻g的,然后每次循環(huán)dx都減1,然后r8左移6位或者5位,這個一般都是數(shù)組操作了,如果是5位的話最多存11個字節(jié),所以這里使用一個長度11的數(shù)組buffer[11],dx其實就會游離指針了我們用變量cursor表示
5.3.3 第三次簡化
unsigned short -[NSTaggedPointerString characterAtIndex:](void * self, void * _cmd, unsigned long long arg2) {
int8_t buffer[11];
r8 = self >> 0x8;
if (length >= 0x8) {
base = rbp - 57;
cursor = length;
if (length < 0xa) {
do {
buffer[base + cursor ] = *(int8_t *)((r8 & 0x3f) + _sixBitToCharLookup)
cursor = cursor - 0x1;
r8 = r8 >> 0x6;
} while (rdx != 0x0);
}
else {
do {
buffer[base + cursor ] = *(int8_t *)((r8 & 0x1f) + _sixBitToCharLookup);
cursor = cursor - 0x1;
r8 = r8 >> 0x5;
} while (rdx != 0x0);
}
}
rax = *(int8_t *)(rbp - 56 + index) & 0xff;
return rax;
}
_sixBitToCharLookup到底是什么呢,其實就是字符串

也就是eilotrm.apdnsIc ufkMShjTRxgC4013bDNvwyUL2O856P-B79AFKEWV_zGJ/HYX
其實程序還少了一段代碼,hopper翻譯偽代碼的時候漏掉了
0000000000060d87 cmp rbx, 0x8
0000000000060d8b jb loc_60dd1 // 當(dāng)bs < 0x8時
...
loc_60dd1:
0000000000060dd1 mov qword [rbp+var_38], r8
var_38就是-56

其實就是將r8的值放到[bp-56]的內(nèi)存處,由于是小端存儲,其實就是講self>> 8的內(nèi)容存放到對應(yīng)的內(nèi)存地址,類似于下面的代碼,但是是占8個字節(jié)的
*(uint64_t *)buffer = self >> 8;
5.3.4 第四次簡化
unsigned short -[NSTaggedPointerString characterAtIndex:](void * self, void * _cmd, unsigned long long arg2) {
int8_t buffer[11];
r8 = self >> 0x8;
if (length >= 0x8) {
base = rbp - 57;
cursor = length;
_sixBitToCharLookup = 'eilotrm.apdnsIc ufkMShjTRxgC4013bDNvwyUL2O856P-B79AFKEWV_zGJ/HYX';
if (length < 0xa) {
do {
buffer[base + cursor ] = _sixBitToCharLookup[r8 & 0x3f]
cursor = cursor - 0x1;
r8 = r8 >> 0x6;
} while (rdx != 0x0);
} else {
do {
buffer[base + cursor ] = _sixBitToCharLookup[r8 & 0x1f];
cursor = cursor - 0x1;
r8 = r8 >> 0x5;
} while (rdx != 0x0);
}
} else {
*(uint64_t *)buffer = self >> 8;
}
rax = *(int8_t *)(rbp - 56 + index) & 0xff;
return rax;
}
這就顯而易見了,對于字符串蘋果的處理如下:
- 對于小于
8個字符的,使用的是8位存儲; -
[8,10)的是通過6位存儲的; -
[10,11]的是通過5位存儲的。
根據(jù)這個結(jié)論我們再來看下5.1的現(xiàn)象,對于上面的判斷條件分別選一個代表
5.3.4.1 小于8位代表0x66656463626165 -> abcdef
可以看出是直接存儲的;
5.3.4.2 [8,10)代表:0x22038a01169585 -> abcdefgh
去掉后面的95剩下0x22038a011695,6位排列如下
001000 100000 001110 001010 000000 010001 011010 010101,每一個就對應(yīng)這個字符串eilotrm.apdnsIc ufkMShjTRxgC4013bDNvwyUL2O856P-B79AFKEWV_zGJ/HYX的索引值,為了方便查找做了一個對照表

所以
001000 100000 001110 001010 000000 010001 011010 010101
分別對應(yīng)
a b c d e f g h
5.3.4.3 [10,11]位代表abcdefghij
但是這個類是__NSCFString并不是我們的NSTaggedPointerString,按道理說5位的話是可以存放10個字節(jié)的啊,這是什么原因呢?
原來:不管是5位還是6位都是查詢的同一個字符串eilotrm.apdnsIc ufkMShjTRxgC4013bDNvwyUL2O856P-B79AFKEWV_zGJ/HYX,也就是上圖索引表的顏色區(qū)分,5位里面沒有包含b字符,但是我們的abcdefghij有b字符,所以不行,修改demo如下看看
NSString *str = [NSString stringWithFormat:@"acdefghijk"];
NSString *str2 = [NSString stringWithFormat:@"acdefghijkm"];
NSString *str3 = [NSString stringWithFormat:@"acdefghijkmn"];
NSLog(@"真實地址:0x%lx %@ %@", str,_objc_decodeTaggedPointer_(str), str3, object_getClass(str));
NSLog(@"真實地址:0x%lx %@ %@", str2,_objc_decodeTaggedPointer_(str2), str3, object_getClass(str2));
NSLog(@"真實地址:0x%lx %@ %@", str3,_objc_decodeTaggedPointer_(str3), str3, object_getClass(str3));
輸出
真實地址:0x10e5023aa86d2a5 acdefghijk NSTaggedPointerString
真實地址:0x21ca047550da46b5 acdefghijkm NSTaggedPointerString
真實地址:0xc64838cff22b0b46 acdefghijkmn __NSCFString
可以看到能夠支持11個字節(jié)了,0x10e5023aa86d2a5去掉0x10e5023aa86d2,按5位排列下看看
01000 01110 01010 00000 10001 11010 10101 00001 10110 10010
也就是 a c d e f g h i j k
所以我們可以得出能夠存[10,11]位字符是以所存字符在eilotrm.apdnsIc ufkMShjTRxgC4013內(nèi)為前提的。
最后再來看下蘋果對于非ASCII是怎么處理的,以漢字方(Unicode)編碼為\u65b9,占3個字節(jié),按道理也是可以放進指針里面的,我們看看蘋果有沒有這樣做
NSString *notAscii_1 = [NSString stringWithFormat:@"方"];
NSLog(@"源地址:%p %@ %@", notAscii_1,notAscii_1, object_getClass(notAscii_1));
輸出
源地址:0x101907df0 方 __NSCFString
發(fā)現(xiàn)蘋果并沒有放進指針內(nèi),而是真實的oc對象。
至此,我們之前的猜測一一驗證了。
下面總結(jié)一下TaggedPointer的特點
六、什么樣的字符會放進TaggedPointer?
總結(jié)了以下表格,注意這個只適用ASCII的情況,對于非ASCII都是使用的oc對象。

傳入的字符任意一個不在所在行的范圍,存的地方就會發(fā)生變化。
七、一個和TaggedPointer相關(guān)的面試題
下面代碼會發(fā)生什么問題?
@property (nonatomic, copy) NSString *target;
//.... dispatch_queue_t queue = dispatch_queue_create("parallel", DISPATCH_QUEUE_CONCURRENT);
// 方式一
for (int i = 0; i < 1000000 ; i++) {
dispatch_async(queue, ^{
self.target = [NSString stringWithFormat:@"ksddkjalkjd%d",I];
});
}
//.... dispatch_queue_t queue = dispatch_queue_create("parallel", DISPATCH_QUEUE_CONCURRENT);
// 方式二
for (int i = 0; i < 1000000 ; i++) {
dispatch_async(queue, ^{
self.target = [NSString stringWithFormat:@"ksddkjalkj"];
});
}
先說下結(jié)果吧 ,方式一會閃退,方式二正常運行。
分析這個道題,target的set方法實現(xiàn)
- (void)setTarget:(NSString *)target {
if(_target != target) {
[_target release];
target = [target retain];
}
}
方式一是真正的oc對象,由于是多線程會出現(xiàn)[_target release];被調(diào)用多次,從而閃退;
方式二不是oc對象,而是TaggedPointer,在release和retain的時候都會判斷是不是TaggedPointer
objc_object::rootRelease(bool performDealloc, bool handleUnderflow)
{
if (isTaggedPointer()) return false;
bool sideTableLocked = false;
...
}
ALWAYS_INLINE id
objc_object::rootRetain(bool tryRetain, bool handleOverflow)
{
if (isTaggedPointer()) return (id)this;
}
其他的方式可以加鎖解決,就不說了。