你不知道的TaggedPointer

一跳座、環(huán)境介紹

  1. mac版本Mac Mojave 10.14
  2. objc版本objc runtime 750

二兵拢、為什么要使用TaggedPointer溅话?

以前我們初始化一個(gè)對(duì)象(64位為例)缎讼,開發(fā)的代碼如下

NSNumber *number2 = [NSNumber numberWithInteger:2];

此時(shí)的內(nèi)存圖如下

15469379882496.jpg

可以看到我就想存一個(gè)2用掉了24個(gè)字節(jié)澎语,由于我們的NSNumberNSDate對(duì)象的值一般不需要8個(gè)字節(jié),4個(gè)字節(jié)的長度2^31=2147483648可以表達(dá)的數(shù)量已經(jīng)達(dá)到了20多億了萍膛,為了不造成內(nèi)存的浪費(fèi)融欧,想到將指針的值(8個(gè)字節(jié))進(jìn)行拆分,一部分表示數(shù)據(jù)卦羡,一部分用來表示是一個(gè)特殊的指針,他不執(zhí)行任何對(duì)象麦到,這就是TaggedPointer技術(shù)绿饵,這樣指針 = Data + Tag,那么我們的存一個(gè)數(shù)字只需要8個(gè)字節(jié)就夠了瓶颠。

三拟赊、一個(gè)簡單的例子

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é)果卻是這個(gè)樣子的

number1 pointer is 0x19ec25e574ba1459
number2 pointer is 0x19ec25e574ba1759
number3 pointer is 0x19ec25e574ba1659
numberffff pointer is 0x19ec25e57445ea59

這個(gè)地址有點(diǎn)特殊,研究了一下粹淋,發(fā)現(xiàn)原來是在10_14以后蘋果對(duì)TaggedPointer進(jìn)行了混淆吸祟,文件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值借杰,這是個(gè)隨機(jī)值过吻,要想獲取這個(gè)值:

方法一:通過斷點(diǎn)來獲取

15471013678173.jpg

通過lldb指令讀取

(lldb) p/x objc_debug_taggedpointer_obfuscator
(uintptr_t) $0 = 0x19ec25e574ba157e

方法二: 看來runtime源碼知道objc_debug_taggedpointer_obfuscator是個(gè)全局變量,只要在我們用的地方申明一下即可

extern uintptr_t objc_debug_taggedpointer_obfuscator;

通過NSLog打印就可以了

NSLog(@"%lx",objc_debug_taggedpointer_obfuscator);

為了方便查看蔗衡,簡單寫了一個(gè)方法纤虽,用來解開混淆

uintptr_t _objc_decodeTaggedPointer_(id  ptr) {
    NSString *p = [NSString stringWithFormat:@"%ld",ptr];
    return [p longLongValue] ^ objc_debug_taggedpointer_obfuscator;
}

3.2 真實(shí)的地址

NSNumber *number1 = @1;
NSNumber *number2 = @2;
NSNumber *number3 = @3;
NSNumber *numberFFFF = @(0xFFFF);

NSLog(@"number1 pointer is %p---真實(shí)地址:==0x%lx", number1,_objc_decodeTaggedPointer_(number1));
NSLog(@"number2 pointer is %p---真實(shí)地址:==0x%lx", number2,_objc_decodeTaggedPointer_(number2));
NSLog(@"number3 pointer is %p---真實(shí)地址:==0x%lx", number3,_objc_decodeTaggedPointer_(number3));
NSLog(@"numberffff pointer is %p---真實(shí)地址:==0x%lx", numberFFFF,_objc_decodeTaggedPointer_(numberFFFF));

輸出

number1 pointer is 0xfda27e12be89be71---真實(shí)地址:==0x127
number2 pointer is 0xfda27e12be89bd71---真實(shí)地址:==0x227
number3 pointer is 0xfda27e12be89bc71---真實(shí)地址:==0x327
numberffff pointer is 0xfda27e12be764071---真實(shí)地址:==0xffff27

會(huì)發(fā)現(xiàn),不管運(yùn)行多少次绞惦,都是以27結(jié)尾逼纸,我們有理由相信,蘋果貢獻(xiàn)了1個(gè)字節(jié)(8個(gè)bit)來標(biāo)識(shí)這是個(gè)特殊的指針济蝉,最后1個(gè)字節(jié)用來標(biāo)識(shí)杰刽,這個(gè)類指針菠发,判斷是否是TaggedPointer不同平臺(tái)判斷的方式不一樣,但對(duì)我們理解根本不影響

