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)存占用來(lá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è)我們通過(guò)NSNumber
對(duì)象存儲(chǔ)一個(gè)NSInteger
的值,系統(tǒng)實(shí)際上會(huì)給我們分配多少內(nèi)存呢?
由于Tagged Pointer
無(wú)法禁用,所以以下將變量i
設(shè)了一個(gè)很大的數(shù),以讓NSNumber
對(duì)象存儲(chǔ)在堆上。
可以通過(guò)設(shè)置環(huán)境變量OBJC_DISABLE_TAGGED_POINTERS
為YES
來(lái)禁用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
還是無(wú)法禁用。
在 64 bit 下,如果沒(méi)有使用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ù)混淆,開(kāi)發(fā)者通過(guò)打印指針無(wú)法判斷它是不是一個(gè)Tagged Pointer
,更無(wú)法讀取Tagged Pointer
的存儲(chǔ)數(shù)據(jù)。
所以在分析Tagged Pointer
之前,我們需要先關(guān)閉Tagged Pointer
的數(shù)據(jù)混淆,以方便我們調(diào)試程序。通過(guò)設(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ù)過(guò)大,指針的8
個(gè)字節(jié)不夠存儲(chǔ),所以在堆中分配了內(nèi)存。
注意: MacOS
與iOS
平臺(tái)下的Tagged Pointer
有差別,下面會(huì)講到。
0x127 中的 2 和 7 表示什么?我們先來(lái)看這個(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ù)第二位用來(lái)表示數(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
位視圖:
接下來(lái)我們來(lái)分析一下Tagged Pointer
在NSString
中的應(yīng)用。同NSNumber
一樣,在64 bit
的MacOS
下,如果一個(gè)NSString
對(duì)象指針為Tagged Pointer
,那么它的后 4 位(0-3)作為標(biāo)識(shí)位,第 4-7 位表示字符串長(zhǎng)度,剩余的 56 位就可以用來(lái)存儲(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é)果來(lái)看,有三種NSString
類型:
類型 | 描述 |
---|---|
__NSCFConstantString | 1. 常量字符串,存儲(chǔ)在字符串常量區(qū),繼承于 __NSCFString。相同內(nèi)容的 __NSCFConstantString 對(duì)象的地址相同,也就是說(shuō)常量字符串對(duì)象是一種單例,可以通過(guò) == 判斷字符串內(nèi)容是否相同。 2. 這種對(duì)象一般通過(guò)字面值@"..." 創(chuàng)建。如果使用 __NSCFConstantString 來(lái)初始化一個(gè)字符串,那么這個(gè)字符串也是相同的 __NSCFConstantString。 |
__NSCFString | 1. 存儲(chǔ)在堆區(qū),需要維護(hù)其引用計(jì)數(shù),繼承于 NSMutableString。 2. 通過(guò)stringWithFormat: 等方法創(chuàng)建的NSString 對(duì)象(且字符串值過(guò)大無(wú)法使用Tagged Pointer 存儲(chǔ))一般都是這種類型。 |
NSTaggedPointerString | Tagged Pointer ,字符串的值直接存儲(chǔ)在了指針上。 |
打印結(jié)果分析:
NSString 對(duì)象 | 類型 | 分析 |
---|---|---|
a | __NSCFConstantString | 通過(guò)字面量@"..." 創(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ù)制出來(lái)是可變對(duì)象,內(nèi)容大小可以擴(kuò)展;而Tagged Pointer 存儲(chǔ)的內(nèi)容大小有限,因此無(wú)法滿足可變對(duì)象的存儲(chǔ)要求。 |
e | __NSCFConstantString | 使用 __NSCFConstantString 來(lái)初始化的字符串 |
f | NSTaggedPointerString | 通過(guò)stringWithFormat: 方法創(chuàng)建,指針足夠存儲(chǔ)字符串的值。 |
string1 | NSTaggedPointerString | 通過(guò)stringWithFormat: 方法創(chuàng)建,指針足夠存儲(chǔ)字符串的值。 |
string2 | NSTaggedPointerString | 通過(guò)stringWithFormat: 方法創(chuàng)建,指針足夠存儲(chǔ)字符串的值。 |
string3 | __NSCFString | 通過(guò)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
代表字符串長(zhǎng)度。
對(duì)于string2
的指針值0x880e28045a54195
,雖然從指針中看不出來(lái)字符串的值,但其也是一個(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)用開(kāi)銷。
內(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ì)象,它沒(méi)有isa
指針,所以如果你直接訪問(wèn)Tagged Pointer
的isa
成員的話,在編譯時(shí)將會(huì)有如下警告:
對(duì)于Tagged Pointer
,應(yīng)該換成相應(yīng)的方法調(diào)用,如isKindOfClass
和object_getClass
。只要避免在代碼中直接訪問(wèn)Tagged Pointer
的isa
,即可避免這個(gè)問(wèn)題。
當(dāng)然現(xiàn)在也不允許我們?cè)诖a中直接訪問(wèn)對(duì)象的isa
了,否則編譯不通過(guò)。
我們通過(guò) 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組件即是用來(lái)創(chuàng)建App中的Tab視圖切換選項(xiàng)欄,下面將詳解iOS App中UiTabBarController組件的基本用法,包括左右滑動(dòng)切換標(biāo)簽頁(yè)等基本功能的實(shí)現(xiàn),需要的朋友可以參考下2016-05-05iOS開(kāi)發(fā)實(shí)現(xiàn)搜索框(UISearchController)
這篇文章主要為大家詳細(xì)介紹了iOS開(kāi)發(fā)實(shí)現(xiàn)搜索框,文中示例代碼介紹的非常詳細(xì),具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下2022-08-08iOS開(kāi)發(fā)之通過(guò)銀行卡號(hào)獲取所屬銀行名稱
本文給大家分享一段代碼關(guān)于ios通過(guò)銀行卡號(hào)獲取所屬銀行名稱,代碼簡(jiǎn)單易懂,在項(xiàng)目開(kāi)發(fā)中經(jīng)常會(huì)遇到這樣的功能,需要的朋友一起學(xué)習(xí)吧2016-11-11IOS 波紋進(jìn)度(waveProgress)動(dòng)畫實(shí)現(xiàn)
這篇文章主要介紹了IOS 紋進(jìn)度(waveProgress)動(dòng)畫實(shí)現(xiàn)的相關(guān)資料,需要的朋友可以參考下2016-09-09iOS實(shí)現(xiàn)自定義購(gòu)物車角標(biāo)顯示購(gòu)物數(shù)量(添加商品時(shí)角標(biāo)抖動(dòng) Vie)
本文主要介紹了iOS實(shí)現(xiàn)自定義購(gòu)物車及角標(biāo)顯示購(gòu)物數(shù)量(添加商品時(shí)角標(biāo)抖動(dòng) Vie)的相關(guān)知識(shí)。具有很好的參考價(jià)值。下面跟著小編一起來(lái)看下吧2017-04-04