iOS-內(nèi)存管理2-內(nèi)存區(qū)域、Tagged Pointer

一. 內(nèi)存區(qū)域

iOS程序的內(nèi)存布局.png

驗(yàn)證上圖,代碼如下:

#import <UIKit/UIKit.h>
#import "AppDelegate.h"

int a = 10;
int b;

int main(int argc, char * argv[]) {
    @autoreleasepool {
        static int c = 20;
        
        static int d;
        
        int e;
        int f = 20;

        NSString *str1 = @"123";
        NSString *str2 = @"123";
        
        NSObject *obj = [[NSObject alloc] init];
        
        NSLog(@"\n&a=%p\n&b=%p\n&c=%p\n&d=%p\n&e=%p\n&f=%p\nstr1=%p\nstr2=%p\nobj=%p\n",
              &a, &b, &c, &d, &e, &f, str1, str2, obj);
        
        return UIApplicationMain(argc, argv, nil, NSStringFromClass([AppDelegate class]));
    }
}

運(yùn)行后,整理打?。?/p>

代碼段:函數(shù)都存放在代碼段,由于函數(shù)地址沒(méi)法直接打印,只能進(jìn)入?yún)R編查看,這里就省略了。

數(shù)據(jù)段:字符串常量
str1=0x10dfa0068
str2=0x10dfa0068

數(shù)據(jù)段:已初始化的全局變量、靜態(tài)變量
&a =0x10dfa0db8
&c =0x10dfa0dbc

數(shù)據(jù)段:未初始化的全局變量、靜態(tài)變量
&d =0x10dfa0e80
&b =0x10dfa0e84

堆:alloc動(dòng)態(tài)分配的空間
obj=0x608000012210

棧:函數(shù)調(diào)用開(kāi)銷(xiāo),比如局部變量
&f =0x7ffee1c60fe0
&e =0x7ffee1c60fe4

可以發(fā)現(xiàn):

  1. 上面的內(nèi)存地址從小到大。
  2. 字符串常量一樣的時(shí)候,指針指向的內(nèi)存地址也一樣,說(shuō)明保存在內(nèi)存中只有一份內(nèi)存。
  3. 棧里面內(nèi)存分配的確是從大到小的。先分配e,再分配f,所以e內(nèi)存地址比f(wàn)大。
  4. 堆和棧地址位數(shù)比較多,說(shuō)明系統(tǒng)給堆、棧預(yù)留的空間比較大。

二. Tagged Pointer技術(shù)

1. 關(guān)于Tagged Pointer

  1. 從64bit開(kāi)始,iOS引入了Tagged Pointer技術(shù),用于優(yōu)化NSNumber、NSDate、NSString等小對(duì)象的存儲(chǔ),Tagged Pointer技術(shù)是編譯器幫我們做的。
  2. 在沒(méi)有使用Tagged Pointer之前, NSNumber等對(duì)象需要?jiǎng)討B(tài)分配內(nèi)存、維護(hù)引用計(jì)數(shù)等,NSNumber指針存儲(chǔ)的是堆中NSNumber對(duì)象的地址值。
  3. 使用Tagged Pointer之后,NSNumber指針里面存儲(chǔ)的數(shù)據(jù)變成了:Tag + Data,也就是將數(shù)據(jù)直接存儲(chǔ)在了指針中,其中Tag是標(biāo)記是什么類(lèi)型,比如NSNumber、NSDate、NSString等。
  4. 當(dāng)指針不夠存儲(chǔ)數(shù)據(jù)時(shí),才會(huì)使用動(dòng)態(tài)分配內(nèi)存的方式來(lái)存儲(chǔ)數(shù)據(jù)。
  5. objc_msgSend能識(shí)別Tagged Pointer,比如NSNumber的intValue方法,優(yōu)點(diǎn)就是直接從指針提取數(shù)據(jù),節(jié)省了以前的調(diào)用開(kāi)銷(xiāo),同時(shí)也會(huì)節(jié)省內(nèi)存
  6. 如何判斷一個(gè)指針是否為T(mén)agged Pointer?
    iOS平臺(tái),轉(zhuǎn)換成二進(jìn)制,最高有效位是1(第64bit位)
    Mac平臺(tái),轉(zhuǎn)換成二進(jìn)制,最低有效位是1(第0bit位)

