面试题1:介绍下内存的几大区域。
面试题2:对NSString、NSNumber的理解。
一、内存布局
内存布局有4个核心区域:
- 代码段:编译之后的代码。
- 数据段:
- 字符串常量:比如
NSString *str = @"123"
- 已初始化数据:已初始化的全局变量、静态变量等
- 未初始化数据:未初始化的全局变量、静态变量等
- ……
- 字符串常量:比如
- 堆:通过
alloc
、malloc
、calloc
等动态分配的空间。分配的内存空间地址越来越大 - 栈:函数调用开销,比如局部变量。分配的内存空间地址越来越小
二、Tagged Pointer
从64bit开始,iOS引入了TaggedPointer技术,用于优化NSNumber
、NSDate
、NSString
等小对象的存储。
在没有使用TaggedPointer之前,NSNumber
等对象需要动态分配内存、维护引用计数等,NSNumber
指针存储的是堆中NSNumber
对象的地址值(和普通对象没有区别)。
使用Tagged Pointer之后,NSNumber
指针里面存储的数据变成了:Tag + Data
,也就是将数据直接存储在了指针中。当指针不够存储数据时,才会使用动态分配内存的方式来存储数据。
objc_msgSend
内部能识别Tagged Pointer,比如NSNumber
的intValue
方法,直接从指针提取数据,节省了以前的调用开销(不需要找isa链条)。
如何判断一个指针是否为TaggedPointer?
- iOS平台,指针的最高有效位是1(第64bit)。
- Mac平台,指针的最低有效位是1。堆空间分配的地址最低有效位是0(因为内存对齐数是16,16的二进制最后一位一定是0)。
在objc源码中对象release操作有判断当前对象是否是TaggedPointer:
1 |
|
面试题:思考以下2段代码能发生什么事?有什么区别?
代码一:
1 | @property (nonatomic, copy) NSString *name; |
执行代码一会程序崩溃(报错:坏内存访问),因为ARC最终转换成了MRC,而self.name = *
是调用了name的setter方法:
1 | - (void)setName:(NSString *)name { |
由于是异步访问name的setter方法,所以可能成员变量_name
被释放多次,导致程序崩溃。
解决方法:把属性name的修饰符改为atomic
(setter和getter方法内部线程安全)。
1 | @property (atomic, copy) NSString *name; |
也可以在访问name的线程外部加锁保证线程安全(推荐。这样不影响其他代码正常访问)。
代码二:
1 | dispatch_queue_t queue = dispatch_get_global_queue(0, 0); |
执行上面的代码后,喜剧性的一幕出现了,程序没有崩溃,不管运行多少次都不会崩溃,仅仅是修改了字符串的长度,为什么会出现这种情况呢?
上面两段代码的变量name
地址值不同,[NSString stringWithFormat:@"abcdefghijk"];
是OC对象(__NSCFString
类型),[NSString stringWithFormat:@"abc"];
是TaggedPointer(NSTaggedPointerString
类型)。而非对象类型是不会有release操作的。