学习了好久的iOS内存管理,一直是断断续续的,现在有时间找了个机会总结了一下,有时候时间久了好多知识点就会遗忘,希望能将这些点记下来,多看几次。
前言:虚拟内存
移动设备的内存资源是有限的,当App运行时占用的内存大小超过了限制后,就会被强杀掉,从而导致用户体验被降低。所以,为了提升App质量,开发者要非常重视应用的内存管理问题。
移动端的内存管理技术,主要有两种:
GC(Garbage Collection,垃圾回收)的标记清除算法;
Apple使用的引用计数方法。
相比较于 GC 标记清除算法,引用计数法可以及时地回收引用计数为0的对象,减少查找次数。但是,引用 计数会带来循环引用的问题,比如当外部的变量强引用 Block时,Block 也会强引用外部的变量,就会出现 循环引用。我们需要通过弱引用,来解除循环引用的问题。
另外,在 ARC(自动引用计数)之前,一直都是通过 MRC(手动引用计数)这种手写大量内存管理代码的 方式来管理内存,因此苹果公司开发了 ARC 技术,由编译器来完成这部分代码管理工作。但是,ARC依然 需要注意循环引用的问题。
内存管理的演进过程:在最开始的时候,程序是直接访问物理内存,但后来有了多程序多任务同时运 行,就出现了很多问题。比如,同时运行的程序占用的总内存必须要小于实际物理内存大小。再比如,程序 能够直接访问和修改物理内存,也就能够直接访问和修改其他程序所使用的物理内存,程序运行时的安全就 无法保障。
虚拟内存:
由于要解决多程序多任务同时运行的这些问题,所以增加了一个 中间层 来间接访问物理内存,这个中间层就是虚拟内存。虚拟内存通过映射,可以将虚拟地址转化成物理地址。
虚拟内存会给每个程序创建一个单独的执行环境,也就是一个独立的虚拟空间,这样每个程序就只能访问自 己的地址空间(Address Space),程序与程序间也就能被安全地隔离开了。
32位的地址空间是 2^32 = 4294967296 个字节,共 4GB,如果内存没有达到 4GB 时,虚拟内存比实际的物 理内存要大,这会让程序感觉自己能够支配更多的内存。如同虚拟内存只供当前程序使用,操作起来和物理 内存一样高效。
有了虚拟内存这样一个中间层,极大地节省了物理内存。iOS的共享库就是利用了这一点,只占用一份物理 内存,却能够在不同应用的多份虚拟内存中,去使用同一份共享库的物理内存。
1、 引用计数 RetainCount
在iOS开发中,使用 引用计数 来管理OC对象的内存:
- 一个新创建的OC对象,它的引用计数是1,当引用计数减为0 ,OC对象就会被销毁,释放其占用的内存空间;
- 调用retain会让对象的引用计数+1;调用release会让对象的引用计数-1;
- 内存管理总结经验:
- 当调用alloc,new,copy,mutablecopy等返回一个对象,在不需要这个对象的时候,需要对这个对象进行release或者autorelease操作;
- 想拥有某个对象,就让它的引用计数+1;不想拥有某个对象,就让它的引用计数-1;
1.1 引用计数存放的位置
从64bit开始,对象的引用计数存放在优化过的isa指针中,也可能存放在sideTable中:
- extra_rc:存放的是对象的引用计数值减1;
- has_sidetable_rc: 如果引用计数值过大,extra_rc中存放不下,这时候此值为1,对象的引用计数存放在sidetable中;
1.2 引用计数存放的位置sidetable和retainCount、release
1.2.1 SideTables与SideTable:
当优化过的isa指针中,引用计数过大存放不下时,就会将引用计数存放到SideTable中;
SideTables其实是一个哈希表,可以通过对象的指针找到对象内容具体存放在哪个SideTable中:
通过指针找到对象引用计数存放的SideTable:
SideTable对应结构如下图:
在runtime源码中,NSObje.mm可以看到SideTable结构体源码:
struct SideTable {
spinlock_t slock;
RefcountMap refcnts; //referanceCount:引用计数表
weak_table_t weak_table;//弱引用表:存放的对象的弱引用指针
};
问题:为什么不是一个SideTable而是多个SideTable?或者(为什么不将所有的对象放到一个table里面,而是放到不同的side-table里面?)
查找或者修改引用计数的时候是要加锁的,如果有多个对象同时查找引用计数:
- 只有一张表的话,查询肯定是需要加锁,同步有先后顺序的;
- 如果是有多张表,就可以异步进行查询,不同的表之间查询是没有影响的;--> 效率更高
1.2.2 retainCount :
SideTable中的引用计数表中RefcountMap,存放的就是对象的引用计数:retainCount
runtime源码如下:
_objc_rootRetainCount(id obj){
return obj->rootRetainCount();
}
objc_object::rootRetainCount()
{
//如果是taggerPointer,直接返回指针;
if (isTaggedPointer()) return (uintptr_t)this;
sidetable_lock();
isa_t bits = LoadExclusive(&isa.bits);
ClearExclusive(&isa.bits);
//判断是否是优化过的isa指针
if (bits.nonpointer) {
uintptr_t rc = 1 + bits.extra_rc;//extra_rc+1 isa指针中存储的引用计数值;
if (bits.has_sidetable_rc) {
// has_sidetable_rc值如果为1:表示引用计数存放在sidetable中
rc += sidetable_getExtraRC_nolock();//细节:注意+=
}
sidetable_unlock();
return rc;
}
sidetable_unlock();
//不是优化过的isa指针直接查找SideTable
return sidetable_retainCount();
}
//优化过的isa指针在SideTable中查找引用计数
size_t objc_object::sidetable_getExtraRC_nolock()
{
ASSERT(isa.nonpointer);
//找到对应的table
SideTable& table = SideTables()[this];
//table.refcnts(引用计数表)中找到对应计数it
RefcountMap::iterator it = table.refcnts.find(this);
if (it == table.refcnts.end()) return 0;
else return it->second >> SIDE_TABLE_RC_SHIFT;
}
//没有优化过的isa指针在SideTable中查找引用计数
objc_object::sidetable_retainCount()
{
//从sideTables中找出当前对象的sideTable
SideTable& table = SideTables()[this];
size_t refcnt_result = 1;
table.lock();
//table.refcnts(引用计数表)中找到对应计数it
RefcountMap::iterator it = table.refcnts.find(this);
if (it != table.refcnts.end()) {
// this is valid for SIDE_TABLE_RC_PINNED too
refcnt_result += it->second >> SIDE_TABLE_RC_SHIFT;
}
table.unlock();
return refcnt_result;
}
步骤:
- 判断是否是TaggerPointer,如果是,直接返回指针
- 判断是否是优化过的isa指针:
- 如果是优化过的isa指针,先读取isa指针中存放的引用计数extra_rc
- 如果has_sidetable_rc为1,代表引用计数存放在SideTable中;
- 注意:rc = 1 + bits.extra_rc;-->rc += sidetable_getExtraRC_nolock();二者之和;
- 如果不是优化过的isa指针,直接去SideTable中去查找引用计数值;
- 如果是优化过的isa指针,先读取isa指针中存放的引用计数extra_rc
1.2.3 release:
当调用alloc,new,copy,mutablecopy等返回一个对象,在不需要这个对象的时候,需要对这个对象进行release或者autorelease操作,让对象的引用计数-1;
- 在ARC中,LLVM编译器会自动帮我们生成对应的[xxx release];
- 在MRC中,需要我们手动添加[xxx release];
当对象的引用计数减为0时,就会调用dealloc()函数;
- (oneway void)release {
_objc_rootRelease(self);
}
_objc_rootRelease(id obj){
obj->rootRelease();
}
objc_object::rootRelease(bool performDealloc, bool handleUnderflow){
if (isTaggedPointer()) return false;
if (slowpath(!newisa.nonpointer)) {
return sidetable_release(performDealloc);
}
//has_sidetable_rc为1,引用计数存放在sidetable中
if (slowpath(newisa.has_sidetable_rc)) {
// 从sidetable中读取对象引用计数值
size_t borrowed = sidetable_subExtraRC_nolock(RC_HALF);
if (borrowed > 0) {
newisa.extra_rc = borrowed - 1;
// 存储修改过的引用计数值
bool stored = StoreReleaseExclusive(&isa.bits,
oldisa.bits, newisa.bits);
return false;
}
}
// Really deallocate.
//如果对象的引用计数减为0,使用objc_msgSend()发送消息调用dealloc()方法;
if (performDealloc) {
objc_msgSend)(this, @selector(dealloc));
}
return true;
}
- 注意在引用计数减为0时,会发送消息调用对象的dealloc()方法销毁对象,并释放对象占用的内存空间;
1.3 weak指针实现原理
示例如下图:
YYPerson只是重写了dealloc方法,添加打印对象释放时机;
-
当在作用域{}中,LLVM编译器会自动添加release操作,使得引用计数-1;
- __strong YYPerson * person1; // 强指针
- __weak YYPerson * person2; // 弱指针
- __unsafe_unretained YYPerson * person3; // 弱指针
1、当没有任何指针指向[YYPerson alloc]创建出来的对象时,出了{}作用域,直接释放:
2、 强引用:__strong:person1
当有强引用指针指向[YYPerson alloc]创建出来的对象时,引用计数+1;
3、弱引用:
- __weak:person2
- __unsafe_unretained:person3
当有弱引用指针指向[YYPerson alloc]创建出来的对象时,引用计数不变;现象如下:
1.3.1 __weak 和 __unsafe-unretained区别
在对象释放以后调用weak指针: 打印显示指针为空;
在对象释放以后调用:_unsafe-unretained指针,直接崩溃,显示野指针错误(指针指向地址的对象已经被释放)
区别 :
weak指针在对象销毁时,dealloc()方法会调用rootDealloc方法,最终会判断,如果该对象有弱引用,会将存放弱引用的散列表weakTable清空,所以最终weak指针会置为nil;
__unsafe_unretained不会有将对应指针清空的操作,所以不太安全,在对象销毁释放以后,再去调用指针就回造成坏内存访问:EXC-BAD-ACCESS;
注意: weak指针置为nil后,再调用方法,在objc_msgSend()中,会先判断,如果消息接收者为空,则直接return,所以不会调用方法,也不会crash;
1.3.2 objc_initWeak()
在进行编译过程前,clang 其实对 __weak 做了转换,调用objc_initWeak,将对应的弱引用指针存储到SideTable中:
NSObject objc_initWeak(&p, 对象指针);
id objc_initWeak(id *location, id newObj) {
// 查看对象实例是否有效
// 无效对象直接导致指针释放
if (!newObj) {
*location = nil;
return nil;
}
// 这里传递了三个 bool 数值
// 使用 template 进行常量参数传递是为了优化性能
// storeWeak:将弱引用指针存放到SideTable中
return storeWeak<false/*old*/, true/*new*/, true/*crash*/>
(location, (objc_object*)newObj);
}
这一块知识点参考了瓜神详细讲解weak实现原理的博文:
瓜神-弱引用的实现方式
1.4 dealloc()方法
当一个对象的引用计数减为0时,就会调用dealloc()方法释放对象并清理对应的内存空间
- (void)dealloc {
_objc_rootDealloc(self);
}
_objc_rootDealloc(id obj){
obj->rootDealloc();
}
objc_object::rootDealloc(){
if (isTaggedPointer()) return; // 如果是isTaggedPointer直接return
/*
isa.nonpointer :0,代表普通的指针,存储着Class、Meta-Class对象的内存地址
1,代表优化过,使用位域存储更多的信息
isa.weakly_referenced:是否有被弱引用指向过,如果没有,释放时会更快
isa.has_assoc:是否有设置过关联对象,如果没有,释放时会更快
isa.has_sidetable_rc:引用计数器是否过大无法存储在isa中,如果为1,那么引用计数会存储在一个叫SideTable的类的属性中
**/
if (fastpath(isa.nonpointer &&
!isa.weakly_referenced &&
!isa.has_assoc &&
!isa.has_cxx_dtor &&
!isa.has_sidetable_rc))
{
free(this);//上述条件都不满足,直接free(),释放当前对象
} else {
object_dispose((id)this);//清理其他相关和引用
}
}
id object_dispose(id obj){
if (!obj) return nil;
objc_destructInstance(obj);
free(obj);
return nil;
}
void *objc_destructInstance(id obj)
{
if (obj) {
// 判断是否有c++析构函数和关联对象
bool cxx = obj->hasCxxDtor();
bool assoc = obj->hasAssociatedObjects();
// 处理c++析构函数相关:清除成员变量
if (cxx) object_cxxDestruct(obj);
//移除关联对象
if (assoc) _object_remove_assocations(obj);
//将指向当前对象的弱引用指针置为nil
obj->clearDeallocating();
}
return obj;
}
objc_object::clearDeallocating()
{
if (slowpath(!isa.nonpointer)) {
// 普通的isa指针
sidetable_clearDeallocating();
}
else if (slowpath(isa.weakly_referenced || isa.has_sidetable_rc)) {
// 优化过的isa指针
clearDeallocating_slow();
}
assert(!sidetable_present());
}
objc_object::clearDeallocating_slow(){
//从 SideTables中取出指针对应存放的SideTable
SideTable& table = SideTables()[this];
//如果有弱引用,清除弱引用指针
if (isa.weakly_referenced) {
weak_clear_no_lock(&table.weak_table, (id)this);
}
//如果引用计数不是存放在isa中,而是存放在SideTable中,清除SideTable中refcnts(引用计数表)中的引用计数
if (isa.has_sidetable_rc) {
table.refcnts.erase(this);
}
}
void
weak_clear_no_lock(weak_table_t *weak_table, id referent_id) {
objc_object *referent = (objc_object *)referent_id;
//通过弱引用表weak_table和弱引用指针referent,找出weak_table中弱引用对象的索引entry
weak_entry_t *entry = weak_entry_for_referent(weak_table, referent);
if (entry == nil) {
return;
}
// zero out references
//DisguisedPtr<objc_object *> weak_referrer_t;
//weak_referrer_t是一个集合类型
weak_referrer_t *referrers;
size_t count;
if (entry->out_of_line()) {
referrers = entry->referrers;
count = TABLE_SIZE(entry);
}
else {
referrers = entry->inline_referrers;
count = WEAK_INLINE_COUNT;
}
for (size_t i = 0; i < count; ++i) {
objc_object **referrer = referrers[i];
if (referrer) {
//遍历如果referrer == 传进来的referent,置为nil
if (*referrer == referent) {
*referrer = nil;
}
else if (*referrer) {
_objc_inform("__weak variable at %p holds %p instead of %p. "
"This is probably incorrect use of "
"objc_storeWeak() and objc_loadWeak(). "
"Break on objc_weak_error to debug.\n",
referrer, (void*)*referrer, (void*)referent);
objc_weak_error();
}
}
}
//清空weak_table对应索引entry中的内容
weak_entry_remove(weak_table, entry);
}
2. autorelease 和 AutoReleasePool
在MRC环境下,有些对象的释放调用了[XXX autoerlease]来释放对象,�调用autorelease的对象不会立即释放,也就是对象的引用计数不会立马 -1;
通过对autorelease方法的研究发现:
调用autorelease 的对象的生命周期是通过一个叫AutoreleasePoolPage的对象来管理的,调用autorelease的对象,其实在@autoreleasepool中执行了一下操作:
@autoreleasepool {
// atautoreleasepoolobj = objc_autoreleasePoolPush();
执行操作
// objc_autoreleasePoolPop(atautoreleasepoolobj);
}
objc_autoreleasePoolPush(): 入栈 --> 将对象加入到AutoreleasePoolPage表中;
objc_autoreleasePoolPop(): 出栈 --> 将对象从AutoreleasePoolPage表中移除;
2.1 AutoReleasePoolPage结构
调用autoreleasePoolPush()函数操作时,会将调用autorelease的对象加入到AutoReleasePoolPage中;
AutoReleasePoolPage其实是:以栈为节点通过双向链表的形式链接起来的数据结构
其结构体成员中,有以下注意的地方
- next : 指向的是下一个可以存放元素的位置;
- thread : 线程,AutoReleasePoolPage和线程是对应关系
- parent : 双向链表中的 prev指针,指向上一个AutoReleasePoolPage
- child : 双向链表中的 next指针,指向下一个AutoReleasePoolPage
2.1.1 autoreleasePoolPush
AutoReleasePoolPage是一个 栈的结构,栈的特点是: 先进后出 :
如下图,入栈顺序为0-9,出栈顺序为9-0:
在执行push操作时,例如添加一个obj(3):
先将哨兵对象指向的位置置为nil;
将push进来的对象指针添加到
再将next指针和哨兵对象向上移动;
如下图:哨兵对象其实是指一个值为POOL_BOUNDARY的值,这个标识了当前autoreleasePool池的起始位置;
autorelease流程:
首先判断next指针是否在栈顶,每个autorelpool都是4096字节:
- 当next指针指向栈顶的时候,当前page已存满,重新创建一个page对象来存放push进来的对象
- 当next不是栈顶时,直接执行入栈操作;
2.1.2 autoreleasePoolPop
执行pop操作有以下流程:
- 根绝传入的哨兵对象找到对应的位置
- 对上次执行push操作添加的所有对象依次发送release消息
- 回退next指针到正确的位置(到另一个哨兵对象的位置,一个page对象只有一个next指针,但是哨兵对象可以有多个)
执行pop操作前:
执行pop操作结束后:到下一个哨兵对象前的对象都被释放,并改变next指针的位置。
所以,总结一下:
调用push方法会将一个POOL_BOUNDARY(哨兵对象)入栈,并且返回其存放的内存地址
调用pop方法时传入一个POOL_BOUNDARY(哨兵对象)的内存地址,会从最后一个入栈的对象开始发送release消息,直到遇到这个POOL_BOUNDARY
id *next指向了下一个能存放autorelease对象地址的区域
2.1.2 多个@autoreleasepool和嵌套使用
在有多个@autoreleasepool{}时,遵循3步:
1、 push入栈;
2、 执行代码;
3、 pop出栈;
多个平级的@autoreleasepool{};
多个嵌套的@autoreleasepool{};
3. autorelease与runloop的关系
经过前面的了解,调用autorelease的对象都是通过autoreleasePoolPage来管理的,而autoreleasePoolPage结构体对象中,有一个NSThread对象,说明autoreleasePoolPage来管理对象的入栈和出栈和线程有一定的关系,而线程处理任务离不开runloop,所以得仔细探究一下runLoop和autorelease之间的联系;
我们知道,在Runloop中,runloop有不同的模式,每种Mode下都会有observers,source0,source1,timers,_name,其中sources和timers是runloop需要处理的任务;
在runloop每一次运行循环中,都会处理一遍所有的timers和blocks,然后进入休眠状态,如果有事件处理,就被唤醒,再次进行循环,处理一遍这些事情;
但是在runloop进行休眠之前,也就是在状态kCFRunLoopBeforeWaiting之前,会处理一些特殊的事情,比如刷新界面的UI:
问题: 在修改UI,背景色或添加一个subview等操作后,是立即生效执行的吗?
答案是:不会立即生效,而是在当前线程进入休眠,也就是kCFRunLoopBeforeWaiting之前进行刷新操作(刷新UI是在主线程,所以当前线程是指主线程);
所以猜测: 调用autorelease的对象也有可能是在当前线程休眠的时候出栈释放的;
以下进行验证:
3.1 添加observer监听runloop的状态
runloop的状态是一个枚举,其对应的各种状态值如下:
有以下两种方式添加一个observer监听Runloop的状态:(注意C语言创建的对象最后都需要release)
方式一: 添加一个C语言ObserverCallBack()函数回调:
void ObserverCallBack (CFRunLoopObserverRef observer, CFRunLoopActivity activity, void *info){
switch (activity) {
case kCFRunLoopEntry:
NSLog(@"kCFRunLoopEntry");
break;
case kCFRunLoopBeforeTimers:
NSLog(@"kCFRunLoopBeforeTimers");
break;
case kCFRunLoopBeforeSources:
NSLog(@"kCFRunLoopBeforeSources");
break;
case kCFRunLoopBeforeWaiting:
NSLog(@"kCFRunLoopBeforeWaiting");
break;
case kCFRunLoopAfterWaiting:
NSLog(@"kCFRunLoopAfterWaiting");
break;
case kCFRunLoopExit:
NSLog(@"kCFRunLoopExit");
break;
break;
default:
break;
}
}
- (void)observeRunloopMode{
CFRunLoopObserverRef observeRef = CFRunLoopObserverCreate(kCFAllocatorDefault, kCFRunLoopAllActivities, YES, 0, ObserverCallBack, NULL);
CFRunLoopRef current = CFRunLoopGetCurrent();
CFRunLoopAddObserver( current, observeRef, kCFRunLoopCommonModes);
CFRelease(current);
CFRelease(observeRef);
}
方式二: 直接使用block(这种方式更简单)
- (void)observeRunloopMode{
CFRunLoopObserverRef observeRef =CFRunLoopObserverCreateWithHandler(kCFAllocatorDefault, kCFRunLoopAllActivities, YES, 0, ^(CFRunLoopObserverRef observer, CFRunLoopActivity activity) {
switch (activity) {
case kCFRunLoopEntry:{
CFRunLoopMode mode = CFRunLoopCopyCurrentMode(CFRunLoopGetCurrent());
NSLog(@"kCFRunLoopEntry ----- %@",mode);
CFRelease(mode);
break;
}
case kCFRunLoopBeforeWaiting:{
CFRunLoopMode mode = CFRunLoopCopyCurrentMode(CFRunLoopGetCurrent());
NSLog(@"kCFRunLoopBeforeWaiting ----- %@-----------",mode);
CFRelease(mode);
break;
}
case kCFRunLoopAfterWaiting:{
CFRunLoopMode mode = CFRunLoopCopyCurrentMode(CFRunLoopGetCurrent());
NSLog(@"kCFRunLoopAfterWaiting ----- %@",mode);
CFRelease(mode);
break;
}
case kCFRunLoopBeforeTimers:{
CFRunLoopMode mode = CFRunLoopCopyCurrentMode(CFRunLoopGetCurrent());
NSLog(@"kCFRunLoopBeforeTimers ----- %@",mode);
CFRelease(mode);
break;
}
case kCFRunLoopBeforeSources:{
CFRunLoopMode mode = CFRunLoopCopyCurrentMode(CFRunLoopGetCurrent());
NSLog(@"kCFRunLoopBeforeSources ----- %@",mode);
CFRelease(mode);
break;
}
case kCFRunLoopExit:{
CFRunLoopMode mode = CFRunLoopCopyCurrentMode(CFRunLoopGetCurrent());
NSLog(@"kCFRunLoopExit ----- %@",mode);
CFRelease(mode);
break;
break;
}
default:
break;
}
});
CFRunLoopRef current = CFRunLoopGetCurrent();
CFRunLoopAddObserver( current, observeRef, kCFRunLoopCommonModes);
CFRelease(current);
CFRelease(observeRef);
}
有了监听Runloop状态的方法,验证猜测结果如下:
所以结论如下:
主线程 的Runloop中注册了2个Observer :
第1个Observer监听了kCFRunLoopEntry事件,会调用objc_autoreleasePoolPush();(优先级最高,保证创建释放池发生在其他所有回调之前)
-
第2个Observer
监听了kCFRunLoopBeforeWaiting事件,会调用objc_autoreleasePoolPop()、objc_autoreleasePoolPush();
监听了kCFRunLoopBeforeExit事件,会调用objc_autoreleasePoolPop()
子线程 中:
- 子线程创建的时候就会创建一个autoreleasepool,并且在线程退出的时候,清空autoreleasepool。
结语
笔记中提到的内容大部分总结自小码哥底层原理,仔细总结起来发现好多细节要自己实现了才能深刻的理解;
学无止境,关于内存管理这块还有很多需要查缺补漏的地方,这篇只是自己的学习笔记,有不对的地方请见谅。