static inline bool 
_objc_isTaggedPointer(const void * _Nullable ptr)
{
    return ((uintptr_t)ptr & _OBJC_TAG_MASK) == _OBJC_TAG_MASK;
}

  1. mac平臺(tái)最后一個(gè)為1专缠;
  2. iPhone和模擬器雷酪,為最高位是1

那么剩下的7個(gè)字節(jié)是不是都用來存放數(shù)據(jù)呢涝婉?

3.3 TaggedPointer存儲(chǔ)的數(shù)字的最大值

NSNumber *numberF13   = @(0xFFFFFFFFFFFFF);
NSNumber *numberF13_1 = @(0x1FFFFFFFFFFFFF);
NSNumber *numberF13_3 = @(0x3FFFFFFFFFFFFF);
NSNumber *numberF13_7 = @(0x7FFFFFFFFFFFFF);
NSNumber *numberF14   = @(0xFFFFFFFFFFFFFF);

NSLog(@"numberF13 pointer is %p---真實(shí)地址:==0x%lx", numberF13,_objc_decodeTaggedPointer_(numberF13));
NSLog(@"numberF13_1 pointer is %p---真實(shí)地址:==0x%lx", numberF13_1,_objc_decodeTaggedPointer_(numberF13_1));
NSLog(@"numberF13_3 pointer is %p---真實(shí)地址:==0x%lx", numberF13_3,_objc_decodeTaggedPointer_(numberF13_3));
NSLog(@"numberF13_7 pointer is %p---真實(shí)地址:==0x%lx", numberF13_7,_objc_decodeTaggedPointer_(numberF13_7));
   
NSLog(@"numberF14 pointer is %p---真實(shí)地址:==0x%lx", numberF14,_objc_decodeTaggedPointer_(numberF14));

輸出如下

number1 pointer is 0x20f9850034a2e631---真實(shí)地址:==0x127
number2 pointer is 0x20f9850034a2e531---真實(shí)地址:==0x227
number3 pointer is 0x20f9850034a2e431---真實(shí)地址:==0x327
numberffff pointer is 0x20f98500345d1831---真實(shí)地址:==0xffff27
numberF13 pointer is 0x2f067affcb5d1821---真實(shí)地址:==0xfffffffffffff37
numberF13_1 pointer is 0x3f067affcb5d1821---真實(shí)地址:==0x1fffffffffffff37
numberF13_3 pointer is 0x1f067affcb5d1821---真實(shí)地址:==0x3fffffffffffff37
numberF13_7 pointer is 0x5f067affcb5d1821---真實(shí)地址:==0x7fffffffffffff37

numberF14 pointer is 0x102500210

從輸出可以看出哥力,到numberF14地址已經(jīng)是真正的oc對(duì)象的地址了,說明有效存儲(chǔ)位置有56位墩弯,所以TaggedPointer所能表達(dá)的數(shù)字范圍為[0 2^65)吩跋。

四、思考:你會(huì)如何實(shí)現(xiàn)NSString的TaggedPointer渔工?

我們現(xiàn)在想做的事情就是如何利用指針來存儲(chǔ)我們的字符數(shù)據(jù)锌钮,而指針的大小就是8個(gè)字節(jié),一共64位引矩,如何利用這個(gè)64位呢梁丘?由NSNumber的靈感,可以使用低1位來表示是TaggedPointer類型旺韭,其他三位來表示具體哪個(gè)類的氛谜,對(duì)于字符串,需要存儲(chǔ)它的長度区端,再讓出4位值漫,還剩下56位,從而問題轉(zhuǎn)為如何利用這個(gè)56位织盼。

