【iOS】OC底层系列十六 - 内存管理-内存布局

面试题1:介绍下内存的几大区域。

面试题2:对NSString、NSNumber的理解。

一、内存布局

内存布局有4个核心区域:

  • 代码段:编译之后的代码。
  • 数据段
    • 字符串常量:比如NSString *str = @"123"
    • 已初始化数据:已初始化的全局变量、静态变量等
    • 未初始化数据:未初始化的全局变量、静态变量等
    • ……
  • :通过allocmalloccalloc等动态分配的空间。分配的内存空间地址越来越大
  • :函数调用开销,比如局部变量。分配的内存空间地址越来越小

二、Tagged Pointer

从64bit开始,iOS引入了TaggedPointer技术,用于优化NSNumberNSDateNSString等小对象的存储。

在没有使用TaggedPointer之前,NSNumber等对象需要动态分配内存、维护引用计数等,NSNumber指针存储的是堆中NSNumber对象的地址值(和普通对象没有区别)。

使用Tagged Pointer之后,NSNumber指针里面存储的数据变成了:Tag + Data,也就是将数据直接存储在了指针中。当指针不够存储数据时,才会使用动态分配内存的方式来存储数据。

objc_msgSend内部能识别Tagged Pointer,比如NSNumberintValue方法,直接从指针提取数据,节省了以前的调用开销(不需要找isa链条)。

如何判断一个指针是否为TaggedPointer?

  • iOS平台,指针的最高有效位是1(第64bit)。
  • Mac平台,指针的最低有效位是1。堆空间分配的地址最低有效位是0(因为内存对齐数是16,16的二进制最后一位一定是0)。

在objc源码中对象release操作有判断当前对象是否是TaggedPointer:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
#if TARGET_OS_OSX && __x86_64__
// 64-bit Mac - tag bit is LSB
# 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)
#else
# define _OBJC_TAG_MSK 1UL
#endif

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

面试题:思考以下2段代码能发生什么事?有什么区别?

代码一:

1
2
3
4
5
6
7
8
@property (nonatomic, copy) NSString *name;

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"];
});
}

执行代码一会程序崩溃(报错:坏内存访问),因为ARC最终转换成了MRC,而self.name = *是调用了name的setter方法:

1
2
3
4
5
6
- (void)setName:(NSString *)name {
if (_name != name) {
[_name release];
_name = [name copy];
}
}

由于是异步访问name的setter方法,所以可能成员变量_name被释放多次,导致程序崩溃。

解决方法:把属性name的修饰符改为atomic(setter和getter方法内部线程安全)。

1
@property (atomic, copy) NSString *name;

也可以在访问name的线程外部加锁保证线程安全(推荐。这样不影响其他代码正常访问)。

代码二:

1
2
3
4
5
6
dispatch_queue_t queue = dispatch_get_global_queue(0, 0);
for (int i = 0; i < 1000; i++) {
dispatch_async(queue, ^{
self.name = [NSString stringWithFormat:@"abc"];
});
}

执行上面的代码后,喜剧性的一幕出现了,程序没有崩溃,不管运行多少次都不会崩溃,仅仅是修改了字符串的长度,为什么会出现这种情况呢?

上面两段代码的变量name地址值不同,[NSString stringWithFormat:@"abcdefghijk"];是OC对象(__NSCFString类型),[NSString stringWithFormat:@"abc"];是TaggedPointer(NSTaggedPointerString类型)。而非对象类型是不会有release操作的。