簡介:
??在objc4源碼中,我們經(jīng)常會在函數(shù)中看到Tagged Pointer;Tagged Pointer究竟是何方神圣?有什么作用呢?本篇文章用于記錄我對Tagged Pointer的理解。
Tagged Pointer主要為了解決兩個問題:
內(nèi)存資源浪費,堆區(qū)需要額外的開辟空間:
??例如NSNumber對象,其值是一個整數(shù)。正常情況下,如果這個整數(shù)只是一個NSInteger的普通變量,那么它所占用的內(nèi)存是與CPU的位數(shù)有關(guān),在32位CPU下占4個字節(jié),在64位CPU下是占8個字節(jié)的。而指針類型的大小通常也是與CPU位數(shù)相關(guān),一個指針所占用的內(nèi)存在32位CPU下為 4 個字節(jié),在64位CPU下也是8個字節(jié)。
??所以一個普通的 iOS 程序,如果沒有Tagged Pointer對象,從 32 位機器遷移到64位機器中后,雖然邏輯沒有任何變化,但這種NSNumber、NSDate一類的對象所占用的內(nèi)存會翻倍。訪問效率,每次set/get都需要訪問堆區(qū),浪費時間,而且需要管理堆區(qū)對象的聲明周期,降低效率:
??為了改進上述提到的內(nèi)存占用和效率問題,所以蘋果提出了Tagged Pointer對象。對于某些占用內(nèi)存很小的數(shù)據(jù)對象,不再單獨開辟空間去存儲,而是將實際的實例值存儲在對象的指針中,同時對該指針進行標記,用于區(qū)分正常的指針指向!
??由于NSNumber、NSDate類的變量本身的值需要占用的內(nèi)存大小常常不需要8個字節(jié),拿整數(shù)來說,4個字節(jié)所能表示的有符號整數(shù)就可以達到20多億(注:2^31=2147483648,另外1位作為符號位),對于絕大多數(shù)情況都是可以處理的。所以蘋果將一個對象的指針拆成兩部分,一部分直接保存數(shù)據(jù)(即下面所說的Data部分),另一部分作為特殊標記(即下面所說的Tag部分),表示這是一個特別的指針,不指向任何一個對象的地址。
Tagged Pointer內(nèi)存管理:
普通類內(nèi)存管理:
??總空間 = 棧指針空間(isa普通類指針) + 堆中分配的空間
??指針變量指向堆分配的內(nèi)存空間,需要動態(tài)分配內(nèi)存,進行引用計數(shù)機制,控制對象堆內(nèi)存的管理。
Tagged Pointe內(nèi)存管理:
??總空間 = 棧指針空間(Tagged Pointer)
??棧指針空間 = 對象的特殊標記(Tag) + 對象的值(Data)
??對象的特殊標記(Tag),總共占有四個二進制位;在ARM64架構(gòu)中最高位的二進制四位用于區(qū)分Tagged Pointer和普通指針的區(qū)別,其它三位用于區(qū)分NSNumber、NSDate、NSString等對象類型
??Data為對象的值
??指針變量包含值,不用進行引用計數(shù)機制對內(nèi)存管理,所以不需要retain,release操作。
Tagged Pointer總結(jié):
-
Tagged Pointer指向的并不是一個類,它是適用于64位處理器的一個內(nèi)存優(yōu)化機制,專門用來存儲小對象,當存儲不下時,則轉(zhuǎn)為對象,例如NSNumber,NSDate,NSString; - 指針的值不再是地址了,而是包含真正值且經(jīng)過特殊處理過的指針。所以,實際上它不再是一個對象了,它只是一個披著對象外衣的普通變量而已。它的內(nèi)存并不存儲在堆中,不需要動態(tài)分配內(nèi)存、維護引用計數(shù)、管理它的生命周期等
也不需要方法調(diào)用時執(zhí)行objc_msgSend流程(消息發(fā)送、動態(tài)方法解析、消息轉(zhuǎn)發(fā)); - 在內(nèi)存讀取上有著3倍的效率,創(chuàng)建時比以前快106倍;
-
Tagged Pointer指針指向的不在是一個類,所以無法直接訪問isa指針。
Tagged Pointer原理和底層實現(xiàn):
1.Xcode設(shè)置環(huán)境變量
??Xcode默認情況下對Tagged Pointer進行了混淆;設(shè)置環(huán)境變量OBJC_DISABLE_TAG_OBFUSCATION為YES, 可以關(guān)閉 Tagged Pointer的數(shù)據(jù)混淆。
設(shè)置環(huán)境變量的步驟:
Edit Scheme -> Run Debug -> Arguments -> Environment Variables -> + -> OBJC_DISABLE_TAG_OBFUSCATION -> YES
代碼如下:
- (void)taggedPointerNumber {
NSNumber *number1 = @(0x1);
NSNumber *number2 = @(0x20);
NSNumber *number3 = @(0x3F);
NSNumber *number4 = @(0xFFFFFFFFFFEFE);
NSNumber *maxNum = @(MAXFLOAT);
// 使用了Tagged Pointer,NSNumber對象的值直接存儲在了指針上,不會在堆上申請內(nèi)存。則使用一個NSNumber對象只需要指針的 8 個字節(jié)內(nèi)存就夠了,大大的節(jié)省了內(nèi)存占用。
// NSLog(@"%zd", malloc_size(number1);
NSInteger number1Size = malloc_size((__bridge const void *)(number1));
NSLog(@"占用堆內(nèi)存=%zd", number1Size);
NSLog(@"指針內(nèi)存=%zd", sizeof(number1));
// NSNumber普通對象,會在堆上申請內(nèi)存
NSInteger maxNumSize = malloc_size((__bridge const void *)(maxNum));
NSLog(@"占用堆內(nèi)存=%zd", maxNumSize);
NSLog(@"指針內(nèi)存=%zd", sizeof(maxNum));
NSLog(@"%p %@ %@", number1, number1, number1.class);
NSLog(@"%p %@ %@", number2, number2, number2.class);
NSLog(@"%p %@ %@", number3, number3, number3.class);
NSLog(@"%p %@ %@", number4, number4, number4.class);
NSLog(@"%p %@ %@", maxNum, maxNum, maxNum.class);
}
/// ARM64開啟混淆打?。?占用堆內(nèi)存=0
指針內(nèi)存=8
占用堆內(nèi)存=32
指針內(nèi)存=8
0x8b33564c2d3526b5 1 __NSCFNumber
0x8b33564c2d3524a5 32 __NSCFNumber
0x8b33564c2d352555 63 __NSCFNumber
0x8bcca9b3d2cac944 4503599627370238 __NSCFNumber
0x282dc8760 3.402823e+38 __NSCFNumber
/// ARM64關(guān)閉混淆打?。?占用堆內(nèi)存=0
指針內(nèi)存=8
占用堆內(nèi)存=32
指針內(nèi)存=8
0xb000000000000012 1 __NSCFNumber
0xb000000000000202 32 __NSCFNumber
0xb0000000000003f2 63 __NSCFNumber
0xb0ffffffffffefe3 4503599627370238 __NSCFNumber
0x281a2afe0 3.402823e+38 __NSCFNumber
1.內(nèi)存
??number1只有棧上的指針內(nèi)存;而maxNum不僅有指針內(nèi)存,在堆中還分配了32字節(jié)的內(nèi)存用于存儲該變量的值。
2.指針
| 變量 | 指針值 | 10進制數(shù)值 |
|---|---|---|
| number1 | 0xb000000000000012 | 1 |
| number2 | 0xb000000000000202 | 32 |
| number3 | 0xb0000000000003f2 | 63 |
| number4 | 0xb0ffffffffffefe3 | 4503599627370238 |
| maxNum | 0x281a2afe0 | 3.402823e+38 |
??通過觀察發(fā)現(xiàn),對象的number1、number2、number3、number4都存儲在了對應的指針中;而maxNum不同由于數(shù)據(jù)過大,導致無法 1 個指針 8 個字節(jié)的內(nèi)存根本存不下,而申請了32字節(jié)堆內(nèi)存。
3.Tagged Pointer和isa

