内存管理

413 阅读8分钟

      记录整理一下我所理解的OC的内存管理,以及关于内存管理的一些思考(问题)。

OC内存管理机制是什么?

ARC(Automatic Reference Counting),自动引用计数,是OC的内存管理机制。简单的来说,就是编译器在编译时期,会帮我们自动插入retain,release代码。我们不用像MRC时期,手动retain对象获取内存,release对象释放内存。

引用计数

此处要说个例子,让我对内存管理有了更深理解的例子~

假设办公室的灯只有一个,有人进来,就开关。就人出去,就关灯。如果每个人进来时开灯,出去时关灯,那么肯定会出现办公室里面有人,灯确是关的状态。

解决这一问题的办法是:办公室里第一个人来的时候开灯,最后一个人离开的时候关灯。如下所示

办公室人数 引用计数 灯的状态 对OC对象所做的动作 我的理解
0个人 0 关灯 - 把灯当成一个实例对象
进来1个人 1 开灯 生成并持有对象 开辟一块儿内存地址,且有一指针指向对象的地址
又进来1个人 2 开灯 持有对象(retain) 多了一个指针指向内存地址
走了1个人 1 开灯 释放对象(release) 少了一个指针指向内存地址
又走了1个人 0 关灯 废弃对象(dealloc) 没有指针指向内存,内存被释放掉

OC对象通过引数计数也能得到很好的管理。上表中,生成并持有对象时,分两种

  1. 持有自己生成的对象。(alloc,new,copy,mutableCopy等)
  2. 持有非自己生成的对象。(除了第一种的,常见的array,dictionary这种类方法等)

有什么区别呢?代码(MRC环境)如下:

//自己生成的对象自己持有
NSMutableArray *array = [[NSMutableArray alloc] init];
//不需要retain。不需要对象时,进行释放。
[array release];

//持有非自己生成的对象
NSMutableArray *array = [NSMutableArray array];//对象被注册到autoreleasepool中,当pool结束时会自动调用release。
//需要retain来持有对象,在不需要对象时,进行释放。
[array retain];
[array release];

但是,在ARC下,不管是自己生成的还是非自己生成的,用strong之后,系统会自动判断,让我们生成并持有。

与内存管理有关的关键字

strong

strong 表示对对象的强引用,指向并拥有该对象,其修饰的对象,引用计数会+1。

只要有任何一个强引用指向对象,对象都不会被释放。只有对象在引用计数为0,或者强行将其置为nil时被销毁。

在arc中,对象类型和id类型的变量默认都是用strong来修饰的。上面持有非自己生成的对象例子,在ARC里面,代码如下:

{
   NSMutableArray *array = [NSMutableArray array];
}

array是用__strong修饰符来修饰,在作用域结束时,会对对象进行release。

strong对象之间赋值

  1. 有两个对象,obj0和obj1,里面有个strong修饰的TestObject的属性。
{
    TestObject obj0 = [[TestObject alloc] init];//obj0对 对象0 强引用
    TestObject obj1 = [[TestObject alloc] init];//obj1对 对象1 强引用
    obj0 = obj1;//obj0重新赋值,对 obj1持有的对象1 强引用,对  原持有对象0的强引用消失
}

也就是说对象1有2个强引用,对象0无引用被废弃。出作用域后,obj0和obj1自动释放自己强持有的对象。对象1无强引用,也会被废弃。 如图所示

  1. obj0里面有个strong修饰的TestObject *obj,obj1里有个strong修饰的TestObject *obj。
{
    TestObject obj0 = [[TestObject alloc] init];//obj0对 对象0 强引用
    TestObject obj1 = [[TestObject alloc] init];//obj1对 对象1 强引用
    obj0.obj = obj1;//对象0的obj变量 对 对象1 强引用
    obj1.obj = obj0;//对象1的obj变量 对 对象0 强引用
}

对象0有两个强引用,obj0和obj1.obj。对象1有两个强引用,obj1和obj0.obj

出了作用域后,obj0和obj1释放对自己持有对象的引用。此时对象0有一个obj1.obj的引用,对象1有一个obj0.obj的引用。导致对象0和对象1都释放不掉。这也就造成了循环引用问题,引发内存泄露。

weak

weak表示对对象的弱引用,指向但并不拥有该对象,其修饰的对象引用计数不会增加。 无须手动设置,该对象会自行在内存中销毁。

weak可用来解决循环引用问题。比如上面的,

  1. 让其中一方的obj变量用strong修饰改成weak。
  2. 那么出了作用域后,obj0和obj1释放对自己持有对象的引用,此时对象0没有obj1.obj的强引用,对象0释放。
  3. obj0.obj释放对对象1的强引用,对象1也就释放掉了。

assign

assign主要用于修饰基本数据类型,比如NSInteger和CGFloat,这些数值主要存在于栈中。

assign一般不用来修饰对象。因为assign修饰的对象被释放掉后,指针地址依然存在,会造成"野指针",在堆上易崩溃。而栈上的内存系统会自动处理。