2. 為什么使用Tagged Pointer?

使用Tagged Pointer,就是直接從指針提取數(shù)據(jù),節(jié)省了以前的調(diào)用開(kāi)銷(xiāo),同時(shí)也會(huì)節(jié)省內(nèi)存。

Tagged Pointer.png

可以發(fā)現(xiàn),使用Tagged Pointer之前,如果想要保存10,需要包裝成NSNumber對(duì)象(16字節(jié)),NSNumber對(duì)象里面放著10,然后用number指針(8字節(jié))指向NSNumber對(duì)象,至少需要24字節(jié)。

使用Tagged Pointer之后,number指針(8字節(jié))里面保存的就是16進(jìn)制的10:0xb000a1。其中0x代表16進(jìn)制,b就是標(biāo)記NSNumber類(lèi)型,a是16進(jìn)制的10,1代表使用了Tagged Pointer技術(shù),這樣,8字節(jié)就能存放NSNumber類(lèi)型的10了。

3. 舉例證明

創(chuàng)建命令行項(xiàng)目(Mac平臺(tái)的),舉例證明:

#import <Foundation/Foundation.h>
//是否使用了TaggedPointer技術(shù)
BOOL isTaggedPointer(id pointer)
{
    return (long)(__bridge void *)pointer & 1;
}

int main(int argc, const char * argv[]) {
    @autoreleasepool {
//        //完整寫(xiě)法
//        NSNumber *number = [NSNumber numberWithInt:10];
//        //簡(jiǎn)寫(xiě)
//        NSNumber *number = @10;

        //再簡(jiǎn)寫(xiě)
        NSNumber *number1 = @4;
        NSNumber *number2 = @5;
        NSNumber *number3 = @(0xFFFFFFFFFFFFFFF);//數(shù)字很大,存儲(chǔ)的是對(duì)象
        
        NSLog(@"%p %p %p", number1, number2, number3);
        NSLog(@"%d %d %d", isTaggedPointer(number1), isTaggedPointer(number2), isTaggedPointer(number3));
    }
    return 0;
}

打印結(jié)果為:

0x77f92be9bdbfa067 0x77f92be9bdbfa167 0x100575490
1 1 0

前兩個(gè)數(shù)字比較小,使用了Tagged Pointer技術(shù),第3個(gè)數(shù)字比較大,被包裝成NSNumber對(duì)象,放在了堆空間。

前兩個(gè)地址,末尾都是7,轉(zhuǎn)成二進(jìn)制就是0b0111,最后末尾是1,說(shuō)明使用了Tagged Pointer技術(shù),可以使用&1獲取最后一位,根據(jù)最后一位是否為1,判斷是否使用了Tagged Pointer技術(shù)(如上打印)。

第三個(gè)地址,末尾是0,轉(zhuǎn)成二進(jìn)制就是0b0000,被包裝成了NSNumber對(duì)象。因?yàn)镺C對(duì)象有內(nèi)存對(duì)齊的概念,OC對(duì)象的內(nèi)存大小必須是16的倍數(shù),所以只要是OC對(duì)象,內(nèi)存地址最后一位一定是0。

如果將number1轉(zhuǎn)成int:

[number1 intValue]

其實(shí)就是objc_msgSend(number1, @selector(intValue)),objc_msgSend能識(shí)別Tagged Pointer,number1使用了Tagged Pointer,所以轉(zhuǎn)成int的時(shí)候,是直接取出number1里面的值,節(jié)省了調(diào)用開(kāi)銷(xiāo)。可以發(fā)現(xiàn),Tagged Pointer不僅僅是內(nèi)存空間的優(yōu)化,還是使用上的優(yōu)化。

4. 補(bǔ)充:查看OC源碼是如何判斷是否使用了Tagged Pointer的?

在objc4里面搜索到如下源碼:

#if TARGET_OS_OSX && __x86_64__
    // 如果是Mac平臺(tái),設(shè)置為0
#   define OBJC_MSB_TAGGED_POINTERS 0
#else
    // 其他平臺(tái)(iOS平臺(tái)),設(shè)置為1