??打斷點,從上圖可以看出,
number1、number2、number3、number4的isa指向了0x0(即nil),是Tagged Pointer指針;maxNum指向了NSNumber類的isa指針。4.
Tagged Pointer位解析??以
number1的Tagged Pointer指針為例:
高位 <-- 低位
0xb000000000000012
4.1最高位解析:
??0x為16進制標識符,在16進制中,一位數(shù)字代表二進制中的四位;
在ARM64架構(gòu)下,Tagged Pointer的標識為二進制的最高位的四位,也就是16進制表示的從左向右的第一位:
/// 16進制的第一位,也是最高位
b
/// 16進制的b轉(zhuǎn)換為二進制的四位,也是指針二進制最高四位
1011
??其中二進制中最高位是Tagged Pointer標識位,如例子中的的1,表示該指針是Tagged Pointer的指針;其它三位表示支持Tagged Pointer的類標識位,如例子中011,轉(zhuǎn)化為10進制就是3,3在支持Tagged Pointer的系統(tǒng)類數(shù)組中代表NSNumber類。
runtime源碼objc-internal.h中有關(guān)支持Tagged Pointer類的標志定義如下:
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,
4.2末尾解析:
??Tagged Pointer16進制的末尾也就是二進制的低位的四位,表示的Tagged Pointer存儲數(shù)據(jù)的類型標識符,例如number1末尾的2表示Tagged Pointer存儲的是int的數(shù)據(jù)。
| 數(shù)據(jù)類型 | 標識符 |
|---|---|
| char | 0 |
| short | 1 |
| int | 2 |
| long int | 3 |
| float | 4 |
| double | 5 |
??除去高位和低位的標識位,中間這一部分才是真正存儲值的區(qū)域。
注意:
-
NSString類型的Tagged Pointer指針與基本類型的指針是不一樣的,末尾的數(shù)字為字符串的長度; -
NSString類型的Tagged Pointer指針存儲char類型,返回的是ASCII碼(該值為16進制的,需要進行十進制轉(zhuǎn)換)
- (void)taggedPointerString {
NSMutableString *mutableStr = [NSMutableString string];
NSString *immutable = nil;
#define _OBJC_TAG_MASK (1UL<<63)
char c = 'a';
do {
[mutableStr appendFormat:@"%c", c++];
immutable = [mutableStr copy];
NSLog(@"%p %@ %@", immutable, immutable, immutable.class);
}while(((uintptr_t)immutable & _OBJC_TAG_MASK) == _OBJC_TAG_MASK);
}
打印信息:
0xa000000000000611 a NSTaggedPointerString
0xa000000000062612 ab NSTaggedPointerString
0xa000000006362613 abc NSTaggedPointerString
0xa000000646362614 abcd NSTaggedPointerString
0xa000065646362615 abcde NSTaggedPointerString
0xa006665646362616 abcdef NSTaggedPointerString
0xa676665646362617 abcdefg NSTaggedPointerString
0xa0022038a0116958 abcdefgh NSTaggedPointerString
0xa0880e28045a5419 abcdefghi NSTaggedPointerString
0x280e3cb40 abcdefghij __NSCFString
Tagged Pointer標識位、類標識、數(shù)據(jù)類型做代碼驗證
1.Tagged Pointer標識位
??Tagged Pointer標識位即判斷是否是Tagged Pointer指針的表示方法。該篇文章源碼版本為objc4-818.2。
??在源碼objc_internal.h中可以找到判斷Tagged Pointer標識位的方法,如下代碼:
static inline bool
_objc_isTaggedPointer(const void * _Nullable ptr)
{
return ((uintptr_t)ptr & _OBJC_TAG_MASK) == _OBJC_TAG_MASK;
}
??上面的代碼將指針ptr和_OBJC_TAG_MASK掩碼進行位與操作。這個掩碼_OBJC_TAG_MASK的源碼同樣在objc_internal.h中可以找到:
#if (TARGET_OS_OSX || TARGET_OS_MACCATALYST) && __x86_64__
// 64-bit Mac - tag bit is LSB
# define OBJC_MSB_TAGGED_POINTERS 0
#else
// Everything else - tag bit is MSB
# define OBJC_MSB_TAGGED_POINTERS 1
#endif
#if OBJC_SPLIT_TAGGED_POINTERS
# define _OBJC_TAG_MASK (1UL<<63)
#elif OBJC_MSB_TAGGED_POINTERS
# define _OBJC_TAG_MASK (1UL<<63)
#else
# define _OBJC_TAG_MASK 1UL
根據(jù)源碼得知:
MacOS(x86_64和ARM64 M芯片)下采用 LSB(Least Significant Bit,即最低有效位)為Tagged Pointer標識位;(define _OBJC_TAG_MASK 1UL)
iOS(ARM64 A芯片)下則采用 MSB(Most Significant Bit,即最高有效位)為Tagged Pointer標識位。(define _OBJC_TAG_MASK (1UL<<63))
2. Tagged Pointer類標識
在源碼objc_internal.h中可以查看到NSNumber、NSDate、NSString等類的標識位,如下:
// 60-bit payloads
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,
// 60-bit reserved
OBJC_TAG_RESERVED_7 = 7,
3. Tagged Pointer數(shù)據(jù)類型
Tagged Pointer16進制的最后一位(即2進制的最后四位)表示數(shù)據(jù)類型,例子如上述代碼。見4.2末尾解析。