copy

与strong类似。不同的是,strong是多个指针指向同一地址,而copy会通过NSCopying接口的copyWithZone:方法复制一份新的地址,指针指向不同的地址。一般用来修饰有对应可变类型的不可变对象上,比如NSString,NSArray和NSDictionary。

思考

下面这种情况会造成循环引用吗?循环引用是什么?

以delegate为例。

  1. 有两个vc,AViewController , BViewController。
  2. vcB.delegate = vcA。且vcB的delegate属性是用strong来修饰的。

vcA代码如下。

    BViewController *vcB = [[BViewController alloc] init];
    vcB.delegate = self;
    [self.navigationController pushViewController:vcB animated:YES];

上面这种情况并不会造成循环引用,因为当vcB pop回来后,并没有对象再持有vcB。

循环引用是指两个或两个以上对象互相强引用,导致所有对象无法释放的现象。

如果将vcB改成self.vcB。且vcB是用strong修饰的,那么会导致循环引用,因为vcA对vcB强引用,而vcB的delegate又对vcA强引用,所以当vcA被pop回来后vcA和vcB互相持有,导致释放不掉。

当然此时将delegate改成weak修饰,也会有内存问题。当vcB pop回来后,由于vcA还持有vcB,所以会导致vcB延迟释放,需要等vcA释放后,vcB才会dealloc。

NSTimer为什么会造成内存泄露?怎么解决?

self强引用timer,timer会被加到Runloop中。所以timer不销毁,self也不会被销毁。

常见的解决方法有:

  1. 手动调用invalidate。可在viewWillDisappear或者didMoveToParentViewController里面直接调用invalidate并置为nil。
  2. 自定义timer对象,在dealloc的时候调用此timer对象的方法(方法实现是对对象里面的timer invalidate并置为nil)。

dealloc什么时候会走?里面一般用来做什么操作?

当对象的引用记数为0时,对象会调用dealloc。 在dealloc中,我们一般会释放观察的行为,比如通知移除、kvo监听移除等

引用计数的值是存在对象中吗?

苹果并没有把引用计数的值放在对象中,而是通过散列表(引用计数表)来管理引用计数的,key是对象内存地址,value是它所对象的引用计数。

autorelease 原理是什么

autorelease使对象在超出生命周期后能正确的被释放(通过调用release方法)。在调用release 后,对象会被立即释放,而调用 autorelease 后,对象不会被立即释放,而是注册到 autoreleasepool 中,经过一段时间后 pool结束,此时调用release方法,对象被释放。

autorelease的核心可以说是AutoreleasePoolPage这个类

  1. 进释放池时:objc_autoreleasePoolPush()。
  2. 出释放池是:objc_autoreleasePoolPop() 以上两个方法都在AutoreleasePoolPage这个类里面,里面有个栈,有一个对象注册进来,就在栈顶加一个。pop后,栈内对象调用release方法。 具体可见 blog.sunnyxx.com/2014/10/15/…

autorelease对象什么时候释放?

autorelease对象是被注册到autoreleasePool中,在pool结束时调用release,释放对象。pool不结束,autorelease对象不会被释放。在一次runloop结束后,autorelease对象会被释放掉。

autorelease对象一定会被加进自动释放池吗?比如下面这种

{
    id __strong obj = [NSMutableArray array];
}

这种并不会被加进自动释放池。模拟的编译器代码如下

{
    id obj = objc_msgSend(NSMutableArray), @selector(array));
    objc_retainAutoreasedReturnValue(obj);
    objc_release(obj);
}
+ (id)array {
    id obj = objc_msgSend(NSMutableArray, @selector(alloc));
    objc_msgSend(obj, @selector(init));
    return objc_autoreleaseReturnValue(obj);
}

objc_autoreleaseReturnValue和objc_autorelease方法不同,该方法不仅仅是加入自动释放池,如果后面跟着 objc_retainAutoreasedReturnValue,那么就不将返回的对象放到自动释放池,而是直接传递给方法或者函数的调用方。而objc_retainAutoreasedReturnValue 方法与objc_retain方法不同,即使对象没被注册到自动释放池中,也可以获取到对象。通过这两个函数,不将对象注册到自动释放池中,而是直接传递,这一过程达到了最优化。

weak和assign的区别是什么?是怎么把对象置为nil的。

weak一般用来修饰oc对象,而assign用来修饰基本数据。因为用weak修饰的对象被释放时,会自动置为nil,而assign不会,易造成野指针,程序崩溃。

和上面的引用计数表一样,weak对象也是通过散列表(weak表)来实现的。weak所指向的对象的地址作为key,weak对象放在value数组中。当weak所指向的对象被释放时,就会以此对象地址为key,从weak表中找到value数组里面的所有weak对象,将其置为nil。

可变的数组、字典等对象用copy修饰会发生什么?

可变的数组、字典等对象用copy修饰后会被copy出来一份不可变的对象。

一般如果对象分可变和不可变,那么可变对象都用strong来修饰,不可变的对象用copy来修饰。