iOS中 Tagged Pointer 技術

2022-07-27 15:00:51

前言:

​ 從64位元開始,iOS引入了Tagged Pointer技術,用於優化NSNumber、NSDate、NSString等小物件的儲存。

Tagged Pointer主要為了解決兩個問題:

  1. 記憶體資源浪費,堆區需要額外的開闢空間
  2. 存取效率,每次set/get都需要存取堆區,浪費時間, 而且需要管理堆區物件的宣告週期,降低效率

Tagged Pointer特點:

  1. 專門用來儲存小物件,比如NSString,NSNumber,NSDate
  2. Tagged Pointer指標的值不再是堆區地址,而是包含真正的值。所以它不會在堆上再開闢空間了,也不需要管理物件的生命週期了。
  3. 記憶體讀取提升3倍,建立比之前快100多倍,銷燬速度更快

一、引入Tagged Pointer 前後對比

1、引入前

NSNumber等物件需要動態分配記憶體、維護參照計數等。 總共的空間= 指標空間 + 堆中分配的空間

2、引入後

NSNumber等物件,只需要分配一個指標即可,這個指標內部會包含這些資料內容。
總空間 = 指標空間
因為不用去用物件的方式管理參照計數,所以省卻了 retainrelease操作。

二、Tagged Pointer 原理

number1只有棧上的指標記憶體;而maxNum不僅有指標記憶體,在堆中還分配了32位元組的記憶體用於儲存該變數的值。通過觀察發現,物件的number1number2number3number4都儲存在了對應的指標中;而maxNum不同由於資料過大,導致無法 1 個指標 8 個位元組的記憶體根本存不下,而申請了32位元組堆記憶體。

  1. NSString型別的Tagged Pointer指標與基本型別的指標是不一樣的,末尾的數位為字串的長度;
  2. NSString型別的Tagged Pointer指標儲存char型別,返回的是ASCII碼(該值為16進位制的,需要進行十進位制轉換)

三、如何判斷是否使用了 Tagged Pointer 技術

BOOL isTaggedPointer(id pointer) {
    return (long)(__bridge void *)pointer & 1;
}

該函數就是呼叫了isTaggedPointer

四、使用 Tagged Pointer 注意點

​ 我們知道,所有OC物件都有isa指標,而Tagged Pointer並不是真正的物件,它沒有isa指標,所以如果你直接存取Tagged Pointerisa成員的話,在編譯時將會有警告。

五、面試題

dispatch_queue_t queue = dispatch_get_global_queue(0, 0);
    for (int i = 0; i<1000; i++) {
        dispatch_async(queue, ^{
            self.name = [NSString stringWithFormat:@"abcdefghijk"];
        });
    }
    
dispatch_queue_t queue = dispatch_get_global_queue(0, 0);
    for (int i = 0; i<1000; i++) {
        dispatch_async(queue, ^{
            self.name = [NSString stringWithFormat:@"abc"];
        });
    }

兩者執行結果有何不同?

首先看self.name = [NSString stringWithFormat:@"abcdefghijk"];

崩潰,並且崩潰在objc_release的地方。

是什麼原因導致崩潰的呢?

我們知道,
self.name = [NSString stringWithFormat:@"abcdefghijk"];
其實是呼叫了
[self setName:[NSString stringWithFormat:@"abcdefghijk"]];

而setName:的實現是:

- (void)setName:(NSString *)name
{
    if (_name != name) {
        [_name release];//老的釋放掉
        _name = [name copy];//傳入的值copy後賦值給_name
    }
}

由於是async非同步操作,self.name = [NSString stringWithFormat:@"abcdefghijk"];即[_name release];有可能會被多條執行緒同時操作。導致,執行緒n把_name釋放掉,執行緒n+1又要執行_name的釋放,從而造成_name已經被釋放兩次,第二次存取的時候,_name已經釋放過,造成壞記憶體存取。

解決方法一:atomic

@property (copy, atomic) NSString *name;
從而:

- (void)setName:(NSString *)name
{
    //加鎖操作
    if (_name != name) {
        [_name release];
        _name = [name copy];
    }
    //解鎖操作
}

解決方法二:

dispatch_queue_t queue = dispatch_get_global_queue(0, 0);
    for (int i = 0; i<1000; i++) {
        dispatch_async(queue, ^{
        //加鎖
            self.name = [NSString stringWithFormat:@"abcdefghijk"];
        });
        //解鎖
    }

self.name = [NSString stringWithFormat:@"abc"];
為何沒有崩潰呢?

從型別可以看出來,
內容多的name型別是__NSCFString
內容少的name型別是NSTaggedPointerString

這就是原因所在。

內容少的name,由於型別是NSTaggedPointerString,在賦值的時候
是直接在指標裡面取值,而不需要release操作,因此,不會崩潰