欧美bbbwbbbw肥妇,免费乱码人妻系列日韩,一级黄片

iOS內(nèi)存管理Tagged Pointer使用原理詳解

 更新時間:2023年01月06日 09:13:08   作者:山海飛鳥  
這篇文章主要為大家介紹了iOS內(nèi)存管理Tagged Pointer使用原理詳解,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進步,早日升職加薪

正文

為了節(jié)省內(nèi)存和提高執(zhí)行效率,蘋果在64bit程序中引入了Tagged Pointer技術(shù),用于優(yōu)化NSNumber、NSDateNSString等小對象的存儲。在引入 Tagged Pointer 技術(shù)之前,NSNumber等對象存儲在堆上,NSNumber的指針中存儲的是堆中NSNumber對象的地址值。

從內(nèi)存占用來看基本數(shù)據(jù)類型所需的內(nèi)存不大。比如NSInteger變量,它所占用的內(nèi)存是與 CPU 的位數(shù)有關(guān),如下。在 32 bit 下占用 4 個字節(jié),而在 64 bit 下占用 8 個字節(jié)。指針類型的大小通常也是與 CPU 位數(shù)相關(guān),一個指針所在 32 bit 下占用 4 個字節(jié),在 64 bit 下占用 8 個字節(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對象存儲一個NSInteger的值,系統(tǒng)實際上會給我們分配多少內(nèi)存呢?
由于Tagged Pointer無法禁用,所以以下將變量i設(shè)了一個很大的數(shù),以讓NSNumber對象存儲在堆上。

可以通過設(shè)置環(huán)境變量OBJC_DISABLE_TAGGED_POINTERSYES來禁用Tagged Pointer,但如果你這么做,運行就Crash

tagged pointers are disabled

因為Runtime在程序運行時會判斷Tagged Pointer是否被禁用,如果是的話就會調(diào)用_objc_fatal()函數(shù)殺死進程。所以,雖然蘋果提供了OBJC_DISABLE_TAGGED_POINTERS這個環(huán)境變量給我們,但是Tagged Pointer還是無法禁用。

在 64 bit 下,如果沒有使用Tagged Pointer的話,為了使用一個NSNumber對象就需要 8 個字節(jié)指針內(nèi)存和 32 個字節(jié)對象內(nèi)存。而直接使用一個NSInteger變量只要 8 個字節(jié)內(nèi)存,相差好幾倍。

NSNumber等對象的指針中存儲的數(shù)據(jù)變成了Tag+Data形式(Tag為特殊標記,用于區(qū)分NSNumber、NSDate、NSString等對象類型;Data為對象的值)。這樣使用一個NSNumber對象只需要 8 個字節(jié)指針內(nèi)存。當指針的 8 個字節(jié)不夠存儲數(shù)據(jù)時,才會在將對象存儲在堆上。

Tagged Pointer 的原理

在現(xiàn)在的版本中,為了保證數(shù)據(jù)安全,蘋果對 Tagged Pointer 做了數(shù)據(jù)混淆,開發(fā)者通過打印指針無法判斷它是不是一個Tagged Pointer,更無法讀取Tagged Pointer的存儲數(shù)據(jù)。

所以在分析Tagged Pointer之前,我們需要先關(guān)閉Tagged Pointer的數(shù)據(jù)混淆,以方便我們調(diào)試程序。通過設(shè)置環(huán)境變量OBJC_DISABLE_TAG_OBFUSCATIONYES。

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類型,可以看到對象的值都存儲在了指針中,對應(yīng)0x1、0x20x3。而number4由于數(shù)據(jù)過大,指針的8個字節(jié)不夠存儲,所以在堆中分配了內(nèi)存。

注意:  MacOSiOS平臺下的Tagged Pointer有差別,下面會講到。

0x127 中的 2 和 7 表示什么?我們先來看這個70x127為十六進制表示,7的二進制為0111。
最后一位1Tagged Pointer標識位,代表這個指針是Tagged Pointer。
前面的011是類標識位,對應(yīng)十進制為3,表示NSNumber類。

備注:  MacOS下采用 LSB(Least Significant Bit,即最低有效位)為Tagged Pointer標識位,而iOS下則采用 MSB(Most Significant Bit,即最高有效位)為Tagged Pointer標識位。

可以在Runtime源碼objc4中查看NSNumber、NSDate、NSString等類的標識位。

// 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ù)第二位對應(yīng)數(shù)據(jù)類型:

Tagged Pointer 倒數(shù)第二位對應(yīng)數(shù)據(jù)類型
0char
1short
2int
3long
4float
5double

下圖是MacOSNSNumberTagged Pointer位視圖:

接下來我們來分析一下Tagged PointerNSString中的應(yīng)用。同NSNumber一樣,在64 bitMacOS下,如果一個NSString對象指針為Tagged Pointer,那么它的后 4 位(0-3)作為標識位,第 4-7 位表示字符串長度,剩余的 56 位就可以用來存儲字符串。

示例:

// 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類型:

類型描述
__NSCFConstantString1. 常量字符串,存儲在字符串常量區(qū),繼承于 __NSCFString。相同內(nèi)容的 __NSCFConstantString 對象的地址相同,也就是說常量字符串對象是一種單例,可以通過 == 判斷字符串內(nèi)容是否相同。 2. 這種對象一般通過字面值@"..."創(chuàng)建。如果使用 __NSCFConstantString 來初始化一個字符串,那么這個字符串也是相同的 __NSCFConstantString。
__NSCFString1. 存儲在堆區(qū),需要維護其引用計數(shù),繼承于 NSMutableString。 2. 通過stringWithFormat:等方法創(chuàng)建的NSString對象(且字符串值過大無法使用Tagged Pointer存儲)一般都是這種類型。
NSTaggedPointerStringTagged Pointer,字符串的值直接存儲在了指針上。

打印結(jié)果分析:

NSString 對象類型分析
a__NSCFConstantString通過字面量@"..."創(chuàng)建
b__NSCFStringa 的深拷貝,指向不同的內(nèi)存地址,被拷貝到堆區(qū)
c__NSCFConstantStringa 的淺拷貝,指向同一塊內(nèi)存地址
dNSTaggedPointerString單獨對 a 進行 copy(如 c),淺拷貝是指向同一塊內(nèi)存地址,所以不會產(chǎn)生Tagged Pointer;單獨對 a 進行 mutableCopy(如 b),復(fù)制出來是可變對象,內(nèi)容大小可以擴展;而Tagged Pointer存儲的內(nèi)容大小有限,因此無法滿足可變對象的存儲要求。
e__NSCFConstantString使用 __NSCFConstantString 來初始化的字符串
fNSTaggedPointerString通過stringWithFormat:方法創(chuàng)建,指針足夠存儲字符串的值。
string1NSTaggedPointerString通過stringWithFormat:方法創(chuàng)建,指針足夠存儲字符串的值。
string2NSTaggedPointerString通過stringWithFormat:方法創(chuàng)建,指針足夠存儲字符串的值。
string3__NSCFString通過stringWithFormat:方法創(chuàng)建,指針不足夠存儲字符串的值。

可以看到,為Tagged Pointer的有d、f、string1、string2指針。它們的指針值分別為
0x6115、0x6615、0x6766656463626175、0x880e28045a54195

其中0x610x66、0x67666564636261分別對應(yīng)字符串的 ASCII 碼。

最后一位5的二進制為0101,最后一位1是代表這個指針是Tagged Pointer,010對應(yīng)十進制為2,表示NSString類。

倒數(shù)第二位1、1、7、9代表字符串長度。

對于string2的指針值0x880e28045a54195,雖然從指針中看不出來字符串的值,但其也是一個Tagged Pointer。

下圖是MacOSNSStringTagged 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;
}

