iOS 内存管理 部分一

主要讲解内存布局和 Tagged Pointer基础知识;

所有测试基于64位架构;Tagged Pointer测试时使用 iOS12以下的系统; (iOS12以上的系统应该是技术有变更, 打印出的内存地址不直观)

iOS 内存管理 部分一
iOS 内存管理 部分二
iOS 内存管理 部分三
iOS 内存管理 部分四


1. 内存布局

内存布局图


  • 代码段(_TEXT): 编译之后的代码存放位置;
  • 数据段(_DATA):
    字符串常量: 例如NSString *str = @"123";;
    已初始化数据: 已经初始化的全局变量, 静态变量等;
    未初始化的数据: 未初始化的全局变量, 静态变量等;
  • 堆区(heap): 通过alloc, malloc, calloc等动态分配的空间, 分配空间地址越来越大;
  • 栈区(stack): 函数的调用开销; 例如局部变量等, 分配的内存空间地址越来越小;

2. 标记指针Tagged Pointer技术

首先说一下Tagged Pointer的知识点;

  • 1. 从64位开始, iOS引入Tagged Pointer 技术用来优化NSNumber, NSString, NSDate等小对象的存储;
  • 2. 使用Tagged Pointer技术后, 在指针中的存储结构变也就是成:Tag+Data的模式;也就是直接将数据存储在指针中;
  • 3. 如何判断一个指针是否是Tagged Pointer
    3.1 iOS平台: 最高位为1(第64位);
    3.2 Mac平台: 最低位为1;
  • 4. objc_msgSend()能够识别Tagged Pointer; 例如: NSNumberintValue, 可以直接从指针中提取数据, 从而可以节省资源开销;

2.1 为什么要引入 Tagged Pointer 技术呢

首先我们看下方代码, 首先我们假设不存在Tagged Pointer技术; 我们知道每个NSObjet对象最小要占用16个字节的内存(原因讲解); 而存放其地址的指针也要占用8个字节, 则每存放一个对象则要最低开销24个字节的内存, 实际上如果我们只是存放一个数字1, 四个字节就够; 那明显是很浪费的内存的; 因此就有了Tagger Pointer技术;

NSNumber *num1 = [NSNumber numberWithInt:1];

我们以 num1为例:


如果不使用Tagged Pointer技术, 那么就是在堆区开辟一块16个的空间里面存放着数字1,在栈区创建开辟一块空间指针num1(8个字节), 里面存放着堆区开辟空间的地址;
而使用了Tagger Pointer技术后, 只需要栈区开辟的num1中直接存放一个地址, 地址中包含着数字1;
所以, 看下方代码:

    NSNumber *num1 = [NSNumber numberWithInt:1];
    NSNumber *num2 = @(2);
    NSNumber *num3 = @3;
    NSNumber *num4 = @4;
    NSNumber *num5 = @55;
    NSLog(@"%p %p %p %p %p", num1, num2, num3, num4, num5);
2020-08-05 17:55:16.111763+0800 MemoryTaggedPointer[2938:542596] 
0xb000000000000012 0xb000000000000022 0xb000000000000032 0xb000000000000042 0xb000000000000372

测试基于64位架构; 所以打印出的地址为包含16位的16进制地址(一个16进制位等于四个2进制位, 一个字节等于8位), 且转化为二进制收首位一定是1(验证过程看补充部分); 最后一位是标志位代表是NSNumner, 往前推移一(两)位表示包含的数字;

16进制下37对应10进制中55; 进制在线转换

2.2 Tagged Pointer 只适用于小数据

由于一个指针的空间只有8个字节, 所以我们可以推断, 当存放的数字超过一个值时, 就不会在使用Tagger Pointer技术存储, 因为8个字节存不下了, 这个时候就会采取在堆区开辟空间, 栈区指针指向的方式;
验证如下, 我们从打印结果可以看到, num1仍然是采用Tagger Pointer技术存放, 而num2则是堆区开辟空间存放;

NSNumber *num1 = @(2);
NSNumber *num2 = @(0xffffffffffffffff);
NSLog(@"%p %p ", num1,num2);
#打印结果
2020-08-05 18:10:28.469909+0800 MemoryTaggedPointer[3117:547322] 
0xb000000000000022 0x17002d6e0

同理, 字符串也是存在这种情况, 如果不是存放在数据段(看第一部分内存布局)的字符串, 如果一个字符串很小则采用Tagger Pointer技术存放, 如果字符串很大则也是在堆区开辟空间方式;

///采用Tagger Pointer存放
NSString *str1 = [NSString stringWithFormat:@"a"];
///采用Tagger Pointer存放
NSString *str2 = [@(1) stringValue];
///存放在数据段
NSString *str3 = @"a";
///存放在数据段
NSString *str4 = @"aaaaaaaaaaaaaaaaabc";
///存放在堆区
NSString *str5 = [NSString stringWithFormat:@"aaaaaaaaaaaaaaaaabc"];

打印结果为

NSLog(@"%@ %@ %@ %@ %@", [str1 class], [str2 class], [str3 class], [str4 class], [str5 class]);
2020-08-05 18:22:11.309778+0800 MemoryTaggedPointer[3254:551035] 
NSTaggedPointerString NSTaggedPointerString __NSCFConstantString __NSCFConstantString __NSCFString

补充

如何验证一个指针是否是 Tagged Pointer

我们从runtime的源码中可以

// Return true if ptr is a tagged pointer object.
// Does not check the validity of ptr's class.
static inline bool 
_objc_isTaggedPointer(const void * _Nullable ptr);

===>
static inline bool 
_objc_isTaggedPointer(const void * _Nullable ptr)
{
    return ((uintptr_t)ptr & _OBJC_TAG_MASK) == _OBJC_TAG_MASK;
}

===>
#if (TARGET_OS_OSX || TARGET_OS_IOSMAC) && __x86_64__
    // 64-bit Mac - tag bit is LSB
 ///Mac 平台
#   define OBJC_MSB_TAGGED_POINTERS 0
#else
    // Everything else - tag bit is MSB
///iOS 和 iPadOS 平台
#   define OBJC_MSB_TAGGED_POINTERS 1
#endif

===>
#if OBJC_MSB_TAGGED_POINTERS
///1向左移63位; 就是转化为二进制时从左开始第64位为1;
#   define _OBJC_TAG_MASK (1UL<<63)
...
#else
#   define _OBJC_TAG_MASK 1UL
#   define _OBJC_TAG_INDEX_SHIFT 1
...
#endif


文中测试代码
参考文章
实例对象占用的内存

你可能感兴趣的:(iOS 内存管理 部分一)