0.知识预备
一般一个进程由以下几个部分组成: .text(code代码段,只读)、.data(初始化的非零全局变量)、.bss(初始化的和零值全局变量)、heap(堆)、stack(栈)
大致内存模型如图所示(copy自black)
动态分配的是堆&栈这两部分空间
堆
用于alloc/new/copy等方法创建一个对象时,分配给进程,常见的便是伙伴算法,用一种链表数组的方式进行内存分配,堆内存分配需要程序员自己调用或者通过某种机制调用dealloc/delete方法归还。需要程序员管理。
栈
一般的函数内部储存临时变量的所在区域,在函数结束后,其生命周期结束,系统栈指针自动pop,原来的栈区空间变为可用,等待新的临时变量覆盖。不需要程序员管理。
举例如下:
- (returnType) func (void)
{
NSString * st = [[NSString alloc] initWithString : @"hello!\n"];
//do something
return st;
}
那么st指针这个临时变量申明在栈区,函数结束返回后,栈指针改变,占用的栈区重新变为可用区域,其生命周期结束,该内存区域的值将会在下一次生成的临时变量覆盖掉。而alloc申请的NSString对象分配在堆区,如果没有dealloc等方法释放,将会一直存在,直到进程结束被系统回收。所以进程申请的堆区内存管理很有必要,不然进程运行期间会造成大量的内存泄露。当然不能忽视的是,即使进程结束操作系统会对分配给进程的内存回收,但是有些内存比如通过Linux系统APIshmget获取的共享内存,进程结束后操作系统并不回收。
1.计数机制管理内存
(1)Objective-C的计数机制
Objective-C语言使用了一种内存计数机制来管理内存,通俗地讲就是统计堆区分配的对象的拥有者数量,为零时调用dealloc(自动调用)将其释放。通过retain计数+1,release计数-1,如果release后计数为0,则在release中调用dealloc方法;举例如下
@interface Car:NSObject
- (void) setEngine: ((Engine *) newEngine);
- (Engine *) engine;
@end
@implementation Engine
- (void) setEngine : (Engine *) newEngine
{
[newEngine retain];
[engine release];
engine = newEngine;
}
@end
这是一个car对象的setEngine方法,修改其指向的Engine对象,所以先让newEngine指向的对象拥有者计数+1,然后让自己之前指向的engine对象-1,释放拥有权。为什么先retain,假设engine指向的对象计数值为1,考虑到newEngine和当前engine指向的是同一个对象的状况,先release将会导致对象被释放,这时engine和newEngine都将会指向释放内存产生错误。
(2) MRC机制
<1>MRC原理
计数值为0释放对象,retain增加计数,release减少计数。内存管理由程序员手动添加retain和release进行程序管理。当对象引用计数为0时,对象被销毁时,系统发送一条dealloc消息。一般开发中我们将重写dealloc方法,释放相关资源。同时一旦调用了dealloc方法,就必须在最后调用[super dealloc];
MRC内存原则
- 需要引用时,+1;不需要引用了,-1;
- 谁创建,谁release;
- 谁retain,谁release;
- 只要调用了alloc,必须有release(autorelease)
- 成员变量的set方法中需要注意的:基本数据不用管,对象数据需要注意。
<2>autorelease概念
有时候会出现并不能很好地确定合适的release时机,如下列代码
- (NSString *) description
{
NSString * description;
description = [[NSString alloc ]
initWithFormat: @ "hello beauty !"];
return (description);
}//description
这段代码中返回了一个NSString对象指针,使得生成的NSString对象retainCount值为1,那么让谁负责释放它?根据内存管理原则,谁创建谁释放,description指针在其生命周期结束时,ARC插入了release语句;谁retain,谁释放,返回的临时指针变量赋值给调用方后生命周期结束,相当于调用方变量接棒了临时变量retain的持有,应该由调用方释放,那么调用代码应该如此写成如下
NSString * desc = [someobject description];
NSLog(@"%@",desc);
[desc release];
哦,Dear,这样未免太过于不精致了,一行代码却变成了三行,更好的解决办法是什么?是的,autorelease pool是时候出来了,先看autorelease方法
-(id) autorelease;
autorelease方法预先设定了一条会在未来发送的release消息,返回值是接受这条消息的对象,当给一个对象发送autorelease消息时,实际上将该对象添加到了自动释放池中。当自动释放池被销毁时,会向该池中所有对象发送release信息。
这时我们更新我们的description代码
- (NSString *) description
{
NSString * description;
description = [[NSString alloc ]
initWithFormat: @ "hello beauty !"];
return ([description autorelease]);
}//description
现在我们只用调用这一段代码就够了
NSLog(@"%@",[someObject description]);
<3>autorelease pool
自动释放池的用法有两种
@autoreleasepool{
//code in here,关键字方法
}
NSAutoreleasePool *pool
pool = [NSAutoreleasePool new];
//code in here,NSAutoreleasePool对象方法
[pool release]
后者是通过创建一个NSAutoreleasePool对象的方式,当池子被释放后,其保留计数器值归0,然后池子被销毁,销毁过程中该池子将释放在其中的每一个对象。
两个方法中,@autoreleasepool
方法更快,因为一般而言Objective-C语言创建和释放内存的能力在我们之上。
观察以下代码
#import <Foundation/Foundation.h>
__weak id p;
NSString * func()
{
NSString * a = [[NSString alloc]
initWithFormat:@"hello beauty!"];
p = a;
return a;
}
int main(int argc, const char * argv[]) {
@autoreleasepool {
NSString * b = func();
NSLog(@"the addr is %p",p);
NSLog(@"%@",b);
NSLog(@"the addr is %p",p);
}
NSLog(@"the addr is %p",p);
return 0;
}
运行结果如下
2018-04-02 17:07:16.750504+0800 test[3571:293795] the addr is 0x100428150
2018-04-02 17:07:16.750705+0800 test[3571:293795] hello beauty!
2018-04-02 17:07:16.750743+0800 test[3571:293795] the addr is 0x100428150
2018-04-02 17:07:16.750766+0800 test[3571:293795] the addr is 0x0
从这一角度看autorelease是一种延迟释放,释放的时机是在每一次runloop之后/autoreleasepool{}代码块结束。
<4>autoloop概念
IOS时是由一个一个runloop组成的,每个runloop都会执行下图的一些步骤:
可以看到,每个runloop中都创建一个Autorelease Pool,并在runloop的末尾进行释放,所以,一般情况下,每个接受autorelease消息的对象,都会在下个runloop开始前被释放。也就是说,在一段同步的代码中执行过程中,生成的对象接受autorelease消息后,一般是不会在代码段执行完成前释放的。
那通过们自己创建的Pool,
@autoreleasepool{
//code in here,关键字方法
}
可以提前释放掉一些对象,避免内存占用过大的情况。如下面的循环情况
for (int i = 0; i < 1000; i++)
{
@autoreleasepool
{
//大量的临时对象
}
}
下面是苹果的Using Autorelease Pool Blocks说明
Cocoa总是希望代码在自动释放池块中执行,否则自动释放的对象不会被释放,并且您的应用程序会泄漏内存。(如果你
autorelease
在自动释放池块外发送消息,Cocoa会记录一条合适的错误消息。)AppKit和UIKit框架处理自动释放池块中的每个事件循环迭代(例如鼠标放下事件或点击,即会runloop改变)。因此,您通常不必自己创建自动释放池块,甚至也看不到用于创建自动释放池的代码。但是,有三种情况您可以使用自己的自动释放池块:
如果您正在编写不基于UI框架的程序,例如命令行工具。
如果你编写一个创建许多临时对象的循环。
您可以在循环中使用自动释放池块来在下一次迭代之前处理这些对象。在循环中使用自动释放池块有助于减少应用程序的最大内存占用量。
如果你产生了一个辅助线程。
一旦线程开始执行,您必须创建自己的自动释放池块; 否则,你的应用程序会泄漏对象。(有关详细信息,请参阅自动释放池块和线程。)
值得注意的是,在实际的IOS开发中,苹果公司一般不推荐我们在自己的代码中使用autorelease方法,也不要使用会返回自动释放对象的一些便利方法,一般这些方法的会返回一个新对象的类方法,如NSString的stringWith开头的方法。
关于runloop的具体机制,这个感觉很吃时间和基础,我现在学得还不够扎实,还需要啃,暂时不做更多的阐述,等后面继续阐述。
标记下可以参考的文章
(3) ARC机制
ARC机制是在编译时候为我们主动添加retain和release,ARC自动标记autorelease的对象会在autoreleasepool的runloop更新时release掉。有时候runloop内会产生大量的临时对象,这时候我们就通过自己建立的@autoreleasepool进行提前释放,避免内存占用过高。
ARC机制的有效对象
- 代码块指针
- Objective-C对象指针
- 通过_attribute((NSObject))类型定义的指针
如char *指针,CF对象(如CFStringREf)是不支持ARC特性的我们需要自己编写内存管理代码,协同ARC机制共同发挥作用。
2.深入ARC内存管理
(1)ARC的实现
使用ARC,开发者不再需要手动的retain/release/autorelease. 编译器会自动插入对应的代码,再结合Objective C的runtime,实现自动引用计数。如ARC代码段
{
//code here
NSObject * obj =[[NSObject alloc] init];
//doing sth
}
等同于MRC
{
//code here
NSObject * obj =[[NSObject alloc] init];
//doing sth
[obj release];
}
属性所有权关键字如下
- assign对应关键字__unsafe_unretained, 顾名思义,就是指向的对象被释放的时候,仍然指向之前的地址,容易引起野指针。
- copy对应关键字__strong,只不过在赋值的时候,调用copy方法。
- retain对应__strong
- strong对应__strong
- unsafe_unretained对应__unsafe_unretained
- weak对应__weak。
(2)计数方法的实现
引用计数主要依赖于这三个方法:
- retain 增加引用计数
- release 降低引用计数,引用计数为0的时候,释放对象。
- autorelease 在当前的auto release pool结束后,降低引用计数。
在Cocoa Touch中,NSObject协议中定义了这三个方法,由于Cocoa Touch中,绝大部分类都继承自NSObject(NSObject类本身实现了NSObject协议),所以可以“免费”获得NSObject提供的运行时和ARC管理方法,这就是为什么适用OC开发iOS的时候,你的类要继承自NSObject。通过开源的runtime代码我们可以一探究竟。
<1>retain与release
先看NSObject的retain
//来自NSObject.mm文件
// Replaced by ObjectAlloc
- (id)retain {
return ((id)self)->rootRetain();
}
//rootretain()定义简化如下,原代码位于objc-object.h
inline id objc_object::rootRetain()
{
return rootRetain(false, false);
}
inline id objc_object::rootRetain(bool tryRetain, bool handleOverflow)
{
if (isTaggedPointer()) return (id)this; //Taggedpointer是假对象
isa_t oldisa;
isa_t newisa;
do{
oldisa = LoadExclusive(&isa.bits);
newisa = oldisa; //原值
if(xx) sidetable_unlock();
uintptr_t carry;
newisa.bits = addc(newisa.bits, RC_ONE, 0, &carry);
// rc计数++
if(slowpath(carry)){
//省略一段判定
if (!tryRetain && !sideTableLocked) sidetable_lock();
//省略。。
}
}while(xxx);
//省略一段代码
}
可以发现retain
的操作主要是通过最终调用rootRetain(false, false);
使得引用计数值++,是通过SideTable进行管理。
struct SideTable {
spinlock_t slock;
RefcountMap refcnts;
weak_table_t weak_table;
SideTable() {
memset(&weak_table, 0, sizeof(weak_table));
}
~SideTable() {
_objc_fatal("Do not delete SideTable.");
}
void lock() { slock.lock(); }
void unlock() { slock.unlock(); }
void forceReset() { slock.forceReset(); }
// Address-ordered lock discipline for a pair of side tables.
template<HaveOld, HaveNew>
static void lockTwo(SideTable *lock1, SideTable *lock2);
template<HaveOld, HaveNew>
static void unlockTwo(SideTable *lock1, SideTable *lock2);
};
可以看到有一个自旋锁,一个引用计数map。从这里我们就可以比较清楚的看到了引用计数的底层实现,存在一个全局的map,以地址作为key,引用计数的值为value。
release的实现相似,不过多了一个计数值为零的dealloc释放代码
ALWAYS_INLINE bool
objc_object::rootRelease(bool performDealloc, bool handleUnderflow)
{
if (isTaggedPointer()) return false;
isa_t oldisa;
isa_t newisa;
do {
oldisa = LoadExclusive(&isa.bits);
newisa = oldisa;
if(xx) sidetable_unlock();
// don't check newisa.fast_rr; we already called any RR overrides
uintptr_t carry;
newisa.bits = subc(newisa.bits, RC_ONE, 0, &carry);
// rc计数值--
if (slowpath(carry)) {
// don't ClearExclusive()
goto underflow; //underflow进行lock,dealloc判定等
}
} while (xx);
underflow:
//省略大部分代码实现
if (!sideTableLocked) {
ClearExclusive(&isa.bits);
sidetable_lock();
}
if (performDealloc) {
((void(*)(objc_object *, SEL))objc_msgSend)(this, SEL_dealloc);
}
//发送dealloc消息
return true;
}
release:查找map,对引用计数减1,如果引用计数小于阈值,则调用SEL_dealloc
<2>autorelease pool
我们提到了,使用autorelease
可以将对象放入autorelease pool中,等到pool drain时候,会对池中的每一个对象发送release消息。我们自己建立的release pool在作用域结束后release所有池中对象,主线程runloop的autorelease pool会在runloop更新时drain,向对象发送release消息。
先看autorelease方法实现
static inline id autorelease(id obj)
{
assert(obj);
assert(!obj->isTaggedPointer());
id *dest __unused = autoreleaseFast(obj);
assert(!dest || dest == EMPTY_POOL_PLACEHOLDER || *dest == obj);
return obj;
}
static inline id *autoreleaseFast(id obj)
{
AutoreleasePoolPage *page = hotPage();
if (page && !page->full()) {
return page->add(obj);
} else if (page) {
return autoreleaseFullPage(obj, page);
} else {
return autoreleaseNoPage(obj);
}
}
id *add(id obj)
{
assert(!full());
unprotect();
id *ret = next; // faster than `return next-1` because of aliasing
*next++ = obj;
protect();
return ret;
}
void releaseAll()
{
releaseUntil(begin());
}
void releaseUntil(id *stop)
{
//省略部分代码实现
if (obj != POOL_BOUNDARY) {
objc_release(obj);
}
}
可以看到autorelease会把对象存储到AutoreleasePoolPage的链表里。等到auto release pool被释放的时候,把链表内存储的对象release掉。所以,AutoreleasePoolPage就是自动释放池的内部实现。
class AutoreleasePoolPage
{
static size_t const COUNT = SIZE / sizeof(id);
magic_t const magic;
id *next;
pthread_t const thread;
AutoreleasePoolPage * const parent;
AutoreleasePoolPage *child;
uint32_t const depth;
uint32_t hiwat;
id * begin();
id * end();
id *add(id obj);
void releaseAll() ;
//省略一堆方法实现
}
其中parent和child指针用来构造双链表,magic校验AutoreleasePoolPage完整性,thread保存所在线程。