循环引用如何查(自动引用计数与循环引用)

本文首发于公众号【程序员华仔】

------------------------

本文主要讲解 自动引用计数循环引用 这两个大问题。

对于自动引用计数,没有什么争议。

而对于循环引用,这里主要是讲Object-C语言下的循环引用, 因为据我了解,Swift语言下也有循环引用。这两者根本原因是一致的,但解决方法有很大的差异。 所以这里特别说明是Object-C语言下的循环引用。对于Swift下的循环引用,以后再讲解。

自动引用计数

概念

说自动引用计数之前,先说下引用计数,引用计数是苹果公司设计出来,用来跟踪和管理App的内存情况的一套机制。它的运行机制大概是,当创建一个类对象,引用计数就为1,retain一次,计数 1,release一次,计数-1, 当计数减为0,系统就释放该对象,内存也回收,实现了对App的内存管理。

引用计数又分为:手动引用计数(Manual Reference Count)自动引用计数(Auto Reference Count),前者简称MRC,后者为ARC

MRC在iOS开发前期使用。主要由开发人员来管理引用计数。即要用的时候,retain一次,要释放的时候release一次。直到引用计数为零,系统才会释放对象,回收内存。

ARC是后来才推出的内存管理机制,它简化了流程。引用计数不需要开发人员来管理和维护了,全由系统帮助完成。 简单的来说,ARC无须开发人员考虑内存的管理情况,它会在类对象被引用的时候,引用计数 1;在release的时候,引用计数-1;在类对象不再使用的时候,自动释放其占用的内存。让开发人员从复杂的内存管理中解脱出来,大大地提高了开发人员的效率。

显然地,ARC更好用。那这么好用的ARC,我们再深入讲下它的工作机制。

自动引用计数的工作机制

正如前面所说一样,每当创建一个新的类对象时,ARC就会分配一块内存来储存该对象的信息。内存中会包含对象的类型信息,同时引用计数器会 1 。

当其他对象引用这个类对象时,计数再 1,若一直引用,那引用计数就一直累加。

当引用的类对象设置为nil,内部实现就是调用release一次,引用计数就会-1。多个类对象设置为nil, 就会减少多个计数值。

当对象不再使用了,ARC 就会释放对象所占用的内存,并让释放的内存能挪作他用。这确保了不再被使用的对象,不会一直占用内存空间。当然它的判断条件就是引用计数是否为零。为零就释放内存。

当 ARC 释放了类对象,类对象所对应的方法和属性将不能再被调用。否则App就会崩溃。

自动引用计数内部实现机制

在引用计数的底层实现中,是通过维护一个引用计数表来跟踪和计算每一个对象的引用情况。其中表里的key为内存块地址的散列值,value为该对象的引用计数。

每引用一次,value的值就 1, 同样的release一次,value值就-1,只有value值为0,才会销毁该对象并从引用计数表移除该key和value。

上述的对象引用过程,其实就是对对象的强引用。之所以称之为“强”引用,是因为它会将对象牢牢地保持住,只要强引用还在,对象是不允许被销毁的。

在实际开发过程中,声明一个类对象,默认就是强引用。

Person *person1;

该声明等价于

__strong Person *person1;

自动引用计数的实践

下面的例子展示ARC的工作机制。

1.创建一个Person类,定义name属性。在init和dealloc函数上分别打印init和dealloc的日志。

#import <Foundation/Foundation.h>

@interface Person : NSObject {

}

@property(nonatomic,retain) NSString *name;

@end

@implementation Person

- (id)init {

self = [super init];

NSLog(@"Person is being init");

return self;

}

- (void)dealloc {

NSLog(@"Person is being dealloc");

}

@end

2.然后在ViewdidLoad函数中定义 三个对象

Person *person1;

Person *person2;

Person *person3;

由于三个对象没有赋值,所以为空的。

这三个对象是强引用关系,因为它们等价于

__strong Person *person1;

__strong Person *person2;

__strong Person *person3;

3.接着创建Person对象并赋值给person1。

person1 = [[Person alloc] init];

通过这一段代码创建了一个Person对象,并把对象赋值给person1。也就是说person1强引用了Person对象。

在引用计数表中,就实现了 1操作。

4.接着在把person1赋值给person2,person3。实现person2,person3的强引用。

person2 = person1;

person3 = person1;

