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

驗(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):
- 上面的內(nèi)存地址從小到大。
- 字符串常量一樣的時(shí)候,指針指向的內(nèi)存地址也一樣,說(shuō)明保存在內(nèi)存中只有一份內(nèi)存。
- 棧里面內(nèi)存分配的確是從大到小的。先分配e,再分配f,所以e內(nèi)存地址比f(wàn)大。
- 堆和棧地址位數(shù)比較多,說(shuō)明系統(tǒng)給堆、棧預(yù)留的空間比較大。
二. Tagged Pointer技術(shù)
1. 關(guān)于Tagged Pointer
- 從64bit開(kāi)始,iOS引入了Tagged Pointer技術(shù),用于優(yōu)化NSNumber、NSDate、NSString等小對(duì)象的存儲(chǔ),Tagged Pointer技術(shù)是編譯器幫我們做的。
- 在沒(méi)有使用Tagged Pointer之前, NSNumber等對(duì)象需要?jiǎng)討B(tài)分配內(nèi)存、維護(hù)引用計(jì)數(shù)等,NSNumber指針存儲(chǔ)的是堆中NSNumber對(duì)象的地址值。
- 使用Tagged Pointer之后,NSNumber指針里面存儲(chǔ)的數(shù)據(jù)變成了:Tag + Data,也就是將數(shù)據(jù)直接存儲(chǔ)在了指針中,其中Tag是標(biāo)記是什么類(lèi)型,比如NSNumber、NSDate、NSString等。
- 當(dāng)指針不夠存儲(chǔ)數(shù)據(jù)時(shí),才會(huì)使用動(dòng)態(tài)分配內(nèi)存的方式來(lái)存儲(chǔ)數(shù)據(jù)。
- 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)存。
- 如何判斷一個(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)存。

可以發(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)題。

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. 如何解決呢?
改用atomic修飾,不推薦,具體可參考:加鎖方案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)制,如下:

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