一道面试题
1 | NSInteger i = 0xFFFFFFFFFFFFFF; |
由于NSNumber
继承自NSObject
,所有它有isa
指针,加上内存对齐的处理,系统给NSNumber
对象分配了 32 个字节内存。通过 LLDB
指令读取它的内存,实际上它并没有用完 32 个字节。
Tagged Pointer
iPhone 5s开始采用64bit cpu架构,也就是指针占用4个字节,32bit的寻址范围是4G,显然64bit的指针存在空间浪费,对于小对象,比如NSNumer
,NSDate
,NSString
,TaggeddPointer
不再是地址,而是真正的值。这种方式可以节省内存,提高执行效率
引入 Tagged Pointer
技术之后NSNumber
等对象的指针中存储的数据变成了Tag+Data形式(Tag为特殊标记,用于区分NSNumber
、NSDate
、NSString
等对象类型;Data
为对象的值)。这样使用一个NSNumber
对象只需要 8 个字节指针内存。当指针的 8 个字节不够存储数据时,才会在将对象存储在堆上。
实现分析
1 | - (void)viewDidLoad { |
number1
~number3
指针为Tagged Pointer
类型,可以看到对象的值都存储在了指针中,对应倒数第二位开始的1、2、4f。而number4
由于数据过大,指针的8个字节不够存储,所以在堆中分配了内存。
最后一位用来表示数据类型。
第一位b
的二进制为1011
,其中第一位1是Tagged Pointer
标识位。后面的011
是类标识位,对应十进制为3
,表示NSNumber
类。
下图是iOS下NSNumber
的Tagged Pointer
位视图
相关题目
1 | @property (nonatomic, strong) NSString * name; |
1 | @property (nonatomic, strong) NSString * name; |
第一段代码Crash,而第二段却没有问题。
分别打印两段代码的self.name
类型看看,原来第一段代码中self.name为__NSCFString
类型,而第二段代码中为NSTaggedPointerString
类型。
__NSCFString
存储在堆上,它是个正常对象,需要维护引用计数的。self.name
通过setter
方法为其赋值。而setter方法的实现如下:
1 |
|
异步并发执行setter
方法,可能就会有多条线程同时执行[_name release]
,连续release
两次就会造成对象的过度释放,导致Crash
。
解决办法:
- 使用atomic属性关键字。
- 加锁
1 | @property (atomic, strong) NSString * name; |
第二段代码中的NSString为NSTaggedPointerString
类型,在objc_release
函数中会判断指针是不是TaggedPointer
类型,是的话就不对对象进行release
操作,也就避免了因过度释放对象而导致的Crash
,因为根本就没执行释放操作。
1 | __attribute__((aligned(16), flatten, noinline)) |
isa指针(NONPOINTER_ISA)
- 非指针型isa:值的部分代表
class
地址 - 指针型isa:值代表
class
地址
非指针型isa
同理64位存储一个内存地址是种浪费,isa
是一个公用体结构union
,NONPOINTER_ISA
是给对象分配了内存空间,但是不使用sideTable
管理引用计数,而是把引用计数存在了isa
当中。
1 | union isa_t |
指针型isa
SideTable
包含了引用计数表,弱引用计数表,以及一个自旋锁。结构如下
1 | struct SideTable { |
- 引用计数表
是用hash
表实现的,引用计数会存在多张sideTable
中,修改引用计数,需要经过两次hash
算法,第一次是从sideTables
中找到具体的sideTable
,第二次是从sideTable
中找到对应的引用计数。之所以设计成多张sideTable
而不是一张sideTables
,是因为每次操作都需要加锁,减锁操作,多张可以分离锁,加快操作速度。
1 | SideTable &table = SideTables()[this]; |
- 弱引用表
苹果使用sideTables
保存所有的weak
引用,key就是对象地址,weak_entry_t
作为值,weak_entry_t
中保存了所有指向该对象的弱引用。
1 | struct weak_table_t { |
总结
目前OC管理内存的方式有两种Taged Pointer 和 isa指针