EffectiveOC的学习-5

这里是EffectiveOC的 29 - 36条 ,内存管理

2016-07-05 | 阅读

29. 理解引用计数

虽然现在开发都是基于ARC进行开发的,但其原理还是基于MRC,依旧是引用计数.

Objective-C中使用引用计数来管理内存,也就是每个对象上有一个计数器,记录了引用次数,当这个对象没有被其他人引用时,即计数变为0时,表示没人关注这个对象,就会将这个对象销毁.

NSObject中有4个计数器相关的方法:

  • retain 增加一个引用
  • release 释放一个引用
  • autorelease 在自动释放池中释放一个引用.
  • retainCount : 查看一个对象当前的引用计数的数量

一般对象的引用与释放的流程大致如下图所示:

应用程序会创建多个对象,而对象互相联系,互相持有.对于对象互相持有的引用逻辑是, 当ObjectA持有ObjectB时, 会调用 [ObjectB retain]来表示 A持有了B. 当A对象不需要B对象时,会调用[ObjectB release表示 A 释放了B的引用. 如果当前B对象没有被其他人持有的话,将被释放.

引用是被互相持有的,所以会有一个根对象,在iOS中根对象是UIApplication,是在应用程序启动时创建的单例.

一些方法会自动执行retain的操作,如 alloc 或者init等方法,还有集合添加元素的方法,如NSMutableArrayaddObject这些方法都会附带retain的操作,以正确地持有对象.

调用release方法,只是释放了对象,内存被回收,但是内存不一定被覆盖,所以如果有一个野指针指向了这块地址,在这块地址被使用之前,这个对象还是保持之前的状态,是一个完整的有效的对象.

默认的set方法中,对内存的管理是这样操作的 :

- (void)setA:(id)A{
	[A retain];
	[_A release];
	_A = A;
}

先持有A对象,避免A被释放,然后再释放原来的_A对象,最后将指针_A指向当前的A对象. 这里的顺序是很重要的,一定要先retain A对象. 举例,如果要设置的对象与当前对象都指向了同一个对象, 那先进行release就会导致这个对象被释放,再调用retain就会报错.

自动释放池是引用计数中的一个重要特性. 使用autorelease表示稍后会释放引用,稍后指的是下一次事件循环的时候,而不是像release这样立刻释放. autorelease一般用于跨函数传递对象时使用, 如 :

- (NSString *)stringValue {
	NSString *str = [[NSString alloc] initWithString:@"HelloWorld"];
	return [str autorelease];
}

由于创建对象时,已经进行了一次retain操作,但函数调用者可能并不会持有这个对象,调用者需要在使用一次返回值后,返回的对象自行释放,所以我们用autorelease, 无需调用者再进行内存管理操作.

OC中没有使用垃圾回收机制,所以对于循环引用,只能自行注意了.

30. 以ARC简化引用计数

ARC会静态的分析代码,并为代码中添加retain , releaseautorelease操作. ARC自动处理内存管理,而我们需要做的是正确地使用引用,即对属性的__strong ,__weak__unsafe_unretained 这些描述的正确使用.

ARC在调用retain release这些方法时,是直接调用底层C语言的,所以相对于手动通过OC的消息转发机制调用,还是有一定的效率提升的. 而且需要注意的是,ARC是自动添加引用计数的代码,来实现功能的,ARC最终还是基于MRC来实现的,所以现代的ARC的自动管理,其性能其实在某些程度上还是优于手动管理的MRC的。

ARC中需要遵循的方法命名规则

对于 alloc,new ,copy,mutableCopy这些方法或者以这些单词开头的方法,返回的对象是归调用者所持有.相当于这些方法的实现中最后都调用了retain方法. 而一般不是以以上四个词语开头的方法,返回对象一般不归于调用者所有,返回对象一般调用了autorelease,会自动释放. 这个规则是ARC中必须遵守的,但由于使用时全部使用ARC,几乎可以不用在意. 但是,如果代码中是 ARC 和 MRC混编的,就一定要注意这条规则.

我们使用ARC的一个重要原因,是Apple在ARC中进行了许多优化,导致其效率比MRC更高. 如 在编译期,ARC会把能够互相抵消的retainreleaseautorelease操作移除,如

-(id)funA{
	id a = [[A alloc] init];
	return a;
}
// ...
A = [X funA];
_A = [X funA];

正常ARC下代码如此编写,而使用MRC时,我们可能会这么写 :

-(id)funA{
	id a = [[A alloc] init];
	return [a autorelease];
}
// ...
A = [X funA];// 局部变量,继续autorelease
_A = [[X funA] retain];// 持有属性,使用retain

ARC会智能的处理这两种情况,做到性能最优。

首先ARC的实现 不是去删除函数返回值处的autorelease操作(因为可能会在MRC的环境下调用这个函数)。ARC对于这种情况,通过两个函数来进行优化。

objc_autoreleaseReturnValue,这个方法会在调用之前去检测之后的代码, 如果这个方法之后立即在返回对象上调用retain操作,则不执行autorelease操作,同时设置一个标记位。

objc_retainAutoreleasedReturnValue,替换函数调用处的retain函数,执行时会检测一下刚才在objc_autoreleaseReturnValue中设置的标记位,如果检测到标记位,则不执行retain操作. 而这两个函数本身也针对不同的处理器进行了优化。

由于ARC对内存管理的优化,所以我们会在使用performSelector时,Xcode会抛出警告performSelector may cause a leak because its selector is unknown, 由于编译器不知道Selector的具体类型,不知道返回值是基础数据类型还是OC对象,所以无法正常添加retain或者release操作,就会造成一些内存问题。

ARC进行了一些复杂的优化,使我们可以不使用MRC的同时,获取比之前使用MRC更快的速度.

变量的内存管理语义

ARC中通过内存管理语义来处理变量的引用计数 ,有以下四种修饰符:

  • __strong : 默认情况,强引用
  • __unsafe_unretained: 不安全的弱引用,不持有对象,所以这里会出现野指针的情况
  • __weak : 安全的弱引用, 不持有对象,但是是安全,即当对象释放时,这个指针会自动清空.
  • __autoreleasing : 声明autoreleasing,避免ARC添加retain操作。

ARC中清理实例变量

ARC中在dealloc中插入一些清理代码,以释放强引用的实例变量.

而如果类中有一些非Objective-C的对象,如 CoreFoundation中得对象,或是手动通过malloc分配的内存,这种时候就需要在dealloc方法中进行清理了.

但是在dealloc方法中,不用调用父类的dealloc方法.ARC中不能直接调用dealloc方法,ARC会自动的调用父类的dealloc方法. 但是在MRC中,还是要调用[super dealloc]的;

覆盖内存管理方法

在MRC中,我们会覆写内存管理方法,如在实现单例时,我们可以覆盖release方法,导致对象无法释放.

但是在ARC中不要这样做,ARC对于这些方法都进行了优化.

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

dealloc中,通常要做的一件事是释放观察者,一般使用:

[[NSNotificationCenter defaultCenter] removeObserver:self];

释放该对象上所有的订阅.

dealloc的含义是对象释放,在中间添加一些对象释放时做的资源清理的工作, 但一些清理工作不能完全指望在dealloc中执行,因为dealloc方法在某些情况下,不一定会被调用. 如应用程序终止时,一些对象没有调用dealloc,但由于程序已经终止,即使不调用dealloc,这些对象也已经死亡了,再也不会有人去调用他们了.

所以一些资源释放的操作,应该抽离出一个单独的接口, 如一个网络套接字的连接类,应该有 :

- (void)open;
- (void)close;

这样单独的方法,open中建立套接字连接, close中断开连接释放资源. dealloc中可以判断当前释放在连接中,如果在连接中,就调用close来释放连接. 而不要把逻辑的控制完全依赖于dealloc函数.

尽量不要在dealloc中调用自己的方法,如果这里调用的方法中,有异步操作,那就会导致更多的问题了.一个对象在执行某些操作的同时,自身已经被释放, 这样做很容易导致程序崩溃.

dealloc中,也不应该调用属性的存取方法,可以的话直接处理属性. 因为属性的存取方法可能被其他人覆写了,如在KVO中. 这样会导致一些莫名其妙的错误.

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

在MRC中,编写异常安全代码 ,如 :

@try {
	NSObject *object = [[NSObject alloc] init];
	[object doSomething];
	[object release];
}
@catch (...) {

}

如果程序在运行[object doSomething]抛出异常,则会导致后面的[object release]无法被调用,从而造成内存泄露.所以我们改进这种写法,将内存释放的操作放在finally中执行:

NSObject *object;
@try {
	object = [[NSObject alloc] init];
	[object doSomething];
}
@catch (...) {

}
@finally {
	[object release];
}

这样内存也安全了,但是不方便的地方,必须确认在 try块中所有调用过retain方法的变量,并提前将变量在try块之前进行声明, 这样做还是很容易出错的.

而在ARC中,默认是不会进行自动的内存优化来解决这个问题,可以通过设置fobjc-arc-exceptions这个标记,来让ARC处理这种情况. 设置该标记后,ARC会加入大量代码以跟踪对象,从而在抛出异常时将对象正确释放. 由于iOS中建议异常只有严重异常,只用崩溃,所以不需要过于关注这些异常.

33. 以弱引用来避免保留环

由于OC中使用的是一个简单地引用计数形式去处理内存,所以引用环会导致对象无法释放,而在其他语言中通过垃圾回收机制可以解决引用环的释放.

一般在ARC中通过weak来引用对象,而不持有对象,在引用环的合适地方使用这种弱引用,就可以解决引用环的问题.

weakunsafe-unretained要安全,weak会在引用对象释放时置为nil,而在OC中对nil调用方法是安全的. 而unsafe-unretained会在对象释放后依然指向原来的地址,这就会出现野指针的问题,某些情况下会导致应用崩溃.

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

前面提到过autorelease,调用后会在某个时刻释放, 实际上调用autorelease 是将对象加入了一个autoreleasepool 自动释放池,这个池存放一些自动释放的对象,然后在drain 清空自动释放池时,系统会向其中的对象发送release消息.

调用autorelease方法必须在autoreleasepool中,否则会报错. iOS中基本上所有的线程都默认拥有自动释放池,如在主函数中:

int main(int argc, char * argv[]) {
    @autoreleasepool {
        return UIApplicationMain(argc, argv, nil, NSStringFromClass([AppDelegate class]));
    }
}

这个池是应用程序中,最外围捕捉全部自动释放对象的池. autoreleasepool可以互相嵌套的, 如 :

@autoreleasepool {
	NSString *string = [NSString stringWithString:@"hello"];
	@autoreleasepool {
		NSNumber *number = [NSNumber numberWithInt:1];
	}
}

由于stringWithStringnumberWithInt 不是以new alloc等名词开始的函数,所以这些函数只是普通的工厂方法,其返回的对象是autorelease的对象,放在不同层的自动释放池中.

使用自动释放池进行嵌套的目的是, 借此控制应用程序的内存峰值,使其不会过高,如 :

for (int i = 0 ; i < 100000; i++ ){
	[self doSomething];
}

如果这个循环中作一些操作,如创建了一些临时对象,这些临时对象会在这个循环完成后才会释放,则导致了程序的内存占有量会持续上升,所以,我们用autoreleasepool来解决这个问题 :

for (int i = 0 ; i < 100000; i++ ){
	@autoreleasepool {
		NSString *str = [NSString stringWithFormat:@"i = %@",@(i)];
		[self doSomething];
	}
}

这样,这个创建的临时对象str就会在每次循环完成时自动释放,而不是等整个循环完成时才释放对象.

但需要注意的是,创建autoreleasepool本身也是有一定消耗的,需要根据情况来使用.

35. 用 僵尸对象 来调试内存管理问题

向一个野指针发送消息,是一个不安全的操作,这样做有时不会出错,有时则会崩溃,是不可控的.Cocoa中提供了僵尸对象 Zombie Object这个功能, 启动这个选项后进行调试,运行期系统会将所有已经回收的实例转化为一个特殊的僵尸对象, 这个对象会在收到消息后抛出异常,并准确说明发送的消息,并描述之前的对象. 使用Zombie Object就可以让我们方便的找到野指针,并解决问题.

启动僵尸对象, 在Xcode - Edit Scheme - Run -Diagnostics中可以找到Enable Zombie Objects选项,选中后就可以启动该功能.

介绍一下该功能的实现原理,首先 在系统发现NSZombieEnable的环境变量后,就通过method swizzling将所有的NSObjectdealloc方法进行替换,替换后的dealloc执行以下操作:

// 获取一个zombie类名
Class cls = object_getClass(self);
const char *clsName = class_getName(cls);
const char *zombieClsName = "_NSZombie_" + clasName;

Class zombieCls = objc_lookUpClass(zombieClsName);
if(! zombieCls) {
	// 如果当前对应的zombie类不存在,则创建一个新的zombie类
	Class baseZombieCls =  objc_lookUpClass("_NSZombie_");
	zombieCls = objc_duplicateClass(baseZombieCls ,zombieClsName,0);

}
// 销毁对象,但是不释放内存,只清空一下与这个对象的关联.
objc_destructInstance(self);
// 将当前self 对象的isa指针指向 新的zombieClass
objc_setClass(self,zombieCls);

对于一个原来的类NSString,创建一个新的_NSZombie_NSString的类,来处理变成僵尸对象后消息的接受与报错提示. 调用objc_destructInstance后销毁了对象,但是没有释放内存,即内存泄露了,所以这个方法只用于调试.

_NSZombie_这个类也是跟NSObject一级的根类,_NSZombie_中没有实现任何方法,只有一个实例变量为isa. 然后在消息转发机制中,在forwarding中,处理消息时,首先判断是否是一个_NSZombie_的类,如果是,则做特殊处理,即打印出僵尸对象所收到的消息以及原来所属的类.

36. 不要使用retainCount

NSObject中定义了方法retainCount ,用来查询当前的保留计数.但在ARC中已经将该方法废弃,但是即使在MRC中,也不应该使用这个不靠谱的方法.

while([object retainCount]) {
	[object release];
}

上面这段代码有很多错误.首先,在引用计数中有autorelease, 对一个对象检测其retainCount 然后判断是否需要release,这显然是错误的,因为这个对象可能当前引用计数是1,但是带有autorelease,而如果通过这个方法判断,强行释放了对象,然后再autoreleasepool中又会重复释放一次对象,导致崩溃. 由于有autorelease,所以这个retainCount就不是一个准确的值了,autorelease决定了引用计数外来的值.

然后 ,retainCount可能永远不会返回0.系统有时会对对象释放过程进行优化, 即 在保留计数还是1的时候就将对象进行回收.

编译器常量的引用计数是不会变的, 如 :

NSString *str = @"Some String";
NSLog(@"%@",@([str retainCount]));

NSNumber *number1 = @1;
NSLog(@"%@",@([number1 retainCount]));

第一个输出的保留计数是 2的64次方 -1 ,而第二个输出的保留计数为 2的63次方 -1 , 一般使用字面量表示的字符串都是常量字符串,这些字符串表示的数据在编译器直接放在应用程序的二进制文件中,在应用运行期间,既不会创建对象,也不会释放对象. 所以这些常量的内存管理操作都是空操作,保留计数不会改变.