注:在实际开发中不会这么分解写代码。这里只是方便解说。正常的写法是2,3,4一起完成,如下代码所示:

Person *person1 = [[Person alloc] init];

Person *person2 = person1;

Person *person3 = person1;

5.对person1和person2对象进行释放操作。

person1 = nil;

person2 = nil;

通过这两行代码操作,Person对象内存释放吗? 显然没有。因为person3还持有该引用。

6.最后,person3设置为nil, 引用计数为0,ARC 才会销毁Person对象,回收内存。

以上就是自动引用计数的使用过程。

类对象间的循环引用

在上面的例子中,ARC 会跟踪新创建的Person对象的引用计数,并且会在 Person对象不再被需要时销毁它。

然而,我们可能会写出一个类对象的引用计数永远不能为0 的代码。即如果两个对象互相持有对方,每个对象都让对方一直存在,这种情况就是所谓的循环引用。

下面以具体类来说明下。

1.新建两个类,Person和Car

Class Car;

@interface Person : NSObject {

}

@property(nonatomic,retain) NSString *name;

@property(nonatomic,retain) Car *car;

@end

Class Person;

@interface Car : NSObject {

}

@property(nonatomic,retain) NSString *color;

@property(nonatomic,retain) Person *person;

@end

上述Person类中有两个属性,name表示“Person”对应的姓名,car表示“Person”对应的一辆车。假设这“Person”对象只有姓名和车。

同样地,Car对象下,color和person属性,对应的就是车的款式(以颜色代替)和车主。

2.创建类对应的实例并给属性赋值。

Person * john = [[Person alloc] init];

Car * johnCar = [[Car alloc] init];

john.name = “John”;

john.car = johnCar;

johnCar.color = “red”;

johnCar.person = john;

以上代码,在两个对象被创建和赋值后,就表现了强引用的关系。对象john现在有一个指向Car对象的强引用(johnCar),而变对象johnCar 有一个指向 Person 对象的强引用(car)。如下图。

循环引用如何查(自动引用计数与循环引用)(1)

同样地,这种强引用关系就包含了循环引用。即john对象持有了johnCar,johnCar也持有了john。 如下图。

循环引用如何查(自动引用计数与循环引用)(2)

当我们要释放john 和johnCar时,引用计数并不会降为 0,对象也不会被 ARC 销毁,这样就导致了内存泄露。

john = nil;

johnCar = nil;

未释放对象。

解决类对象的循环引用

针对上面的问题,在OC中,主要使用弱引用来解决循环引用问题。

弱引用

弱引用不会对其引用的对象进行强引用,因而不会阻止 ARC 销毁被引用的对象。

这个特性阻止了引用变为循环引用。我们只要在声明属性或者变量时,在前面加上 weak 关键字,就表明这是一个弱引用。

在底层的实现上,系统维护着一个弱引用表,每一次声明弱应用变量或属性都会在这张表中登记, 当对弱引用变量赋值时,就在这张表中建立起弱引用与对象之间的关系。有多少次赋值,就会建立多少次弱引用关系。

由于弱引用不会持有所引用的对象,即使引用存在,对象也有可能被销毁。因此,ARC 会在引用的对象被销毁后自动将其弱引用赋值为 nil,这个操作是为了对象的安全。

这样,对于上述的john和johnCar的循环引用问题,我们只要一方使用弱引用,就可以解除循环引用的问题。如下图所示:

循环引用如何查(自动引用计数与循环引用)(3)

在person释放的时候,可以不用等待Car的持有关系,因为它是弱引用。这种方式就解决了循环引用的问题,避免了内存泄露。

最后说明下:

关于循环引用有:1.类对象的循环引用;2.委托的循环引用;3.block的循环引用三个场景。

这次主要介绍类对象的循环引用和解决方法。

对于委托的循环引用,根本原因和类对象一样的(相互持有),解决方法就是在声明处使用weak关键字就可以了。

对于block的循环引用,内容比较多,见我的另一文章【探究Block底层原理(三)】

,

免责声明:本文仅代表文章作者的个人观点,与本站无关。其原创性、真实性以及文中陈述文字和内容未经本站证实,对本文以及其中全部或者部分内容文字的真实性、完整性和原创性本站不作任何保证或承诺,请读者仅作参考,并自行核实相关内容。文章投诉邮箱:anhduc.ph@yahoo.com

    分享
    投诉
    首页