九久 發表於 2023-1-6 09:13:08

iOS内存管理Tagged Pointer使用原理详解

<div id="navCategory"><h5 class="catalogue">目录</h5><ul class="first_class_ul"><li>正文</li><ul class="second_class_ul"><li>Tagged Pointer 的原理</li><li>MacOS 分析</li><li>如何判断 Tagged Pointer</li><li>&nbsp;Tagged Pointer 注意点</li></ul></ul></div><p class="maodian"></p><h2>正文</h2>
<div class="cros igoods"><div class="goodsin" data-img="https://img14.360buyimg.com/pop/jfs/t6586/118/178402578/56244/59c7902e/593acd1cNb1b001a9.jpg" data-name="iOS开发指南 从Hello World到App Store上架 第5版(图灵出品)" data-owner="京东自营" data-price="119" data-tgid="38" data-url="https://union-click.jd.com/jdc?e=&amp;p=JF8BAL4JK1olXDYCV19VDkkSAV9MRANLAjZbERscSkAJHTdNTwcKBlMdBgABFksUAmcOGV4XQl9HCANtd0pNaz8LTTh2AEIEEF4OCjYScD1Aa1cZbQEHU1tVCk4UM28LHVwVXAMCZG5dCXtBbW8JGloUXAMGU1ltCXsXBGkLE1wTXgMAUF9dOEsfB19LTx5BHkVKZG5tC3snM284GGtLMwdRUlxcWx9CbTJaQB5NXlkFOl5dCEsSBG0PGmsXXAcAVm5t"></div></div>
<p>为了节省内存和提高执行效率,苹果在<code>64bit</code>程序中引入了<code>Tagged Pointer</code>技术,用于优化<code>NSNumber</code>、<code>NSDate</code>、<code>NSString</code>等小对象的存储。在引入 Tagged Pointer 技术之前,<code>NSNumber</code>等对象存储在堆上,<code>NSNumber</code>的指针中存储的是堆中<code>NSNumber</code>对象的地址值。</p>
<p>从内存占用来看基本数据类型所需的内存不大。比如<code>NSInteger</code>变量,它所占用的内存是与 CPU 的位数有关,如下。在 32 bit 下占用 4 个字节,而在 64 bit 下占用 8 个字节。指针类型的大小通常也是与 CPU 位数相关,一个指针所在 32 bit 下占用 4 个字节,在 64 bit 下占用 8 个字节。</p>
<div class="jb51code"><pre class="brush:cpp;">#if __LP64__ || 0 || NS_BUILD_32_LIKE_64
typedef long NSInteger;
typedef unsigned long NSUInteger;
#else
typedef int NSInteger;
typedef unsigned int NSUInteger;
#endif
</pre></div>
<p>假设我们通过<code>NSNumber</code>对象存储一个<code>NSInteger</code>的值,系统实际上会给我们分配多少内存呢?<br />由于<code>Tagged Pointer</code>无法禁用,所以以下将变量<code>i</code>设了一个很大的数,以让<code>NSNumber</code>对象存储在堆上。</p>
<p>可以通过设置环境变量<code>OBJC_DISABLE_TAGGED_POINTERS</code>为<code>YES</code>来禁用<code>Tagged Pointer</code>,但如果你这么做,运行就<code>Crash</code>。</p>
<p><code>tagged pointers are disabled</code></p>
<p>因为<code>Runtime</code>在程序运行时会判断<code>Tagged Pointer</code>是否被禁用,如果是的话就会调用<code>_objc_fatal()</code>函数杀死进程。所以,虽然苹果提供了<code>OBJC_DISABLE_TAGGED_POINTERS</code>这个环境变量给我们,但是<code>Tagged Pointer</code>还是无法禁用。</p>
<p>在 64 bit 下,如果没有使用<code>Tagged Pointer</code>的话,为了使用一个<code>NSNumber</code>对象就需要 8 个字节指针内存和 32 个字节对象内存。而直接使用一个<code>NSInteger</code>变量只要 8 个字节内存,相差好几倍。</p>
<p><code>NSNumber</code>等对象的指针中存储的数据变成了<code>Tag</code>+<code>Data</code>形式(<code>Tag</code>为特殊标记,用于区分<code>NSNumber</code>、<code>NSDate</code>、<code>NSString</code>等对象类型;<code>Data</code>为对象的值)。这样使用一个<code>NSNumber</code>对象只需要 8 个字节指针内存。当指针的 8 个字节不够存储数据时,才会在将对象存储在堆上。</p>
<p class="maodian"></p><h3>Tagged Pointer 的原理</h3>
<p>在现在的版本中,为了保证数据安全,苹果对 Tagged Pointer 做了数据混淆,开发者通过打印指针无法判断它是不是一个<code>Tagged Pointer</code>,更无法读取<code>Tagged Pointer</code>的存储数据。</p>
<p>所以在分析<code>Tagged Pointer</code>之前,我们需要先关闭<code>Tagged Pointer</code>的数据混淆,以方便我们调试程序。通过设置环境变量<code>OBJC_DISABLE_TAG_OBFUSCATION</code>为<code>YES</code>。</p>
<p style="text-align:center"><img alt="" src="https://img.jbzj.com/file_images/article/202301/2023010609040401.jpg" /></p>
<p class="maodian"></p><h3>MacOS 分析</h3>
<div class="jb51code"><pre class="brush:cpp;">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;
}
// 关闭 Tagged Pointer 数据混淆后:0x127 0x227 0x327 0x600003a090e0
// 关闭 Tagged Pointer 数据混淆前:0xaca2838a63a4fb34 0xaca2838a63a4fb04 0xaca2838a63a4fb14 0x600003a090e0
</pre></div>
<p>从以上打印结果可以看出,<code>number1~number3</code>指针为<code>Tagged Pointer</code>类型,可以看到对象的值都存储在了指针中,对应<code>0x1</code>、<code>0x2</code>、<code>0x3</code>。而<code>number4</code>由于数据过大,指针的<code>8</code>个字节不够存储,所以在堆中分配了内存。</p>
<p><strong>注意:</strong> &nbsp;<code>MacOS</code>与<code>iOS</code>平台下的<code>Tagged Pointer</code>有差别,下面会讲到。</p>
<p><strong>0x127 中的 2 和 7 表示什么?</strong>我们先来看这个<code>7</code>,<code>0x127</code>为十六进制表示,<code>7</code>的二进制为<code>0111</code>。<br />最后一位<code>1</code>是<code>Tagged Pointer</code>标识位,代表这个指针是<code>Tagged Pointer</code>。<br />前面的<code>011</code>是类标识位,对应十进制为<code>3</code>,表示<code>NSNumber</code>类。</p>
<p><strong>备注:</strong> &nbsp;<code>MacOS</code>下采用 LSB(Least Significant Bit,即最低有效位)为<code>Tagged Pointer</code>标识位,而<code>iOS</code>下则采用 MSB(Most Significant Bit,即最高有效位)为<code>Tagged Pointer</code>标识位。</p>
<p>可以在<code>Runtime</code>源码<code>objc4</code>中查看<code>NSNumber</code>、<code>NSDate</code>、<code>NSString</code>等类的标识位。</p>
<div class="jb51code"><pre class="brush:cpp;">// 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,
    ......
}
</pre></div>
<p>0x127 中的 2(即倒数第二位)又代表什么呢?</p>
<p>倒数第二位用来表示数据类型。</p>
<p>示例:</p>
<div class="jb51code"><pre class="brush:cpp;">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
</pre></div>
<p><code>Tagged Pointer</code>倒数第二位对应数据类型:</p>
<table><tbody><tr><th>Tagged Pointer 倒数第二位</th><th>对应数据类型</th></tr><tr><td>0</td><td>char</td></tr><tr><td>1</td><td>short</td></tr><tr><td>2</td><td>int</td></tr><tr><td>3</td><td>long</td></tr><tr><td>4</td><td>float</td></tr><tr><td>5</td><td>double</td></tr></tbody></table>
<p>下图是<code>MacOS</code>下<code>NSNumber</code>的<code>Tagged Pointer</code>位视图:</p>
<p style="text-align:center"><img alt="" src="https://img.jbzj.com/file_images/article/202301/2023010609040402.jpg" /></p>
<p>接下来我们来分析一下<code>Tagged Pointer</code>在<code>NSString</code>中的应用。同<code>NSNumber</code>一样,在<code>64 bit</code>的<code>MacOS</code>下,如果一个<code>NSString</code>对象指针为<code>Tagged Pointer</code>,那么它的后 4 位(0-3)作为标识位,第 4-7 位表示字符串长度,剩余的 56 位就可以用来存储字符串。</p>
<p>示例:</p>
<div class="jb51code"><pre class="brush:cpp;">// MRC 环境
#define HTLog(_var) \
{ \
    NSString *name = @#_var; \
    NSLog(@"%@: %p, %@, %lu", name, _var, , ); \
}
int main(int argc, const char * argv[]) {
    @autoreleasepool {
      NSString *a = @"a";
      NSMutableString *b = ;
      NSString *c = ;
      NSString *d = [ copy];
      NSString *e = ;
      NSString *f = ;
      NSString *string1 = ;
      NSString *string2 = ;
      NSString *string3 = ;
      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 */
</pre></div>
<p>从打印结果来看,有三种<code>NSString</code>类型:</p>
<table><tbody><tr><th>类型</th><th>描述</th></tr><tr><td>__NSCFConstantString</td><td>1. 常量字符串,存储在字符串常量区,继承于 __NSCFString。相同内容的 __NSCFConstantString 对象的地址相同,也就是说常量字符串对象是一种单例,可以通过 == 判断字符串内容是否相同。 2. 这种对象一般通过字面值<code>@&quot;...&quot;</code>创建。如果使用 __NSCFConstantString 来初始化一个字符串,那么这个字符串也是相同的 __NSCFConstantString。</td></tr><tr><td>__NSCFString</td><td>1. 存储在堆区,需要维护其引用计数,继承于 NSMutableString。 2. 通过<code>stringWithFormat:</code>等方法创建的<code>NSString</code>对象(且字符串值过大无法使用<code>Tagged Pointer</code>存储)一般都是这种类型。</td></tr><tr><td>NSTaggedPointerString</td><td><code>Tagged Pointer</code>,字符串的值直接存储在了指针上。</td></tr></tbody></table>
<p>打印结果分析:</p>
<table><tbody><tr><th>NSString 对象</th><th>类型</th><th>分析</th></tr><tr><td>a</td><td>__NSCFConstantString</td><td>通过字面量<code>@&quot;...&quot;</code>创建</td></tr><tr><td>b</td><td>__NSCFString</td><td>a 的深拷贝,指向不同的内存地址,被拷贝到堆区</td></tr><tr><td>c</td><td>__NSCFConstantString</td><td>a 的浅拷贝,指向同一块内存地址</td></tr><tr><td>d</td><td>NSTaggedPointerString</td><td>单独对 a 进行 copy(如 c),浅拷贝是指向同一块内存地址,所以不会产生<code>Tagged Pointer</code>;单独对 a 进行 mutableCopy(如 b),复制出来是可变对象,内容大小可以扩展;而<code>Tagged Pointer</code>存储的内容大小有限,因此无法满足可变对象的存储要求。</td></tr><tr><td>e</td><td>__NSCFConstantString</td><td>使用 __NSCFConstantString 来初始化的字符串</td></tr><tr><td>f</td><td>NSTaggedPointerString</td><td>通过<code>stringWithFormat:</code>方法创建,指针足够存储字符串的值。</td></tr><tr><td>string1</td><td>NSTaggedPointerString</td><td>通过<code>stringWithFormat:</code>方法创建,指针足够存储字符串的值。</td></tr><tr><td>string2</td><td>NSTaggedPointerString</td><td>通过<code>stringWithFormat:</code>方法创建,指针足够存储字符串的值。</td></tr><tr><td>string3</td><td>__NSCFString</td><td>通过<code>stringWithFormat:</code>方法创建,指针不足够存储字符串的值。</td></tr></tbody></table>
<p>可以看到,为<code>Tagged Pointer</code>的有<code>d</code>、<code>f</code>、<code>string1</code>、<code>string2</code>指针。它们的指针值分别为<br /><code>0x6115</code>、<code>0x6615</code>、<code>0x6766656463626175</code>、<code>0x880e28045a54195</code>。</p>
<p>其中<code>0x61</code>、<code>0x66</code>、<code>0x67666564636261</code>分别对应字符串的 ASCII 码。</p>
<p>最后一位<code>5</code>的二进制为<code>0101</code>,最后一位<code>1</code>是代表这个指针是<code>Tagged Pointer</code>,<code>010</code>对应十进制为<code>2</code>,表示<code>NSString</code>类。</p>
<p>倒数第二位<code>1</code>、<code>1</code>、<code>7</code>、<code>9</code>代表字符串长度。</p>
<p>对于<code>string2</code>的指针值<code>0x880e28045a54195</code>,虽然从指针中看不出来字符串的值,但其也是一个<code>Tagged Pointer</code>。</p>
<p>下图是<code>MacOS</code>下<code>NSString</code>的<code>Tagged Pointer</code>位视图:</p>
<p style="text-align:center"><img alt="" src="https://img.jbzj.com/file_images/article/202301/2023010609040403.jpg" /></p>
<p class="maodian"></p><h3>如何判断 Tagged Pointer</h3>
<p>在<code>objc4</code>源码中找到判断<code>Tagged Pointer</code>的函数:</p>
<div class="jb51code"><pre class="brush:cpp;">// objc-internal.h
static inline bool
_objc_isTaggedPointer(const void * _Nullable ptr)
{
    return ((uintptr_t)ptr &amp;amp; _OBJC_TAG_MASK) == _OBJC_TAG_MASK;
}
</pre></div>
<p>可以看到,它是将指针值与一个<code>_OBJC_TAG_MASK</code>掩码进行按位与运算,查看该掩码:</p>
<div class="jb51code"><pre class="brush:cpp;">#if (TARGET_OS_OSX || TARGET_OS_IOSMAC) &amp;amp;&amp;amp; __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&amp;lt;&amp;lt;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&amp;lt;&amp;lt;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
</pre></div>
<p>由此我们可以验证:</p>
<ul><li><code>MacOS</code>下采用 LSB(Least Significant Bit,即最低有效位)为<code>Tagged Pointer</code>标识位;</li><li><code>iOS</code>下则采用 MSB(Most Significant Bit,即最高有效位)为<code>Tagged Pointer</code>标识位。</li></ul>
<p>而存储在堆空间的对象由于内存对齐,它的内存地址的最低有效位为 0。由此可以辨别<code>Tagged Pointer</code>和一般对象指针。</p>
<p>在<code>objc4</code>源码中,我们经常会在函数中看到<code>Tagged Pointer</code>。比如<code>objc_msgSend</code>函数:</p>
<div class="jb51code"><pre class="brush:cpp;">    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,        // 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,
    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,
    b   LGetIsaDone
// SUPPORT_TAGGED_POINTERS
#endif
</pre></div>
<p><code>objc_msgSend</code>能识别<code>Tagged Pointer</code>,比如<code>NSNumber</code>的<code>intValue</code>方法,直接从指针提取数据,不会进行<code>objc_msgSend</code>的三大流程,节省了调用开销。</p>
<p>内存管理相关的,如<code>retain</code>方法中调用的<code>rootRetain</code>:</p>
<div class="jb51code"><pre class="brush:cpp;">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;
    ......
</pre></div>
<p class="maodian"></p><h3>&nbsp;Tagged Pointer 注意点</h3>
<p>我们知道,所有<code>OC</code>对象都有<code>isa</code>指针,而<code>Tagged Pointer</code>并不是真正的对象,它没有<code>isa</code>指针,所以如果你直接访问<code>Tagged Pointer</code>的<code>isa</code>成员的话,在编译时将会有如下警告:</p>
<p style="text-align:center"><img alt="" src="https://img.jbzj.com/file_images/article/202301/2023010609040404.jpg" /></p>
<p>对于<code>Tagged Pointer</code>,应该换成相应的方法调用,如<code>isKindOfClass</code>和<code>object_getClass</code>。只要避免在代码中直接访问<code>Tagged Pointer</code>的<code>isa</code>,即可避免这个问题。</p>
<p>当然现在也不允许我们在代码中直接访问对象的<code>isa</code>了,否则编译不通过。</p>
<p>我们通过 LLDB 打印<code>Tagged Pointer</code>的<code>isa</code>,会提示如下错误:</p>
<p style="text-align:center"><img alt="" src="https://img.jbzj.com/file_images/article/202301/2023010609040405.jpg" /></p>
<p>以上就是iOS内存管理Tagged Pointer使用原理详解的详细内容,更多关于iOS内存管理Tagged Pointer的资料请关注琼殿技术社区其它相关文章!</p>
                           
                            <div class="art_xg">
                              <b>您可能感兴趣的文章:</b><ul><li>iOS 断点上传文件的实现方法</li><li>iOS开发中以application/json上传文件实例详解</li><li>IOS开发教程之put上传文件的服务器的配置及实例分享</li><li>iOS&nbsp;简单的操作杆旋转实现示例详解</li><li>iOS&nbsp;底层alloc&nbsp;init&nbsp;new&nbsp;源码流程示例分析</li><li>iOS通过UIDocumentInteractionController实现应用间传文件</li></ul>
                            </div>

                        </div>
                        <!--endmain-->
頁: [1]
查看完整版本: iOS内存管理Tagged Pointer使用原理详解