可以看到,它是將指針值與一個_OBJC_TAG_MASK掩碼進行按位與運算,查看該掩碼:

#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

由此我們可以驗證:

  • MacOS下采用 LSB(Least Significant Bit,即最低有效位)為Tagged Pointer標識位;
  • iOS下則采用 MSB(Most Significant Bit,即最高有效位)為Tagged Pointer標識位。

而存儲在堆空間的對象由于內(nèi)存對齊,它的內(nèi)存地址的最低有效位為 0。由此可以辨別Tagged Pointer和一般對象指針。

objc4源碼中,我們經(jīng)常會在函數(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能識別Tagged Pointer,比如NSNumberintValue方法,直接從指針提取數(shù)據(jù),不會進行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 注意點

我們知道,所有OC對象都有isa指針,而Tagged Pointer并不是真正的對象,它沒有isa指針,所以如果你直接訪問Tagged Pointerisa成員的話,在編譯時將會有如下警告:

對于Tagged Pointer,應(yīng)該換成相應(yīng)的方法調(diào)用,如isKindOfClassobject_getClass。只要避免在代碼中直接訪問Tagged Pointerisa,即可避免這個問題。

當然現(xiàn)在也不允許我們在代碼中直接訪問對象的isa了,否則編譯不通過。

我們通過 LLDB 打印Tagged Pointerisa,會提示如下錯誤:

以上就是iOS內(nèi)存管理Tagged Pointer使用原理詳解的詳細內(nèi)容,更多關(guān)于iOS內(nèi)存管理Tagged Pointer的資料請關(guān)注腳本之家其它相關(guān)文章!

相關(guān)文章

最新評論