計(jì)算機(jī)中存儲(chǔ)的就是01杨何,對(duì)于字符串的編碼有ASCII非ASCII

  1. ASCII是利用一個(gè)字節(jié)的大小表示字符的,一共是128個(gè)(最高位都為0)沥邻;
  2. 后面為了統(tǒng)一編碼出現(xiàn)了Unicode編碼危虱,Unicode是規(guī)定了符號(hào)的二進(jìn)制代碼,沒有規(guī)定如何存儲(chǔ)谋国,具體如何存儲(chǔ)的槽地,后來就出現(xiàn)了,UTF-16(字符用兩個(gè)字節(jié)或四個(gè)字節(jié)表示)芦瘾、UTF-32(字符用四個(gè)字節(jié)表示)和UTF-8(最常用的捌蚊,兼容了ASCII

對(duì)于非ASCII

  1. 如果是UTF-32編碼的,要想包含所有Unicode,需要4個(gè)字節(jié)近弟,那么最多也只能保存1個(gè)字符缅糟,沒有任何意義;
  2. 如果是 UTF-16編碼的祷愉,要想包含所有Unicode窗宦,也需要4個(gè)字節(jié)赦颇,最少也需要2個(gè)字節(jié),按最少的算赴涵,那么56位媒怯,也只能放3個(gè)16為的字符,還是很少髓窜;
  3. 如果是UTF-8扇苞,如果撇開ASCII的話,那么也是最多需要4個(gè)字節(jié)寄纵,最少2個(gè)字節(jié)鳖敷,56位還是最多放3個(gè)字節(jié)。

對(duì)于非ASCII我們貌似沒有找到一個(gè)好的方案來存儲(chǔ)程拭,那么我們要實(shí)現(xiàn)TaggedPointer的話定踱,是不是可以不考慮非ASCII的情況,畢竟在實(shí)際場景恃鞋,我們用到ASCII的場景的幾率還是比非ASCII大的多崖媚,對(duì)于非ASCII的還是交給開辟控件的方式。

對(duì)于ASCII:

如果我們不考慮非ASCII的話恤浪,那么有以下方案可以用來存儲(chǔ)數(shù)據(jù):

  1. 方案一: 使用8位存儲(chǔ)一個(gè)字符至扰,這也是默認(rèn)計(jì)算機(jī)存儲(chǔ)ASCII的方式,由于占用一個(gè)字節(jié)资锰,那么這種方式56位可以放7個(gè)字節(jié);
  2. 方案二: 使用7位存儲(chǔ)一個(gè)字符阶祭,ASCII其實(shí)真正存儲(chǔ)數(shù)據(jù)的是7位绷杜,如果是用7位表示一個(gè)字符的話,那么最多可以放8個(gè)字節(jié)濒募,比方案一多出一個(gè)字節(jié)鞭盟;
  3. 方案三: 使用6位存儲(chǔ),有人可能想6位怎么可能瑰剃,存儲(chǔ)ASCII最少也得7位啊齿诉,6怎么存儲(chǔ),是的晌姚,直接存是不行的粤剧,但是我們可以不直接存字符,而是提供一個(gè)表格挥唠,存索引抵恋。ASCII一共有128個(gè),但是我們常用的根本就沒有那么多宝磨,那么我們可以不可以選出一些常用的來作為我們的可選值 弧关? 6位的話盅安,最多可以存儲(chǔ)2^ 6 = 64個(gè)不同的字符,所以肯定是不能滿查找ASCII集合,但是世囊,我們可以找來常見的64個(gè)字符比如[a-zA-z0-9./_-],這里就有66個(gè)了别瞭,再從這個(gè)66個(gè)里面取出2個(gè)不常用的就可以了,這樣的話我們就可以存儲(chǔ)9個(gè)字節(jié)了株憾;
  4. 方案四: 使用5位存儲(chǔ)蝙寨,這種的話我們的查找范圍就縮小為了2^5 = 32個(gè),也就是我們要在方案三的基礎(chǔ)上在找出更加常用的32個(gè)字符号胚,這種方案可以存儲(chǔ)11個(gè)字符籽慢;
  5. 方案五: 使用4位存儲(chǔ),那范圍就是2^4 = 16個(gè)猫胁,這種感覺行也行箱亿,但是范圍太小了
  6. 更少的想想不大可能了

下面看下蘋果是如何實(shí)現(xiàn)的

五、對(duì)于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 真實(shí)地址:0x%lx %@ %@", immutable,_objc_decodeTaggedPointer_(immutable), immutable, object_getClass(immutable));
} while(((uintptr_t)immutable & 1) == 1);

輸出届惋,這里我省去了源地址,因?yàn)檫@里打印了類的類型更直觀寫

