iOS底层-内存管理(一)
iOS程序的内存布局
-
如图所示
- 保留区
- 从地址0开始,比较小的哪一段是我们不能使用的,保留的
- 代码段:
- 编译之后的代码
- 高级语言->汇编语言->机器语言
- 我们写的每一句代码都存储在这里
- 数据段:
- 字符串常量:比如
NSString *str = @"123";
- 已初始化数据:已初始化的全局变量、静态变量等
- 未初始化数据:未初始化的全局变量、静态变量等
- 特点: 整个应用程序中都存在的
- 字符串常量:比如
- 栈:
- 函数调用开销,比如局部变量。分配的内存空间地址从大到小
- 特点: 内存临时开辟,用完就自动回收
- 堆:
- 通过alloc、malloc、calloc等动态分配的空间,分配的内存空间地址从小到大
- 特点:根据需要程序员手动分配内存,并且手动管理内存的释放
- 保留区
-
代码验证:
int a = 10; //初始化的全局变量 int b ; //未初始化的全局变量 int main(int argc, const char * argv[]) { @autoreleasepool { //初始化的静态局部变量 static int c = 20; //未初始化的静态局部变量 static int d; int e = 30; int f; //字符串常量 NSString *str = @"123"; //堆 NSObject *obj = [[NSObject alloc] init]; NSLog(@"=a=========%p",&a); NSLog(@"=b=========%p",&b); NSLog(@"=c=========%p",&c); NSLog(@"=d=========%p",&d); NSLog(@"=e=========%p",&e); NSLog(@"=f=========%p",&f); NSLog(@"=str=======%p",str); NSLog(@"=obj=======%p",obj); } return 0; }
-
打印:
=a=========0x100001180 =b=========0x10000118c =c=========0x100001184 =d=========0x100001188 =e=========0x7ffeefbff5ac =f=========0x7ffeefbff5a8 =str=======0x100001040 =obj=======0x1006826d0
-
整理:
//常量 str = 0x100001040 //已初始化:全局变量、静态变量 &a = 0x100001180 &c = 0x100001184 //未初始化:全局变量、静态变量 &d = 0x100001188 &b = 0x10000118c //堆 obj = 0x1006826d0 //栈:栈中的地址从大到小 &f = 0x7ffeefbff5a8 &e = 0x7ffeefbff5ac
-
Tagged Pointer (小型对象的内存管理)
引入:
- 为什么要使用TaggedPointer?
-
以前我们初始化一个对象(64位为例),开发的代码如下
NSNumber *number2 = [NSNumber numberWithInteger:2];
- 这句代码的内存分析:
- 一个栈上分配8个字节内存给number2变量,用于存储NSNumber对象在堆上的地址
- 一个堆上分配16个字节(isa、2),用于存储NSNumber对象
- 总共消耗24个字节
- 由于NSNumber和NSDate对象的值一般不需要8个字节,4个字节的长度即可,为了不造成内存的浪费,想到将指针的值(8个字节)进行拆分,一部分表示数据,一部分用来表示数据的类型,他不是任何对象,这就是TaggedPointer技术,这样指针 = Data + Tag,那么我们的存一个数字只需要8个字节就够了。
-
总结
- 从64bit开始,iOS引入了
Tagged Pointer
技术,用于优化NSNumber、NSDate、NSString等小对象的存储 - 在没有使用
Tagged Pointer
之前, NSNumber等对象需要动态分配内存、维护引用计数等,NSNumber
指针存储的是堆中NSNumber
对象的地址值 - 使用
Tagged Pointer
之后,NSNumber
指针里面存储的数据变成了:Tag + Data
,也就是将数据直接存储在了指针中(Tag指的是数据类型)- 即用指针类型(8个字节)存储
Tag+Data
- 即用指针类型(8个字节)存储
- 当指针不够存储数据时,才会使用动态分配内存的方式来存储数据
objc_msgSend
能识别Tagged Pointer
,比如NSNumber的intValue
方法,直接从指针提取数据,节省了以前的调用开销- 如何判断一个指针是否为
Tagged Pointer
?- iOS平台,最高有效位是1(第64bit)
- Mac平台,最低有效位是1
- objc源码证明:
struct objc_object
这个结构体对象有个判断当前对象是否是taggedPointer
的成员函数isTaggedPointer
//判断当前对象是否是TaggedPointer bool isTaggedPointer(); inline bool objc_object::isTaggedPointer() { return _objc_isTaggedPointer(this); } 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 //mac开发64位 # define OBJC_MSB_TAGGED_POINTERS 0 #else // Everything else - tag bit is MSB 其他开发 # define OBJC_MSB_TAGGED_POINTERS 1 #endif #if OBJC_MSB_TAGGED_POINTERS # define _OBJC_TAG_MASK (1UL<<63) //第一位为1 #else # define _OBJC_TAG_MASK 1UL //最后一位为1 #endif
例证
-
上面的结论我们来证实一下:
NSNumber *number1 = @1; NSNumber *number2 = @2; NSNumber *number3 = @3; NSNumber *numberFFFF = @(0xFFFF); NSLog(@"number1 pointer is %p", number1); NSLog(@"number2 pointer is %p", number2); NSLog(@"number3 pointer is %p", number3); NSLog(@"numberffff pointer is %p", numberFFFF);
-
打印:
number1 pointer is 0xd56dc97421ba919c number2 pointer is 0xd56dc97421ba91ac number3 pointer is 0xd56dc97421ba91bc numberffff pointer is 0xd56dc97421b56e7c
- 很惊讶的发现这个地址好像并不是
Tag + Data
,而且还跟网上很多博客上说的不一样,这是什么问题呢? - 继续研究一下objc源码
-
- objc源码分析
-
在源码中找到
_objc_makeTaggedPointer
,从注释来看,这个方法就是对TaggedPointer的编码static inline void * _Nonnull _objc_makeTaggedPointer(objc_tag_index_t tag, uintptr_t value) { // PAYLOAD_LSHIFT and PAYLOAD_RSHIFT are the payload extraction shifts. // They are reversed here for payload insertion. // assert(_objc_taggedPointersEnabled()); if (tag <= OBJC_TAG_Last60BitPayload) { // assert(((value << _OBJC_TAG_PAYLOAD_RSHIFT) >> _OBJC_TAG_PAYLOAD_LSHIFT) == value); uintptr_t result = (_OBJC_TAG_MASK | ((uintptr_t)tag << _OBJC_TAG_INDEX_SHIFT) | ((value << _OBJC_TAG_PAYLOAD_RSHIFT) >> _OBJC_TAG_PAYLOAD_LSHIFT)); //编码 return _objc_encodeTaggedPointer(result); } else { // assert(tag >= OBJC_TAG_First52BitPayload); // assert(tag <= OBJC_TAG_Last52BitPayload); // assert(((value << _OBJC_TAG_EXT_PAYLOAD_RSHIFT) >> _OBJC_TAG_EXT_PAYLOAD_LSHIFT) == value); uintptr_t result = (_OBJC_TAG_EXT_MASK | ((uintptr_t)(tag - OBJC_TAG_First52BitPayload) << _OBJC_TAG_EXT_INDEX_SHIFT) | ((value << _OBJC_TAG_EXT_PAYLOAD_RSHIFT) >> _OBJC_TAG_EXT_PAYLOAD_LSHIFT)); //编码 return _objc_encodeTaggedPointer(result); } } //编码 static inline void * _Nonnull _objc_encodeTaggedPointer(uintptr_t ptr) { return (void *)(objc_debug_taggedpointer_obfuscator ^ ptr); } //解码 static inline uintptr_t _objc_decodeTaggedPointer(const void * _Nullable ptr) { return (uintptr_t)ptr ^ objc_debug_taggedpointer_obfuscator; } //objc_debug_taggedpointer_obfuscator 的初始化 static void initializeTaggedPointerObfuscator(void) { //mac_os< 10.14 和 iOS< 12.0 if (sdkIsOlderThan(10_14, 12_0, 12_0, 5_0, 3_0) || // Set the obfuscator to zero for apps linked against older SDKs, // in case they're relying on the tagged pointer representation. DisableTaggedPointerObfuscation) { objc_debug_taggedpointer_obfuscator = 0; } else { // Pull random data into the variable, then shift away all non-payload bits. arc4random_buf(&objc_debug_taggedpointer_obfuscator, sizeof(objc_debug_taggedpointer_obfuscator)); objc_debug_taggedpointer_obfuscator &= ~_OBJC_TAG_MASK; } }
-
分析:
- 可以看到一个对象通过
_objc_makeTaggedPointer
函数创建一个TaggedPointer _objc_makeTaggedPointer
内部本质调用了_objc_encodeTaggedPointer
进行编码_objc_encodeTaggedPointer
的本质是对ptr进行了异或操作与objc_debug_taggedpointer_obfuscator
objc_debug_taggedpointer_obfuscator
这个值很有特点:- 在MAC_OS<14.0,iOS<12.0时,
objc_debug_taggedpointer_obfuscator = 0;
,与0异或,原值不变 - 反之:
objc_debug_taggedpointer_obfuscator &= ~_OBJC_TAG_MASK;
,非零值,再异或肯定不是原值了 _OBJC_TAG_MASK
又根据当前是mac或者非mac取不同的值
- 在MAC_OS<14.0,iOS<12.0时,
- 因此,分析出打印结果不正常的原因:因为当前我用的是MAC_OS>14.0,iOS>12.0,当前打印的是与
objc_debug_taggedpointer_obfuscator
异或后的值
- 可以看到一个对象通过
-
- 那么就不能查看真正的
tag+Data
了吗?- 可以的—可以手动实现
_objc_decodeTaggedPointer
- 因为上面打印的是_objc_encodeTaggedPointer编码后的,因此我们可以手动解码
_objc_decodeTaggedPointer
然后打印
- 可以的—可以手动实现
-
示例
#import "ViewController.h" #import <objc/runtime.h> @implementation ViewController -(void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event{ NSNumber *number1 = @1; NSNumber *number2 = @2; NSNumber *number3 = @3; NSNumber *numberFFFF = @(0xFFFF); NSLog(@"number1 pointer is %p---真实地址:==0x%lx", number1,_objc_decodeTaggedPointer_(number1)); NSLog(@"number2 pointer is %p---真实地址:==0x%lx", number2,_objc_decodeTaggedPointer_(number2)); NSLog(@"number3 pointer is %p---真实地址:==0x%lx", number3,_objc_decodeTaggedPointer_(number3)); NSLog(@"numberffff pointer is %p---真实地址:==0x%lx", numberFFFF,_objc_decodeTaggedPointer_(numberFFFF)); } //看源码知道objc_debug_taggedpointer_obfuscator是个外部全局变量,只要在我们用的地方申明一下即可 extern uintptr_t objc_debug_taggedpointer_obfuscator; //手动实现解码 uintptr_t _objc_decodeTaggedPointer_(id ptr) { NSString *p = [NSString stringWithFormat:@"%ld",ptr]; return [p longLongValue] ^ objc_debug_taggedpointer_obfuscator; } @end
-
打印:
number1 pointer is 0xb12a3e5504bc342f---真实地址:==0xb000000000000012 number2 pointer is 0xb12a3e5504bc341f---真实地址:==0xb000000000000022 number3 pointer is 0xb12a3e5504bc340f---真实地址:==0xb000000000000032 numberffff pointer is 0xb12a3e5504b3cbcf---真实地址:==0xb0000000000ffff2
-
可以看到数据是直接存储在指针中的
-
Tagged Pointer的应用
-
下面代码会发生什么问题?
dispatch_queue_t queue = dispatch_get_global_queue(0, 0); for (int i = 0; i<1000; i++) { dispatch_async(queue, ^{ //会崩溃 //self.name = [NSString stringWithFormat:@"abcdefghijk"]; //不会崩溃 self.name = [NSString stringWithFormat:@"abc"]; }); }
-
self.name的本质
-(void)setName:(NSString *)name{ if (name != _name) { //n个线程同时访问,就会一个对象多次释放,崩溃 [_name release]; _name = [name retain]; } }
self.name = [NSString stringWithFormat:@"abcdefghijk"];
会崩溃- 是真正的oc对象,由于是多线程会出现
[_target release];
被调用多次,从而闪退;
- 是真正的oc对象,由于是多线程会出现
self.name = [NSString stringWithFormat:@"abc"];
不会崩溃- 不是oc对象,而是TaggedPointer,在release和retain的时候都会判断是不是TaggedPointer
-
objc源码
objc_object::rootRelease(bool performDealloc, bool handleUnderflow) { if (isTaggedPointer()) return false; bool sideTableLocked = false; ... } ALWAYS_INLINE id objc_object::rootRetain(bool tryRetain, bool handleOverflow) { if (isTaggedPointer()) return (id)this; }
-