iOS中内存管理的观察
iOS程序的内存布局
iOS 中的内存区域分为以下几个: 代码段 TEXT:编译之后的代码;
数据段 DATA ① 字符串常量:⽐如NSString *str = @"123"; ② 已初始化数据:已初始化的全局变量、静态变量等; ③ 未初始化数据:未初始化的全局变量、静态变量等;
栈 STACK:函数调⽤开销,⽐如局部变量。分配的内存空间地址越来越⼩;
堆 HEAP:通过alloc、malloc、calloc等动态分配的空间,分配的内存空间地址越来越⼤
Tagged Pointer
我们知道一个对象分配的内存地址是 16 个字节,再加上一个指针 8 个字节,总共 24 个字节,比较浪费内存空间的,所以出现了Tagged Pointer
。
从 64bit 开始,iOS引⼊了Tagged Pointer
⽤于优化NSNumber、NSDate、NSString
等⼩对象的存储,在没有使⽤Tagged Pointer
之前, 这些小对象需要动态分配内存、维护引⽤计数等,指针存储的是堆中对象的地址值,使⽤Tagged Pointer
之后,这些小对象指针⾥⾯存储的数据变成了:Tag + Data
,也就是将数据直接存储在了指针中,当指针不够存储数据的时候,才会使⽤动态分配内存的⽅式来存储数据,objc_msgSend
能识别Tagged Pointer
,⽐如NSNumber
的intValue
⽅法,直接从指针提取数据,节省了以前的调⽤开销
在 iOS 平台,最⾼有效位是 1(第64bit), 在 Mac平台,最低有效位是 1 的对象是Tagged Pointer
对象。
可以观察两个对象的isa,一个是空,一个是 __NSCFNumber
,说明前面的小对象使用的是 Tagged Pointer
技术。
OC对象的内存管理
iOS的内存管理中,使用引用计数来进行内存管理,秉持着:谁创建谁释放,谁引用谁管理的原则 MRC 手动引用计数管理,就是我们在创建使用对象之后,只要是调⽤alloc、new、copy、mutableCopy⽅法返回了⼀个对象,在不需要这个对象时,要调⽤release或者autorelease来释放它。
NSString *str1 = [[NSString alloc] initWithFormat:@"123"];
NSString *str2 = [str1 copy];
NSMutableString *str3 = [str2 mutavleCopy];
[str1 release];
[str2 release];
[str3 release];
MRC 下 setter 方法的写法,需要考虑内存问题,比如下面的案例
- (void)setDog:(NSObject *)dog {
if(_dog != dog) { //不判断会引起僵尸对象
[_dog release]; // 不释放会引起内存泄漏
_dog = [dog retain]; // 引用计数加一,保证外面的释放的时候不至于没有对象
}
}
ARC
自动引用计数管理,就是系统利用 LLVM
编译器自动帮我们生成 release/retain/autorelease
这些代码,利用 Runtime
帮我们实现弱引用等,在 LLVM
和 Runtime
的相互作用下实现自动内存管理。
ARC
中禁止手动调用 retain/release/retainCount/dealloc
,新增了weak/strong
属性关键字
深拷贝与浅拷贝 深拷贝是对象拷贝,浅拷贝是指针拷贝,系统提供的对象拷贝如下
自定义对象如果想实现拷贝,那需要实现 NSCoping 协议
- (id)copyWithZone:(NSZone *)zone {
MrcModel *model = [MrcModel new];
model.name = self.name;
return model;
}
引用计数 学习 isa 结构的时候学习过 isa 共用体的结构
引用计数是存储在 extra_rc
中的,如果不够存,has_sidetable_rc
这个参数会变成 1,那么引用计数会存储在一个叫 SideTable
中的 refcnts
散列表中。
struct SideTable {
spinlock_t slock;
RefcountMap refcnts;
weak_table_t weak_table;
}
弱引用管理
被声明为 __weak
的对象指针,经过编译器的编译之后,会调用 objc_initWeak
函数,然后经过一系列的调用栈,最终在weak_register_no_lock()
这个函数内进行弱引用变量的添加,通过哈希算法来进行位置查找,而这个位置就是上一步中看到的 SideTable
中的 weak_table
散列表中。
当对象要释放的时候,如果存在弱引用,则会先把弱引用表中存储的该对象的引用全部清除掉,清除时是同样的查找步骤,只不过是调用 weak_clear_no_lock()
这个函数。
自动释放池
⾃动释放池是以栈为节点以双向链表形式组合而成的数据结构。主要底层数据结构是:__AtAutoreleasePool、AutoreleasePoolPage
,调⽤了autorelease
的对象最终都是通过AutoreleasePoolPage
对象来管理的。
每个AutoreleasePoolPage
对象占⽤4096字节内存,除了⽤来存放它内部的成员变量,剩下的空间⽤来存放 autorelease
对象的地址。
调用了 AutoreleasePoolPage::Push()
之后会先把一个叫 POOL_BOUNDARY
的值插入 AutoReleasePoolPage
中,后面调用的每一个 autorelease
都会往 AutoReleasePage
中写入一个 autorelease
包裹的对象。
将要释放的时候调用 AutoReleasePoolPage::Pop()
,传入的地址是 Push
的返回值,其实就是 POOL_BOUNDARY
的地址,会从最后⼀个⼊栈的对象开始发送release消息,直到遇到这个 POOL_BOUNDARY
。
AutoReleasePoolPage
中 next
指针指向的是下一个能够给存储对象的位置。
如果有多层嵌套的话会插入多个 POOL_BOUNDARY
Runloop与Autorelease
iOS在主线程的 Runloop
中注册了2个 Observer
第1个监听kCFRunLoopEntry
事件,会调⽤objc_autoreleasePoolPush()
;
第2个监听kCFRunLoopBeforeWaiting
事件,会调⽤objc_autoreleasePoolPop()、objc_autoreleasePoolPush()
,同时也监听了kCFRunLoopBeforeExit
事件,会调⽤objc_autoreleasePoolPop()
。
由此可见,方法中的局部变量并不一定会立即释放,这要看 ARC
环境下编译器如何处理,如果添加的是 release
,那么方法结束立即释放,如果添加的是 autorelease
,那么会在当次 Runloop
结束的时候释放。