真實(shí)地址:0x6115                 a               NSTaggedPointerString
真實(shí)地址:0x626125               ab              NSTaggedPointerString
真實(shí)地址:0x63626135             abc             NSTaggedPointerString
真實(shí)地址:0x6463626145           abcd            NSTaggedPointerString
真實(shí)地址:0x656463626155         abcde           NSTaggedPointerString
真實(shí)地址:0x66656463626165       abcdef          NSTaggedPointerString
真實(shí)地址:0x6766656463626175     abcdefg         NSTaggedPointerString
真實(shí)地址:0x22038a01169585       abcdefgh        NSTaggedPointerString
真實(shí)地址:0x880e28045a54195      abcdefghi       NSTaggedPointerString
真實(shí)地址:0xf9eb5f3ca3c376e0     abcdefghij      __NSCFString

前面提到過最后一個(gè)字節(jié)低4位標(biāo)志是TaggedPointer信息菠赚,高4位存放字符串的長度脑豹,所以最后一個(gè)數(shù)字5是標(biāo)志位,倒數(shù)一個(gè)數(shù)字就是字符串的長度衡查。

從上面的輸出可以看出:

  1. 當(dāng)字符串的長度<=7的時(shí)候瘩欺,蘋果是直接存儲(chǔ)的字符ASCII值,aASCII值是61拌牲,b62...俱饿。
  2. 當(dāng)字符串長度大于7的時(shí)候具體如何做的,我們通過逆向CoreFoundation.framework來查看

5.2 hopper -> length

先來看下length方法塌忽,看看是不是和我們猜測的一樣

15471101516030.jpg

翻譯一下就是


rdi = self ^ *_objc_debug_taggedpointer_obfuscator; // 解密得到真實(shí)地址

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ù)來反推如何存的排惨,

15471113839214.jpg

下面開始簡化該偽代碼榕酒,如果你覺得不想看,可以直接跳到第四次簡化開始看眷蜓。

___stack_chk_guard是為了安全加的迄损,不考慮,前面分析過((((r8 ^ rdi) & 0xe) == 0xe ? 0x1 : 0x0) << 0x3 | 0x4)在這里等價(jià)于0x4账磺,arg2就是傳進(jìn)來的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ù)分析這段代碼

  1. self >> 0x4 & 0xf;其實(shí)就是字符串的length
  2. self >> 0x4 >> 0x4;其實(shí)就是字符串的開始位置
  3. 0xffffffffffffffc7其實(shí)是-0x39 = -57的補(bǔ)碼,0xffffffffffffffc7-0x38 = -56的補(bǔ)碼

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;
}


  1. bp其實(shí)就是棧指針芹敌,這里使用bp說明是通過bp來操控椚叮空間的,然后每次循環(huán)dx都減1氏捞,然后r8左移6位或者5位碧聪,這個(gè)一般都是數(shù)組操作了,如果是5位的話最多存11個(gè)字節(jié)液茎,所以這里使用一個(gè)長度11的數(shù)組buffer[11]逞姿,dx其實(shí)就會(huì)游離指針了我們用變量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到底是什么呢,其實(shí)就是字符串

15471761831012.jpg

也就是eilotrm.apdnsIc ufkMShjTRxgC4013bDNvwyUL2O856P-B79AFKEWV_zGJ/HYX

其實(shí)程序還少了一段代碼捆等,hopper翻譯偽代碼的時(shí)候漏掉了

0000000000060d87         cmp        rbx, 0x8
0000000000060d8b         jb         loc_60dd1 // 當(dāng)bs < 0x8時(shí)
...
loc_60dd1:
0000000000060dd1         mov        qword [rbp+var_38], r8   

var_38就是-56

15471166175011.jpg

其實(shí)就是將r8的值放到[bp-56]的內(nèi)存處滞造,由于是小端存儲(chǔ),其實(shí)就是講self>> 8的內(nèi)容存放到對(duì)應(yīng)的內(nèi)存地址栋烤,類似于下面的代碼谒养,但是是占8個(gè)字節(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;
}


這就顯而易見了,對(duì)于字符串蘋果的處理如下:

  1. 對(duì)于小于8個(gè)字符的明郭,使用的是8位存儲(chǔ)买窟;
  2. [8,10)的是通過6位存儲(chǔ)的;
  3. [10,11]的是通過5位存儲(chǔ)的薯定。