#   define OBJC_MSB_TAGGED_POINTERS 1
#endif

#if OBJC_MSB_TAGGED_POINTERS //如果是iOS平臺(tái)
#   define _OBJC_TAG_MASK (1UL<<63)  //1左移63位
#else  //如果是Mac平臺(tái)
#   define _OBJC_TAG_MASK 1UL   //1
#endif

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

可以發(fā)現(xiàn),和我們上面說(shuō)的一樣,如果是iOS平臺(tái),就看最高有效位是不是1,如果是Mac平臺(tái),就看最低有效位是不是1。

三. 面試題

思考以下兩段代碼能發(fā)生什么事?有什么區(qū)別?

@property (copy, nonatomic) NSString *name;

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"];
    });
}

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

運(yùn)行之后,第一段代碼報(bào)錯(cuò)壞內(nèi)存訪(fǎng)問(wèn),并且崩在objc_release這個(gè)函數(shù)。第二段代碼沒(méi)問(wèn)題。

報(bào)錯(cuò).png

1. 為什么報(bào)壞內(nèi)存訪(fǎng)問(wèn)錯(cuò)誤?

我們知道,上面的setter方法,ARC內(nèi)部實(shí)現(xiàn)是:

- (void)setName:(NSString *)name
{
    _name = name;
}

ARC最終都要轉(zhuǎn)成MRC,MRC內(nèi)部實(shí)現(xiàn)是:

- (void)setName:(NSString *)name
{
    if (_name != name) {
        [_name release]; //先釋放舊的
        _name = [name copy]; //再賦值新的
    }
}

問(wèn)題就出在 [_name release],當(dāng)多條線(xiàn)程同時(shí)調(diào)用setter方法的時(shí)候,就有可能多個(gè)線(xiàn)程同時(shí)訪(fǎng)問(wèn)[_name release],剛開(kāi)始引用計(jì)數(shù)器為1,當(dāng)?shù)诙蝦elease的時(shí)候,_name已經(jīng)釋放了,這時(shí)候再訪(fǎng)問(wèn)_name就會(huì)報(bào)錯(cuò):壞內(nèi)存訪(fǎng)問(wèn)。

2. 如何解決呢?

  1. 改用atomic修飾,不推薦,具體可參考:加鎖方案2

  2. 調(diào)用setter方法之前加鎖,如下:

// 在這里加鎖
self.name = [NSString stringWithFormat:@"abcdefghijk"];
// 在這里解鎖

具體怎么加鎖可參考:加鎖方案1

3. 為什么第二段代碼不會(huì)崩潰?

首先,運(yùn)行以下代碼:

NSString *str1 = [NSString stringWithFormat:@"abcdefghijk"];
NSString *str2 = [NSString stringWithFormat:@"abc"];
    
NSLog(@"%@ %@", [str1 class], [str2 class]);
NSLog(@"%p %p",str1, str2); 

打?。?/p>

__NSCFString NSTaggedPointerString
0x6000037fa1a0 0xec1baa29b42aee38

根據(jù)上面我們學(xué)的Tagged Pointer技術(shù),結(jié)合打印的類(lèi)型信息,很容易知道,str1是OC對(duì)象,str2使用了Tagged Pointer技術(shù),abc的值直接存儲(chǔ)到str2指針里面了。所以,對(duì)于str2,不是OC對(duì)象,不會(huì)調(diào)用setter方法,而是直接從指針里面獲取值,所以不會(huì)出現(xiàn)上面的崩潰。

4. 補(bǔ)充:驗(yàn)證str2使用了Tagged Pointer技術(shù)

由于上面代碼是iOS項(xiàng)目,如果將0xec1baa29b42aee38轉(zhuǎn)成二進(jìn)制,最高位是1,說(shuō)明使用了Tagged Pointer技術(shù)。

將0xec1baa29b42aee38轉(zhuǎn)成二進(jìn)制,如下:

轉(zhuǎn)成二進(jìn)制.png

可以發(fā)現(xiàn),最高位第64位 (0 - 63),的確是1,說(shuō)明真的使用了Tagged Pointer技術(shù)。

Demo地址:內(nèi)存區(qū)域、Tagged Pointer技術(shù)

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書(shū)系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容