iOS內(nèi)存管理Tagged Pointer使用原理詳解
正文
為了節(jié)省內(nèi)存和提高執(zhí)行效率,蘋果在64bit程序中引入了Tagged Pointer技術(shù),用于優(yōu)化NSNumber、NSDate、NSString等小對(duì)象的存儲(chǔ)。在引入 Tagged Pointer 技術(shù)之前,NSNumber等對(duì)象存儲(chǔ)在堆上,NSNumber的指針中存儲(chǔ)的是堆中NSNumber對(duì)象的地址值。
從內(nèi)存占用來看基本數(shù)據(jù)類型所需的內(nèi)存不大。比如NSInteger變量,它所占用的內(nèi)存是與 CPU 的位數(shù)有關(guān),如下。在 32 bit 下占用 4 個(gè)字節(jié),而在 64 bit 下占用 8 個(gè)字節(jié)。指針類型的大小通常也是與 CPU 位數(shù)相關(guān),一個(gè)指針?biāo)?32 bit 下占用 4 個(gè)字節(jié),在 64 bit 下占用 8 個(gè)字節(jié)。
#if __LP64__ || 0 || NS_BUILD_32_LIKE_64 typedef long NSInteger; typedef unsigned long NSUInteger; #else typedef int NSInteger; typedef unsigned int NSUInteger; #endif
假設(shè)我們通過NSNumber對(duì)象存儲(chǔ)一個(gè)NSInteger的值,系統(tǒng)實(shí)際上會(huì)給我們分配多少內(nèi)存呢?
由于Tagged Pointer無法禁用,所以以下將變量i設(shè)了一個(gè)很大的數(shù),以讓NSNumber對(duì)象存儲(chǔ)在堆上。
可以通過設(shè)置環(huán)境變量OBJC_DISABLE_TAGGED_POINTERS為YES來禁用Tagged Pointer,但如果你這么做,運(yùn)行就Crash。
tagged pointers are disabled
因?yàn)?code>Runtime在程序運(yùn)行時(shí)會(huì)判斷Tagged Pointer是否被禁用,如果是的話就會(huì)調(diào)用_objc_fatal()函數(shù)殺死進(jìn)程。所以,雖然蘋果提供了OBJC_DISABLE_TAGGED_POINTERS這個(gè)環(huán)境變量給我們,但是Tagged Pointer還是無法禁用。
在 64 bit 下,如果沒有使用Tagged Pointer的話,為了使用一個(gè)NSNumber對(duì)象就需要 8 個(gè)字節(jié)指針內(nèi)存和 32 個(gè)字節(jié)對(duì)象內(nèi)存。而直接使用一個(gè)NSInteger變量只要 8 個(gè)字節(jié)內(nèi)存,相差好幾倍。
NSNumber等對(duì)象的指針中存儲(chǔ)的數(shù)據(jù)變成了Tag+Data形式(Tag為特殊標(biāo)記,用于區(qū)分NSNumber、NSDate、NSString等對(duì)象類型;Data為對(duì)象的值)。這樣使用一個(gè)NSNumber對(duì)象只需要 8 個(gè)字節(jié)指針內(nèi)存。當(dāng)指針的 8 個(gè)字節(jié)不夠存儲(chǔ)數(shù)據(jù)時(shí),才會(huì)在將對(duì)象存儲(chǔ)在堆上。
Tagged Pointer 的原理
在現(xiàn)在的版本中,為了保證數(shù)據(jù)安全,蘋果對(duì) Tagged Pointer 做了數(shù)據(jù)混淆,開發(fā)者通過打印指針無法判斷它是不是一個(gè)Tagged Pointer,更無法讀取Tagged Pointer的存儲(chǔ)數(shù)據(jù)。
所以在分析Tagged Pointer之前,我們需要先關(guān)閉Tagged Pointer的數(shù)據(jù)混淆,以方便我們調(diào)試程序。通過設(shè)置環(huán)境變量OBJC_DISABLE_TAG_OBFUSCATION為YES。