根據(jù)這個(gè)結(jié)論我們再來看下5.1的現(xiàn)象始绍,對(duì)于上面的判斷條件分別選一個(gè)代表

5.3.4.1 小于8位代表0x66656463626165 -> abcdef

可以看出是直接存儲(chǔ)的;

5.3.4.2 [8,10)代表:0x22038a01169585 -> abcdefgh

去掉后面的95剩下0x22038a011695话侄,6位排列如下

001000 100000 001110 001010 000000 010001 011010 010101亏推,每一個(gè)就對(duì)應(yīng)這個(gè)字符串eilotrm.apdnsIc ufkMShjTRxgC4013bDNvwyUL2O856P-B79AFKEWV_zGJ/HYX的索引值,為了方便查找做了一個(gè)對(duì)照表

15469350941106.jpg

所以

001000 100000 001110 001010 000000 010001 011010 010101

分別對(duì)應(yīng)

a b c d e f g h

5.3.4.3 [10,11]位代表abcdefghij

但是這個(gè)類是__NSCFString并不是我們的NSTaggedPointerString年堆,按道理說5位的話是可以存放10個(gè)字節(jié)的啊径簿,這是什么原因呢?

原來:不管是5位還是6位都是查詢的同一個(gè)字符串eilotrm.apdnsIc ufkMShjTRxgC4013bDNvwyUL2O856P-B79AFKEWV_zGJ/HYX嘀韧,也就是上圖索引表的顏色區(qū)分,5位里面沒有包含b字符缠捌,但是我們的abcdefghijb字符锄贷,所以不行,修改demo如下看看


NSString *str = [NSString stringWithFormat:@"acdefghijk"];
NSString *str2 = [NSString stringWithFormat:@"acdefghijkm"];
NSString *str3 = [NSString stringWithFormat:@"acdefghijkmn"];
    
NSLog(@"真實(shí)地址:0x%lx %@ %@", str,_objc_decodeTaggedPointer_(str), str3, object_getClass(str));
NSLog(@"真實(shí)地址:0x%lx %@ %@", str2,_objc_decodeTaggedPointer_(str2), str3, object_getClass(str2));
NSLog(@"真實(shí)地址:0x%lx %@ %@", str3,_objc_decodeTaggedPointer_(str3), str3, object_getClass(str3));

輸出

真實(shí)地址:0x10e5023aa86d2a5 acdefghijk NSTaggedPointerString
真實(shí)地址:0x21ca047550da46b5 acdefghijkm NSTaggedPointerString
真實(shí)地址:0xc64838cff22b0b46 acdefghijkmn __NSCFString

可以看到能夠支持11個(gè)字節(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)為前提的。

最后再來看下蘋果對(duì)于非ASCII是怎么處理的哑芹,以漢字(Unicode)編碼為\u65b9,占3個(gè)字節(jié)炎辨,按道理也是可以放進(jìn)指針里面的,我們看看蘋果有沒有這樣做

NSString *notAscii_1 = [NSString stringWithFormat:@"方"];

NSLog(@"源地址:%p  %@ %@", notAscii_1,notAscii_1, object_getClass(notAscii_1));
        

輸出

源地址:0x101907df0  方 __NSCFString

發(fā)現(xiàn)蘋果并沒有放進(jìn)指針內(nèi)聪姿,而是真實(shí)的oc對(duì)象碴萧。

至此乙嘀,我們之前的猜測一一驗(yàn)證了。

下面總結(jié)一下TaggedPointer的特點(diǎn)

六破喻、什么樣的字符會(huì)放進(jìn)TaggedPointer虎谢?

總結(jié)了以下表格,注意這個(gè)只適用ASCII的情況曹质,對(duì)于非ASCII都是使用的oc對(duì)象婴噩。

15471895085356.jpg

傳入的字符任意一個(gè)不在所在行的范圍,存的地方就會(huì)發(fā)生變化羽德。

七几莽、一個(gè)和TaggedPointer相關(guān)的面試題

下面代碼會(huì)發(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é)果吧 宅静,方式一會(huì)閃退章蚣,方式二正常運(yùn)行。

分析這個(gè)道題坏为,targetset方法實(shí)現(xiàn)

