正文
为了节省内存和提高执行效率,苹果在64bit
程序中引入了tagged pointer
技术,用于优化nsnumber
、nsdate
、nsstring
等小对象的存储。在引入 tagged pointer 技术之前,nsnumber
等对象存储在堆上,nsnumber
的指针中存储的是堆中nsnumber
对象的地址值。
从内存占用来看基本数据类型所需的内存不大。比如nsinteger
变量,它所占用的内存是与 cpu 的位数有关,如下。在 32 bit 下占用 4 个字节,而在 64 bit 下占用 8 个字节。指针类型的大小通常也是与 cpu 位数相关,一个指针所在 32 bit 下占用 4 个字节,在 64 bit 下占用 8 个字节。
#if __lp64__ || 0 || ns_build_32_like_64 typedef long nsinteger; typedef unsigned long nsuinteger; #else typedef int nsinteger; typedef unsigned int nsuinteger; #endif
假设我们通过nsnumber
对象存储一个nsinteger
的值,系统实际上会给我们分配多少内存呢?
由于tagged pointer
无法禁用,所以以下将变量i
设了一个很大的数,以让nsnumber
对象存储在堆上。
可以通过设置环境变量objc_disable_tagged_pointers
为yes
来禁用tagged pointer
,但如果你这么做,运行就crash
。
tagged pointers are disabled
因为runtime
在程序运行时会判断tagged pointer
是否被禁用,如果是的话就会调用_objc_fatal()
函数杀死进程。所以,虽然苹果提供了objc_disable_tagged_pointers
这个环境变量给我们,但是tagged pointer
还是无法禁用。
在 64 bit 下,如果没有使用tagged pointer
的话,为了使用一个nsnumber
对象就需要 8 个字节指针内存和 32 个字节对象内存。而直接使用一个nsinteger
变量只要 8 个字节内存,相差好几倍。
nsnumber
等对象的指针中存储的数据变成了tag
+data
形式(tag
为特殊标记,用于区分nsnumber
、nsdate
、nsstring
等对象类型;data
为对象的值)。这样使用一个nsnumber
对象只需要 8 个字节指针内存。当指针的 8 个字节不够存储数据时,才会在将对象存储在堆上。
tagged pointer 的原理
在现在的版本中,为了保证数据安全,苹果对 tagged pointer 做了数据混淆,开发者通过打印指针无法判断它是不是一个tagged pointer
,更无法读取tagged pointer
的存储数据。
所以在分析tagged pointer
之前,我们需要先关闭tagged pointer
的数据混淆,以方便我们调试程序。通过设置环境变量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; } // 关闭 tagged pointer 数据混淆后:0x127 0x227 0x327 0x600003a090e0 // 关闭 tagged pointer 数据混淆前:0xaca2838a63a4fb34 0xaca2838a63a4fb04 0xaca2838a63a4fb14 0x600003a090e0
从以上打印结果可以看出,number1~number3
指针为tagged pointer
类型,可以看到对象的值都存储在了指针中,对应0x1
、0x2
、0x3
。而number4
由于数据过大,指针的8
个字节不够存储,所以在堆中分配了内存。
注意: macos
与ios
平台下的tagged pointer
有差别,下面会讲到。
0x127 中的 2 和 7 表示什么?我们先来看这个7
,0x127
为十六进制表示,7
的二进制为0111
。
最后一位1
是tagged pointer
标识位,代表这个指针是tagged pointer
。
前面的011
是类标识位,对应十进制为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(即倒数第二位)又代表什么呢?
倒数第二位用来表示数据类型。
示例:
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
倒数第二位对应数据类型:
tagged pointer 倒数第二位 | 对应数据类型 |
---|---|
0 | char |
1 | short |
2 | int |
3 | long |
4 | float |
5 | double |
下图是macos
下nsnumber
的tagged pointer
位视图:
接下来我们来分析一下tagged pointer
在nsstring
中的应用。同nsnumber
一样,在64 bit
的macos
下,如果一个nsstring
对象指针为tagged pointer
,那么它的后 4 位(0-3)作为标识位,第 4-7 位表示字符串长度,剩余的 56 位就可以用来存储字符串。
示例:
// mrc 环境 #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 */
从打印结果来看,有三种nsstring
类型:
类型 | 描述 |
---|---|
__nscfconstantstring | 1. 常量字符串,存储在字符串常量区,继承于 __nscfstring。相同内容的 __nscfconstantstring 对象的地址相同,也就是说常量字符串对象是一种单例,可以通过 == 判断字符串内容是否相同。 2. 这种对象一般通过字面值@"..." 创建。如果使用 __nscfconstantstring 来初始化一个字符串,那么这个字符串也是相同的 __nscfconstantstring。 |
__nscfstring | 1. 存储在堆区,需要维护其引用计数,继承于 nsmutablestring。 2. 通过stringwithformat: 等方法创建的nsstring 对象(且字符串值过大无法使用tagged pointer 存储)一般都是这种类型。 |
nstaggedpointerstring | tagged pointer ,字符串的值直接存储在了指针上。 |
打印结果分析:
nsstring 对象 | 类型 | 分析 |
---|---|---|
a | __nscfconstantstring | 通过字面量@"..." 创建 |
b | __nscfstring | a 的深拷贝,指向不同的内存地址,被拷贝到堆区 |
c | __nscfconstantstring | a 的浅拷贝,指向同一块内存地址 |
d | nstaggedpointerstring | 单独对 a 进行 copy(如 c),浅拷贝是指向同一块内存地址,所以不会产生tagged pointer ;单独对 a 进行 mutablecopy(如 b),复制出来是可变对象,内容大小可以扩展;而tagged pointer 存储的内容大小有限,因此无法满足可变对象的存储要求。 |
e | __nscfconstantstring | 使用 __nscfconstantstring 来初始化的字符串 |
f | nstaggedpointerstring | 通过stringwithformat: 方法创建,指针足够存储字符串的值。 |
string1 | nstaggedpointerstring | 通过stringwithformat: 方法创建,指针足够存储字符串的值。 |
string2 | nstaggedpointerstring | 通过stringwithformat: 方法创建,指针足够存储字符串的值。 |
string3 | __nscfstring | 通过stringwithformat: 方法创建,指针不足够存储字符串的值。 |
可以看到,为tagged pointer
的有d
、f
、string1
、string2
指针。它们的指针值分别为0x6115
、0x6615
、0x6766656463626175
、0x880e28045a54195
。
其中0x61
、0x66
、0x67666564636261
分别对应字符串的 ascii 码。
最后一位5
的二进制为0101
,最后一位1
是代表这个指针是tagged pointer
,010
对应十进制为2
,表示nsstring
类。
倒数第二位1
、1
、7
、9
代表字符串长度。
对于string2
的指针值0x880e28045a54195
,虽然从指针中看不出来字符串的值,但其也是一个tagged pointer
。
下图是macos
下nsstring
的tagged pointer
位视图:
如何判断 tagged pointer
在objc4
源码中找到判断tagged pointer
的函数:
// 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
标识位。
而存储在堆空间的对象由于内存对齐,它的内存地址的最低有效位为 0。由此可以辨别tagged pointer
和一般对象指针。
在objc4
源码中,我们经常会在函数中看到tagged pointer
。比如objc_msgsend
函数:
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
,比如nsnumber
的intvalue
方法,直接从指针提取数据,不会进行objc_msgsend
的三大流程,节省了调用开销。
内存管理相关的,如retain
方法中调用的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 pointer
的isa
成员的话,在编译时将会有如下警告:
对于tagged pointer
,应该换成相应的方法调用,如iskindofclass
和object_getclass
。只要避免在代码中直接访问tagged pointer
的isa
,即可避免这个问题。
当然现在也不允许我们在代码中直接访问对象的isa
了,否则编译不通过。
我们通过 lldb 打印tagged pointer
的isa
,会提示如下错误:
以上就是ios内存管理tagged pointer使用原理详解的详细内容,更多关于ios内存管理tagged pointer的资料请关注代码网其它相关文章!
发表评论