effective-Objective-C 第五章学习笔记

内存管理

文章目录

理解引用计数

其实引用计数的内容主要是这一张图片的内容

主要有下面三个方法用来操作计数器

  • Retain 递增保留计数
  • release 递减保留计数
  • autorelease 稍后清理自动释放池子,在递减保持计数

对象创建出来的时候,他保留计数至少要为1,若想让他继续存活,则要调用retain方法。下面我们来看一个例子,来理解这个概念:

objc 复制代码
NSMutableArray *array = [[NSMutableArray alloc] initl;
NSNumber *number = [ [NSNumber alloc] initWithInt:1337];
[array addobject: number] ;
[number releasel;
[array release];

如前所述,由于代码中直接调用了release 方法,所以在ARC 下无法编译。在Objective-C中,调用alloc 方法所返回的对象由调用者所拥有。也就是说,调用者已通过alloc 方法表达了想令该对象继续存活下去的意愿。不过请注意,这并不是说对象此时的保留计数必定是1。在 alloc或"initWithint:"方法的实现代码中,也许还有其他对象也保留了此对象,所以,其保留计数可能会大于1。能够肯定的是:保留计数至少为1。保留计数这个概念就应该这样来理解才对。绝不应该说保留计数一定是某个值,只能说你所执行的操作是

递增了该计数还是递减了该计数。创建完数组后,把 number 对象加入其中。调用数组的"addObject:" 方法时,数组也会在 number 上调用 retain 方法,以期继续保留此对象。这时,保留计数至少为2。接下来,代码不再需要 number 对象了,于是将其释放。现在的保留计数至少为1。这样就不能照常使用number 变量了。调用 release 之后,已经无法保证所指的对象仍然存活。当然,根据本例中的代码,我们显然知道 number 对象在调用了release之后仍然存活,因为数组还在引用着它。

其实这段话的意思是我们理解引用计数不是简单的理解为1,而是理解长持有一个数,某些操作增加计数或者是减少计数

属性存取方法的内存管理

如果是strong关系。

objc 复制代码
 [obj retain];
 [_obj release];
 _obj = obj

那么这个方法会先保留新值并释放旧值,然后更新实例变量,让他指向新值。假如还未保留新值在释放旧值的时候,两个值有指向同一个对象,先执行的release 可能会导致此对象永远回收,这个实例变量就变成了一个悬挂指针。所以这个县retain再release的顺序很重要。

自动释放池

在 Objective-C 的引用计数架构中,自动释放池是一项重要特性。调用 release会立刻递减对象的保留计数(而且还有可能令系统回收此对象),然而有时候可以不调用它,改为调用autorelease,此方法会在稍后递减计数,通常是在下一次"事件循环"(event loop)时递减,不过也可能执行得更早些。

这里我们来看一个方法:

objc 复制代码
- (NSString*) stringValue {
	NSString *str = [ [NSString alloc] initWithFormat:@"I am this: %@", selfl;
	return str;
}

这里的保留计数会比原先多1,我们要讲多出来的一次保留操作给抵消掉,不过不能再方法内释放str,否则还没等方法返回,系统就把这个对象回收了,这里我们可以采用autorelease,这样我们就可以正常使用这个函数了。这个方式可以保证对象在跨越方法调用别界后一定存活。

要点

  • 引用计数机制通过可以递增递减的计数器来管理内存,对象创建好之后,其保留计数至少为1,若保留计数正,则对象继续存活,保留计数降到0以后,对象就被销毁了。
  • 在对象生命周期中,其余对象通过引用来保留或释放此对象,保留操作会递增计数,释放会递减计数

以ARC简化引用计数

在有ARC之后,程序猿其实就没必要担心内存管理问题了。使用ARC来进行编程可以省去很多代码。

ARC会自动执行我们的retain,release方法,所以不能在ARC环境下直接调用这些内存管理方法

使用ARC时必须遵循的方法命名规则

  • alloc
  • new
  • copy
  • mutableCopy

如果方法名不易上述这四个词语开头,则表示其所返回的对象并不归调用者所有

归调用者所有的意思是:调用上述四种方法的那段代码要负责释放方法所返回的对象。也就是说,这些对象的保留计数是正值,而调用了这四种方法的那段代码要将其中一次保留操作抵消掉。如果还有其他对象保留此对象,并对其调用了 autorelease,那么保留计数的值可能比1大,这也是 retainCount 方法不太有用的原因之一(参见第36条)。

来看一下下面这两个方法:

objc 复制代码
+ (ECPerson*) newPerson {
EOCPerson *person = [ [EOCPerson alloc] init];
return person;
/ **
* The method name begins with 'new', and since 'person'
* already has an unbalanced +1 retain count from the
* 'alloc', no retains, releases, or autoreleases are
* required when returning.
* /
}
+ (EOCPerson*) somePerson {
EOCPerson *person = [ [EOCPerson alloc] init];
return person;
/**
* The method name does not begin with one of the "owning"
* prefixes, therefore ARC will add an autorelease when
* returning 'person'.
* The equivalent manual reference counting statement is:
* return [person autoreleasel;
* /

前面的一个方法是需要我们自己在使用之后手动release的,而后者则是ARC自己进行一个管理的,这就是两者的一个区别。

同时ARC会进行一个优化,ARC能够讲互相抵消的retain,release,autorelease操作约简。

比方说下面这两段代码:

objc 复制代码
myPerson = [Person personWithName:@"123"];

//这里personWithName方法中其实在返回对象之前调用了一次autorelease,
//所以我们还要手动retain一下
tmp = [Person personWithName:@"123"];
myPerson = [tmp retain]

从这两段代码我们可以看出来一个点,就是autorelease和retain是一个重复的内容。

这里的优化实现是在那个部分呢,也就是我们在函数遇到我们的一个autorelease的时候,他会执行一个objc_autoreleaseReturnValue他会设置一个全局的数据结构设置一个标志位置,而不执行autorelease,如果执行retain,那么就是这个objc_retainAutoreleaseReturnValue来检测全局数据节后的标志位置,如果经过置位,那么就不进行一个retain操作

变量的内存管理语义

ARC会处理局部变量和实例变量的内存管理,默认情况下每一个变量都是指向对象的一个强引用,比方说这段代码:

objc 复制代码
@implementation Person {
    NSMutableSet* internalSet;
}

在MRC中这个set不会自动保留他的值,而在ARC中则会自动保留这个值

这里我们看一个函数

objc 复制代码
- (void) setup (
	id tmp = [EOCOtherClass new];
	_object = [tmp retain];
	[tmp release];
}

这里的一个retain和release其实是可以互相消去的,所以ARC自然会简化这两个操作,我们在设置set方法的时候:

objc 复制代码
- (void) setObject: (id) object {
	[_object release];
	_object = [object retain];
}

这种写法如果两个值相同的话,会出问题,也就是会出现一个已经被释放的object被retain,所以会出问题,但是如果我们呢采用ARC就不会出这种问题,因为ARC会先保留新值,在释放旧值,最后设置实例变量。

  • __strong:默认语义
  • __unsafe_unretained:不保留这个值,这个做法并不安全,下次访问可能被回收了
  • __weak:不保留这个值,但是变量可以安全使用
  • __autorelease:把对象按引用传递给方法时,可以用这个修饰符,这个值在方法返回的时候自动释放

不管采用__ weak 或者 __ unsafe_unretained这俩个修饰符的时候,在设置实例变量的时候都不会保留其值,只有加了. __weak修饰符的weak引用才会自动清理

ARC清理实例变量

一般情况不需要我们在编写dealloc,除了CoreFoundation这个类的独享需要我们自己手动释放,否则一般不需要我们去编写dealloc这个函数里的内容。

覆写内存管理方法

不使用ARC的时候,可以覆写内存管理方法,比方说实现单例类的时候,单例不可释放,所以我们经常覆写release方法。

要点

  • 有ARC之后,程序员就无须担心内存管理问题了。使用ARC来编程,可省去类中的
    许多"样板代码"。
  • ARC管理对象生命期的办法基本上就是:在合适的地方插入"保留"及"释放"操作。在ARC环境下,变量的内存管理语义可以通过修饰符指明,而原来则需要手工执行"保留"及"释放"操作。
  • 由方法所返回的对象,其内存管理语义总是通过方法名来体现。ARC将此确定开发者必须遵守的规则。
  • ARC 只负责管理 Objective-C对象的内存。尤其要注意:CoreFoundation 对象不归ARC 管理,开发者必须适时调用 CFRetain/CFRelease。

在dealloc方法中只释放引用并解除监听

我们在dealloc中主要是释放对象所持有的引用,也就是把OC对象都释放掉。在dealloc中,通常还要做一件事情,那就是把原来的配置过的观测行为全都清理掉,就好比通知中心注销也在dealloc中完成。

objc 复制代码
- (void)dealloc {
    [[NSNotificationCenter defaultCenter] removeObserver:self];
}

就像上面的这样,在dealloc中移除一下观察者。

虽说应该在dealloc中释放引用,但是开销较大或系统内稀缺的资源则不再列。我们不可以通过dealloc调用来释放资源,这可以会让某些不该释放的资源释放了,正确的做法应该是在应用程序用完资源后,就调用这个方法,这样的话,资源对象的生命期就更加明确了

objc 复制代码
#import <Foundation/Foundation.h>
@interface EOCServerConnection : NSObject
- (void) open: (NSString*) address;
- (void) close;
@end

在open的时候获取资源,在close的时候释放资源。

这里我们有些时候会忘记释放资源,有些时候会忘记调用close这个方法,这样可能会导致资源没有正常释放,我们可以在dealloc中做一个保证。

objc 复制代码
- (void) close {
/* clean up resources */
		_closed = YES;
}
(void) dealloc {
	if (!_closed) {
		NSLog (@"ERROR: close was not called before dealloc!");
		[self close];
	}
}

这样操作可以防止内存泄漏。

dealloc方法中尽量不要调用其他方法,调用 dealloc 方法的那个线程会执行"最终的释放操作"(finalrelease),令对象的保留计数降为0,而某些方法必须在特定的线程里(比如主线程里)调用才行。若在 dealloc 里调用了那些方法,则无法保证当前这个线程就是那些方法所需的线程。通过编写常规代码的方式,无论如何都没办法保证其会安全运行在正确的线程上,因为对象处于"正在回收的状态"(deallocating state),为了指明此状况,运行期系统已经改动了对象内部的数据结构。

要点

  • 在 dealloc 方法里,应该做的事情就是释放指向其他对象的引用,并取消原来订阅的"键值观测"(KVO)或 NSNotificationCenter 等通知,不要做其他事情。
  • 如果对象持有文件描述符等系统资源,那么应该专门编写一个方法来释放此种资源。这样的类要和其使用者约定:用完资源后必须调用 close 方法。
  • 执行异步任务的方法不应在dealloc 里调用;只能在正常状态下执行的那些方法也不应在 dealloc 里调用,因为此时对象已处于正在回收的状态了。

编写异常安全代码时留意内存管理问题

首先我们认识一下什么是异常:

@try

用于包裹可能会抛出异常的代码块。如果该代码块运行时出现异常,就会进入 @catch 块处理。

@catch

用于捕获 @try 代码块中抛出的异常。可以有多个 @catch 块,但只有第一个匹配的 @catch 块会被执行。

@finally

无论是否发生异常,@finally 块中的代码都会被执行。通常用于释放资源、清理内存等操作。

@throw

用于手动抛出异常。

在MRC中如何处理异常是比较麻烦的,这里我来看一段代码:

objc 复制代码
@try {
	EOCSomeClass *object = [[EOCSomeClass alloc] init];
	[object doSomethingThatMayThrow] ;
	[object release];
}
@catch (... ) {
	NSLog (@"Whoops, there was an error. Oh well...");
}

这样子其实会出现一个bug,倘如 doSomethingThatMayThrow这个方法出现报错的话,那么就不会指向release这个操作,从而导致内存泄漏的问题,所以应该把代码放在finally里面:

objc 复制代码
EOCSomeClass *object;
@try {
	object = [ [EOCSomeClass alloc] init];
	[object doSomethingThatMayThrow];
}
@catch (...) {
	NSLog (@"Whoops, there was an error. Oh well...");
}
@finally {
	[object release];
}

注意,由于 @fnally块也要引用 object对象,所以必须把它从 @try 块里移到外面去。要是所有对象都得如此释放,那这样做就会非常乏味。而且,假如@try 块中的逻辑更为复杂,含有多条语句,那么很容易就会因为忘记某个对象而导致泄漏。若泄漏的对象是文件描述符或数据库连接等稀缺资源(或是这些稀缺资源的管理者),则可能引发大问题,因为这将导致应用程序把所有系统资源都抓在自己手里而不及时释放。

在ARC环境下:

objc 复制代码
@try {
	EOCSomeClass *object = [ [EOCSomeClass alloc] initl;
	[object doSomethingThatMayThrow] ;
}
@catch (...) {
	NSLog (@"Whoops, there was an error. Oh well...");
}

现在问题更大了:由于不能调用 release,所以无法像手动管理引用计数时那样把释放操作移到@finally块中。你可能认这种状况 ARC自然会处理的。但实际上 ARC 不会自动处理,因为这样做需要加入大量样板代码,以便跟踪待清理的对象,从而在抛出异常时将其释放。可是,这段代码会严重影响运行期的性能,即便在不抛异常时也如此。而且,添加进来

的额外代码还会明显增加应用程序的大小。这些副作用都不甚理想。

要点

  • 捕获异常时,一定要注意将try块内所创立的对象清理干净。
  • 在默认情况下,ARC不生成安全处理异常所需的清理代码。开启编译器标志后,可生成这种代码,不过会导致应用程序变大,而且会降低运行效率。

以弱引用避免保留环

我们这里首先认识循环引用:

objc 复制代码
#import <Foundation/Foundation.h>
@class EOCClassA;
@class EOCClassB;
@interface EOCClassA : NSObject
@property (nonatomic, strong) EOCClassB *other;
@end
@interface EOCClassB : NSObject
@property (nonatomic, strong) EOCClassA *other;
@end

这一段代码我们很清楚的可以看到两个对象被循环引用了:

这样会出现一个循环引用的问题,这里我们看这个图:

保留环会导致内存泄漏。如果只剩一个引用还指向保留环中的实例,而现在又把这个引用移除,那么整个保留环就泄漏了。也就是说,没办法再访问其中的对象了。图5-5所示的保留环更复杂一些,其中有四个对象,只有 ObjectB 还为外界所引用,把仅有的这个引用移除之后,四者所占内存就泄漏了。

为了解决循环引用,最好的方法就是采用弱引用,我们把之前那段代码修改一下:

objc 复制代码
#import <Foundation/Foundation.h>
@class EOCClassA;
@class EOCClassB;
@interface EOCClassA : NSObject
@property (nonatomic, strong) EOCClassB *other;
@end
@interface EOCClassB : NSObject
@property (nonatomic, unsafe_unretained) EOCClassA *other;
@end

这样就解决了循环引用的问题,但是可能会出现一个悬垂指针的问题,这里我们看下面这个图

当指向 EOCClassA 实例的引用移除后,unsafe_ unretained 属性仍然指向那个已经回收的实例,而 weak 属性则指向 nil。

要点

  • 将某些引用设为weak,可避免出现"保留环"。
  • weak 引用可以自动清空,也可以不自动清空。自动清空(autonilling)是随着ARC而引入的新特性,由运行期系统来实现。在具备自动清空功能的弱引用上,可以随意读取其数据,因为这种引用不会指向已经回收过的对象。

以自动释放池快降低内存峰值

在 Objective-C的引用计数架构中,有一项特性叫做"自动释放池"(autorelease pool)。释放对象有两种方式:一种是调用 release 方法,使其保留计数立即递减;另一种是调用 autorelease 方法,将其加入"自动释放池"中。自动释放池用于存放那些需要在稍后某个时刻释放的对象。清空(drain)自动释放池时,系统会向其中的对象发送 release 消息。

objc 复制代码
int main(int argc, char * argv[]) {
    NSString * appDelegateClassName;
    @autoreleasepool {
        // Setup code that might create autoreleased objects goes here.
        appDelegateClassName = NSStringFromClass([AppDelegate class]);
    }
    return UIApplicationMain(argc, argv, nil, appDelegateClassName);
}

这个池可以被认为是最外围捕捉全部自动释放对象的所用的池。

但是我们可以定义多个自动释放池:

objc 复制代码
@autoreleasepool {
NSString *string = [NSString stringWithFormat:@"1 = %i", 1];
	@autoreleasepool {
		NSNumber *number = [NSNumber numberWithInt: 1];
	}
}

这样设置多个自动释放池的好处是,这样可以控制应用程序的一个内存峰值,我们现在看一个例子来理解一下这个内容:

objc 复制代码
NSArray *databaseRecords = /* ... */;
NSMutableArray *people = [NSMutableArray new];
for (NSDictionary *record in databaseRecords) (
	EOCPerson *person = [[EOCPerson alloc] initWithRecord: record];
	[people addObject :person];
}
  
NSArray *databaseRecords = /* ... */;
NSMutableArray *people = [NSMutableArray new];
for (NSDictionary *record in databaseRecords) {
	@autoreleasepool {
		EOCPerson *person = [ [EOCPerson alloc] initWithRecord:recordl;
		[people addobject:person];
	}
}

加上这个自动释放池之后,应用程序在执行循环的时候内存峰值就会降低,不会像之前那么高了。对于是否需要创建自动释放池,还是要看情况,自动释放池也是有一个开销的,只要内存1用量没有那么夸张就不需要优化。

要点

  • 自动释放池排布在栈中,对象收到 autorelease 消息后,系统将其放入最顶端的池里。
  • 合理运用自动释放池,可降低应用程序的内存峰值。
  • @autoreleasepool 这种新式写法能创建出更为轻便的自动释放池。

不要使用retainCount

然而 ARC已经将此方法废弃了。实际上,如果在 ARC 中调用,编译器就会报错,这和在ARC 中调用retain、release、autorelease 方法时的情况一样。虽然此方法已经正式废弃了,但还是经常有人误解它,其实这个方法根本就不应该调用。若在不启用 ARC 的环境下编程

(说真的,还是在 ARC下编程比较好),那么仍可调用此方法,而编译器不会报错。所以,还是必须讲清楚为何不应使用此方法。

我们来看下面这一段代码:

objc 复制代码
while ( [object retainCount]){
	[object release];
}

这段代码有两段问题,一个是如果这个对象在自动释放池里,那么稍后的系统池子中有要重新释放一次,会让程序崩溃

第二个就是可能retainCount永远不为0,因为有些时候系统会优化对象的一个释放行为,在保留计数还是1的时候就把它给回收了。

单例的保留计数非常的大,所以我们不应该依赖于保留计数的具体值来判断。

要点

  • 不要使用retainCount,在ARC环境下
  • retainCount并不可以反应对象的生命期的全貌。
相关推荐
LK_072 分钟前
【蓝桥杯—单片机】第十届省赛真题代码题解题笔记 | 省赛 | 真题 | 代码题 | 刷题 | 笔记
笔记·单片机·蓝桥杯
F——17 分钟前
云计算行业分析
运维·学习·云计算·边缘计算
程序员yt33 分钟前
‌双非硕士的抉择:自学嵌入式硬件开发还是深入Linux C/C++走软开?
c++·嵌入式硬件·学习·计算机网络
Sean_summer1 小时前
2.5学习
学习
AI大模型训练家2 小时前
SpringBoot+Dubbo+zookeeper 急速入门案例
java·开发语言·spring boot·学习·dubbo·java-zookeeper
孞㐑¥3 小时前
C++基础知识
开发语言·c++·经验分享·笔记
F——3 小时前
云计算——AWS Solutions Architect – Associate(saa)1、什么是云,AWS介绍
运维·学习·云计算·aws
小虚竹3 小时前
新星杯进化史:个人发起到CSDN官方支持,创作活动的新篇章
学习·大学生·创作活动·新星杯·大学生创作
s_little_monster3 小时前
【Linux】环境变量
linux·运维·服务器·经验分享·笔记·学习·学习方法
熬夜患者4 小时前
HTML学习笔记(6)
笔记·学习·html