- (void)setTarget:(NSString *)target {
    if(_target != target) {
        [_target release];
        target = [target retain];
    }
}

方式一是真正的oc對(duì)象究驴,由于是多線程會(huì)出現(xiàn)[_target release];被調(diào)用多次,從而閃退匀伏;
方式二不是oc對(duì)象洒忧,而是TaggedPointer,在releaseretain的時(shí)候都會(huì)判斷是不是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;

}

其他的方式可以加鎖解決,就不說了够颠。

感謝

  1. http://www.ruanyifeng.com/blog/2007/10/ascii_unicode_and_utf-8.html
  2. https://mikeash.com/pyblog/friday-qa-2015-07-31-tagged-pointer-strings.html
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末熙侍,一起剝皮案震驚了整個(gè)濱河市,隨后出現(xiàn)的幾起案子履磨,更是在濱河造成了極大的恐慌蛉抓,老刑警劉巖,帶你破解...
    沈念sama閱讀 206,378評(píng)論 6 481
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件剃诅,死亡現(xiàn)場離奇詭異巷送,居然都是意外死亡,警方通過查閱死者的電腦和手機(jī)矛辕,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 88,356評(píng)論 2 382
  • 文/潘曉璐 我一進(jìn)店門笑跛,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人聊品,你說我怎么就攤上這事飞蹂。” “怎么了翻屈?”我有些...
    開封第一講書人閱讀 152,702評(píng)論 0 342
  • 文/不壞的土叔 我叫張陵陈哑,是天一觀的道長。 經(jīng)常有香客問我,道長惊窖,這世上最難降的妖魔是什么刽宪? 我笑而不...
    開封第一講書人閱讀 55,259評(píng)論 1 279
  • 正文 為了忘掉前任,我火速辦了婚禮爬坑,結(jié)果婚禮上纠屋,老公的妹妹穿的比我還像新娘。我一直安慰自己盾计,他們只是感情好售担,可當(dāng)我...
    茶點(diǎn)故事閱讀 64,263評(píng)論 5 371
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著署辉,像睡著了一般族铆。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上哭尝,一...
    開封第一講書人閱讀 49,036評(píng)論 1 285
  • 那天哥攘,我揣著相機(jī)與錄音,去河邊找鬼材鹦。 笑死逝淹,一個(gè)胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的桶唐。 我是一名探鬼主播栅葡,決...
    沈念sama閱讀 38,349評(píng)論 3 400
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼尤泽!你這毒婦竟也來了欣簇?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 36,979評(píng)論 0 259
  • 序言:老撾萬榮一對(duì)情侶失蹤坯约,失蹤者是張志新(化名)和其女友劉穎熊咽,沒想到半個(gè)月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體闹丐,經(jīng)...
    沈念sama閱讀 43,469評(píng)論 1 300
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡横殴,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 35,938評(píng)論 2 323
  • 正文 我和宋清朗相戀三年,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了卿拴。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片衫仑。...
    茶點(diǎn)故事閱讀 38,059評(píng)論 1 333
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡,死狀恐怖巍棱,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情蛋欣,我是刑警寧澤航徙,帶...
    沈念sama閱讀 33,703評(píng)論 4 323
  • 正文 年R本政府宣布,位于F島的核電站陷虎,受9級(jí)特大地震影響到踏,放射性物質(zhì)發(fā)生泄漏杠袱。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 39,257評(píng)論 3 307
  • 文/蒙蒙 一窝稿、第九天 我趴在偏房一處隱蔽的房頂上張望楣富。 院中可真熱鬧,春花似錦伴榔、人聲如沸纹蝴。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,262評(píng)論 0 19
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽塘安。三九已至,卻和暖如春援奢,著一層夾襖步出監(jiān)牢的瞬間兼犯,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 31,485評(píng)論 1 262
  • 我被黑心中介騙來泰國打工集漾, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留切黔,地道東北人。 一個(gè)月前我還...
    沈念sama閱讀 45,501評(píng)論 2 354
  • 正文 我出身青樓具篇,卻偏偏與公主長得像纬霞,于是被迫代替她去往敵國和親。 傳聞我的和親對(duì)象是個(gè)殘疾皇子栽连,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 42,792評(píng)論 2 345

推薦閱讀更多精彩內(nèi)容