MacOS 分析
int main(int argc, const char * argv[]) {
@autoreleasepool {
NSNumber *number1 = @1;
NSNumber *number2 = @2;
NSNumber *number3 = @3;
NSNumber *number4 = @(0xFFFFFFFFFFFFFFFF);
NSLog(@"%p %p %p %p", number1, number2, number3, number4);
}
return 0;
}
// 關(guān)閉 Tagged Pointer 數(shù)據(jù)混淆后:0x127 0x227 0x327 0x600003a090e0
// 關(guān)閉 Tagged Pointer 數(shù)據(jù)混淆前:0xaca2838a63a4fb34 0xaca2838a63a4fb04 0xaca2838a63a4fb14 0x600003a090e0
從以上打印結(jié)果可以看出,number1~number3指針為Tagged Pointer類型,可以看到對(duì)象的值都存儲(chǔ)在了指針中,對(duì)應(yīng)0x1、0x2、0x3。而number4由于數(shù)據(jù)過大,指針的8個(gè)字節(jié)不夠存儲(chǔ),所以在堆中分配了內(nèi)存。
注意: MacOS與iOS平臺(tái)下的Tagged Pointer有差別,下面會(huì)講到。
0x127 中的 2 和 7 表示什么?我們先來看這個(gè)7,0x127為十六進(jìn)制表示,7的二進(jìn)制為0111。
最后一位1是Tagged Pointer標(biāo)識(shí)位,代表這個(gè)指針是Tagged Pointer。
前面的011是類標(biāo)識(shí)位,對(duì)應(yīng)十進(jìn)制為3,表示NSNumber類。
備注: MacOS下采用 LSB(Least Significant Bit,即最低有效位)為Tagged Pointer標(biāo)識(shí)位,而iOS下則采用 MSB(Most Significant Bit,即最高有效位)為Tagged Pointer標(biāo)識(shí)位。
可以在Runtime源碼objc4中查看NSNumber、NSDate、NSString等類的標(biāo)識(shí)位。
// objc-internal.h
{
OBJC_TAG_NSAtom = 0,
OBJC_TAG_1 = 1,
OBJC_TAG_NSString = 2,
OBJC_TAG_NSNumber = 3,
OBJC_TAG_NSIndexPath = 4,
OBJC_TAG_NSManagedObjectID = 5,
OBJC_TAG_NSDate = 6,
......
}
0x127 中的 2(即倒數(shù)第二位)又代表什么呢?
倒數(shù)第二位用來表示數(shù)據(jù)類型。
示例:
int main(int argc, const char * argv[]) {
@autoreleasepool {
char a = 1;
short b = 1;
int c = 1;
long d = 1;
float e = 1.0;
double f = 1.00;
NSNumber *number1 = @(a);
NSNumber *number2 = @(b);
NSNumber *number3 = @(c);
NSNumber *number4 = @(d);
NSNumber *number5 = @(e);
NSNumber *number6 = @(f);
NSLog(@"%p %p %p %p %p %p", number1, number2, number3, number4, number5, number6);
}
return 0;
}
// 0x107 0x117 0x127 0x137 0x147 0x157
Tagged Pointer倒數(shù)第二位對(duì)應(yīng)數(shù)據(jù)類型:
| Tagged Pointer 倒數(shù)第二位 | 對(duì)應(yīng)數(shù)據(jù)類型 |
|---|---|
| 0 | char |
| 1 | short |
| 2 | int |
| 3 | long |
| 4 | float |
| 5 | double |
下圖是MacOS下NSNumber的Tagged Pointer位視圖:

接下來我們來分析一下Tagged Pointer在NSString中的應(yīng)用。同NSNumber一樣,在64 bit的MacOS下,如果一個(gè)NSString對(duì)象指針為Tagged Pointer,那么它的后 4 位(0-3)作為標(biāo)識(shí)位,第 4-7 位表示字符串長度,剩余的 56 位就可以用來存儲(chǔ)字符串。
示例:
// MRC 環(huán)境
#define HTLog(_var) \
{ \
NSString *name = @#_var; \
NSLog(@"%@: %p, %@, %lu", name, _var, [_var class], [_var retainCount]); \
}
int main(int argc, const char * argv[]) {
@autoreleasepool {
NSString *a = @"a";
NSMutableString *b = [a mutableCopy];
NSString *c = [a copy];
NSString *d = [[a mutableCopy] copy];
NSString *e = [NSString stringWithString:a];
NSString *f = [NSString stringWithFormat:@"f"];
NSString *string1 = [NSString stringWithFormat:@"abcdefg"];
NSString *string2 = [NSString stringWithFormat:@"abcdefghi"];
NSString *string3 = [NSString stringWithFormat:@"abcdefghij"];
HTLog(a);
HTLog(b);
HTLog(c);
HTLog(d);
HTLog(e);
HTLog(f);
HTLog(string1);
HTLog(string2);
HTLog(string3);
}
return 0;
}
/*
a: 0x100002038, __NSCFConstantString, 18446744073709551615
b: 0x10071f3c0, __NSCFString, 1
c: 0x100002038, __NSCFConstantString, 18446744073709551615
d: 0x6115, NSTaggedPointerString, 18446744073709551615
e: 0x100002038, __NSCFConstantString, 18446744073709551615
f: 0x6615, NSTaggedPointerString, 18446744073709551615
string1: 0x6766656463626175, NSTaggedPointerString, 18446744073709551615
string2: 0x880e28045a54195, NSTaggedPointerString, 18446744073709551615
string3: 0x10071f6d0, __NSCFString, 1 */
從打印結(jié)果來看,有三種NSString類型:
| 類型 | 描述 |
|---|---|
| __NSCFConstantString | 1. 常量字符串,存儲(chǔ)在字符串常量區(qū),繼承于 __NSCFString。相同內(nèi)容的 __NSCFConstantString 對(duì)象的地址相同,也就是說常量字符串對(duì)象是一種單例,可以通過 == 判斷字符串內(nèi)容是否相同。 2. 這種對(duì)象一般通過字面值@"..."創(chuàng)建。如果使用 __NSCFConstantString 來初始化一個(gè)字符串,那么這個(gè)字符串也是相同的 __NSCFConstantString。 |
| __NSCFString | 1. 存儲(chǔ)在堆區(qū),需要維護(hù)其引用計(jì)數(shù),繼承于 NSMutableString。 2. 通過stringWithFormat:等方法創(chuàng)建的NSString對(duì)象(且字符串值過大無法使用Tagged Pointer存儲(chǔ))一般都是這種類型。 |
| NSTaggedPointerString | Tagged Pointer,字符串的值直接存儲(chǔ)在了指針上。 |
打印結(jié)果分析:
| NSString 對(duì)象 | 類型 | 分析 |
|---|---|---|
| a | __NSCFConstantString | 通過字面量@"..."創(chuàng)建 |
| b | __NSCFString | a 的深拷貝,指向不同的內(nèi)存地址,被拷貝到堆區(qū) |
| c | __NSCFConstantString | a 的淺拷貝,指向同一塊內(nèi)存地址 |
| d | NSTaggedPointerString | 單獨(dú)對(duì) a 進(jìn)行 copy(如 c),淺拷貝是指向同一塊內(nèi)存地址,所以不會(huì)產(chǎn)生Tagged Pointer;單獨(dú)對(duì) a 進(jìn)行 mutableCopy(如 b),復(fù)制出來是可變對(duì)象,內(nèi)容大小可以擴(kuò)展;而Tagged Pointer存儲(chǔ)的內(nèi)容大小有限,因此無法滿足可變對(duì)象的存儲(chǔ)要求。 |
| e | __NSCFConstantString | 使用 __NSCFConstantString 來初始化的字符串 |
| f | NSTaggedPointerString | 通過stringWithFormat:方法創(chuàng)建,指針足夠存儲(chǔ)字符串的值。 |
| string1 | NSTaggedPointerString | 通過stringWithFormat:方法創(chuàng)建,指針足夠存儲(chǔ)字符串的值。 |
| string2 | NSTaggedPointerString | 通過stringWithFormat:方法創(chuàng)建,指針足夠存儲(chǔ)字符串的值。 |
| string3 | __NSCFString | 通過stringWithFormat:方法創(chuàng)建,指針不足夠存儲(chǔ)字符串的值。 |
可以看到,為Tagged Pointer的有d、f、string1、string2指針。它們的指針值分別為0x6115、0x6615、0x6766656463626175、0x880e28045a54195。
其中0x61、0x66、0x67666564636261分別對(duì)應(yīng)字符串的 ASCII 碼。
最后一位5的二進(jìn)制為0101,最后一位1是代表這個(gè)指針是Tagged Pointer,010對(duì)應(yīng)十進(jìn)制為2,表示NSString類。
倒數(shù)第二位1、1、7、9代表字符串長度。
對(duì)于string2的指針值0x880e28045a54195,雖然從指針中看不出來字符串的值,但其也是一個(gè)Tagged Pointer。
下圖是MacOS下NSString的Tagged Pointer位視圖:

如何判斷 Tagged Pointer
在objc4源碼中找到判斷Tagged Pointer的函數(shù):
// objc-internal.h
static inline bool
_objc_isTaggedPointer(const void * _Nullable ptr)
{
return ((uintptr_t)ptr & _OBJC_TAG_MASK) == _OBJC_TAG_MASK;
}
可以看到,它是將指針值與一個(gè)_OBJC_TAG_MASK掩碼進(jìn)行按位與運(yùn)算,查看該掩碼:
#if (TARGET_OS_OSX || TARGET_OS_IOSMAC) && __x86_64__
// 64-bit Mac - tag bit is LSB
# define OBJC_MSB_TAGGED_POINTERS 0 // MacOS
#else
// Everything else - tag bit is MSB
# define OBJC_MSB_TAGGED_POINTERS 1 // iOS
#endif
#define _OBJC_TAG_INDEX_MASK 0x7
// array slot includes the tag bit itself
#define _OBJC_TAG_SLOT_COUNT 16
#define _OBJC_TAG_SLOT_MASK 0xf
#define _OBJC_TAG_EXT_INDEX_MASK 0xff
// array slot has no extra bits
#define _OBJC_TAG_EXT_SLOT_COUNT 256
#define _OBJC_TAG_EXT_SLOT_MASK 0xff
#if OBJC_MSB_TAGGED_POINTERS
# define _OBJC_TAG_MASK (1UL<<63) // _OBJC_TAG_MASK
# define _OBJC_TAG_INDEX_SHIFT 60
# define _OBJC_TAG_SLOT_SHIFT 60
# define _OBJC_TAG_PAYLOAD_LSHIFT 4
# define _OBJC_TAG_PAYLOAD_RSHIFT 4
# define _OBJC_TAG_EXT_MASK (0xfUL<<60)
# define _OBJC_TAG_EXT_INDEX_SHIFT 52
# define _OBJC_TAG_EXT_SLOT_SHIFT 52
# define _OBJC_TAG_EXT_PAYLOAD_LSHIFT 12
# define _OBJC_TAG_EXT_PAYLOAD_RSHIFT 12
#else
# define _OBJC_TAG_MASK 1UL // _OBJC_TAG_MASK
# define _OBJC_TAG_INDEX_SHIFT 1
# define _OBJC_TAG_SLOT_SHIFT 0
# define _OBJC_TAG_PAYLOAD_LSHIFT 0
# define _OBJC_TAG_PAYLOAD_RSHIFT 4
# define _OBJC_TAG_EXT_MASK 0xfUL
# define _OBJC_TAG_EXT_INDEX_SHIFT 4
# define _OBJC_TAG_EXT_SLOT_SHIFT 4
# define _OBJC_TAG_EXT_PAYLOAD_LSHIFT 0
# define _OBJC_TAG_EXT_PAYLOAD_RSHIFT 12
#endif
由此我們可以驗(yàn)證:
MacOS下采用 LSB(Least Significant Bit,即最低有效位)為Tagged Pointer標(biāo)識(shí)位;iOS下則采用 MSB(Most Significant Bit,即最高有效位)為Tagged Pointer標(biāo)識(shí)位。
而存儲(chǔ)在堆空間的對(duì)象由于內(nèi)存對(duì)齊,它的內(nèi)存地址的最低有效位為 0。由此可以辨別Tagged Pointer和一般對(duì)象指針。
在objc4源碼中,我們經(jīng)常會(huì)在函數(shù)中看到Tagged Pointer。比如objc_msgSend函數(shù):
ENTRY _objc_msgSend
UNWIND _objc_msgSend, NoFrame
cmp p0, #0 // nil check and tagged pointer check
#if SUPPORT_TAGGED_POINTERS
b.le LNilOrTagged // (MSB tagged pointer looks negative)
#else
b.eq LReturnZero
#endif
ldr p13, [x0] // p13 = isa
GetClassFromIsa_p16 p13 // p16 = class
LGetIsaDone:
// calls imp or objc_msgSend_uncached
CacheLookup NORMAL, _objc_msgSend
#if SUPPORT_TAGGED_POINTERS
LNilOrTagged:
b.eq LReturnZero // nil check
// tagged
adrp x10, _objc_debug_taggedpointer_classes@PAGE
add x10, x10, _objc_debug_taggedpointer_classes@PAGEOFF
ubfx x11, x0, #60, #4
ldr x16, [x10, x11, LSL #3]
adrp x10, _OBJC_CLASS_$___NSUnrecognizedTaggedPointer@PAGE
add x10, x10, _OBJC_CLASS_$___NSUnrecognizedTaggedPointer@PAGEOFF
cmp x10, x16
b.ne LGetIsaDone
// ext tagged
adrp x10, _objc_debug_taggedpointer_ext_classes@PAGE
add x10, x10, _objc_debug_taggedpointer_ext_classes@PAGEOFF
ubfx x11, x0, #52, #8
ldr x16, [x10, x11, LSL #3]
b LGetIsaDone
// SUPPORT_TAGGED_POINTERS
#endif
objc_msgSend能識(shí)別Tagged Pointer,比如NSNumber的intValue方法,直接從指針提取數(shù)據(jù),不會(huì)進(jìn)行objc_msgSend的三大流程,節(jié)省了調(diào)用開銷。
內(nèi)存管理相關(guān)的,如retain方法中調(diào)用的rootRetain:
ALWAYS_INLINE id
objc_object::rootRetain(bool tryRetain, bool handleOverflow)
{
// 如果是 tagged pointer,直接返回 this
if (isTaggedPointer()) return (id)this;
bool sideTableLocked = false;
bool transcribeToSideTable = false;
isa_t oldisa;
isa_t newisa;
......
Tagged Pointer 注意點(diǎn)
我們知道,所有OC對(duì)象都有isa指針,而Tagged Pointer并不是真正的對(duì)象,它沒有isa指針,所以如果你直接訪問Tagged Pointer的isa成員的話,在編譯時(shí)將會(huì)有如下警告:

對(duì)于Tagged Pointer,應(yīng)該換成相應(yīng)的方法調(diào)用,如isKindOfClass和object_getClass。只要避免在代碼中直接訪問Tagged Pointer的isa,即可避免這個(gè)問題。
當(dāng)然現(xiàn)在也不允許我們?cè)诖a中直接訪問對(duì)象的isa了,否則編譯不通過。
我們通過 LLDB 打印Tagged Pointer的isa,會(huì)提示如下錯(cuò)誤:

以上就是iOS內(nèi)存管理Tagged Pointer使用原理詳解的詳細(xì)內(nèi)容,更多關(guān)于iOS內(nèi)存管理Tagged Pointer的資料請(qǐng)關(guān)注腳本之家其它相關(guān)文章!
相關(guān)文章
詳解iOS App中UiTabBarController組件的基本用法
UiTabBarController組件即是用來創(chuàng)建App中的Tab視圖切換選項(xiàng)欄,下面將詳解iOS App中UiTabBarController組件的基本用法,包括左右滑動(dòng)切換標(biāo)簽頁等基本功能的實(shí)現(xiàn),需要的朋友可以參考下2016-05-05
iOS開發(fā)實(shí)現(xiàn)搜索框(UISearchController)
這篇文章主要為大家詳細(xì)介紹了iOS開發(fā)實(shí)現(xiàn)搜索框,文中示例代碼介紹的非常詳細(xì),具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下2022-08-08
IOS 波紋進(jìn)度(waveProgress)動(dòng)畫實(shí)現(xiàn)
這篇文章主要介紹了IOS 紋進(jìn)度(waveProgress)動(dòng)畫實(shí)現(xiàn)的相關(guān)資料,需要的朋友可以參考下2016-09-09
iOS實(shí)現(xiàn)自定義購物車角標(biāo)顯示購物數(shù)量(添加商品時(shí)角標(biāo)抖動(dòng) Vie)
本文主要介紹了iOS實(shí)現(xiàn)自定義購物車及角標(biāo)顯示購物數(shù)量(添加商品時(shí)角標(biāo)抖動(dòng) Vie)的相關(guān)知識(shí)。具有很好的參考價(jià)值。下面跟著小編一起來看下吧2017-04-04

