Objective-C提供了两种内存管理机制MRC(Mannul Reference Counting)和ARC(Automatic Reference Counting),为Objective-C提供了内存的手动和自动管理。下面我们来探讨一下MRC和ARC的实现。
在iOS内存管理中,有四个这样的关键字:new、alloc、copy、mutableCopy,如果自身使用这些关键字的时候来产生对象,那么创建完之后,自身也就有了对象。
// 使用了alloc分配了内存,obj指向了对象,该对象本身引用计数为1,不需要retain
id obj = [[NSObject alloc] init];
// 使用了new分配了内存,objc指向了对象,该对象本身引用计数为1,不需要retain
id obj = [NSObject new];
// NSMutableArray通过类方法array产生了对象(并没有使用alloc、new、copy、mutableCopt来产生对象),因此该对象不属于obj自身产生的
// 因此,需要使用retain方法让对象计数器+1,从而obj可以持有该对象(尽管该对象不是他产生的)
id obj = [NSMutableArray array];
[obj retain];
id obj = [[NSObject alloc] init];
//释放对象
//指向对象的指针仍然被保留在变量obj中,看似可访问,但对象一经释放绝对不可访问
[obj release];
id obj = [NSMutableArray array];
// 释放一个不属于自己的对象
// obj没有进行retain操作而进行release操作,然后autoreleasePool也会对其进行一次release操作,导致奔溃。
[obj release];
基本思想:通过手动引用计数来进行对象的内存管理
autorelease即“自动释放”,是OC的一种内存自动回收机制,可以将一些临时变量通过自动释放池来回收统一释放。自动释放池销毁的时候,池子里面所有的对象都会做一次release操作
那么,autorelease释放与简单的release释放有什么区别呢?
调用 autorelease 方法,就会把该对象放到离自己最近的自动释放池中(栈顶的释放池,多重自动释放池嵌套是以栈的形式存取的),即:使对象的持有权转移给了自动释放池(即注册到了自动释放池中),调用方拿到了对象,但这个对象还不被调用方所持有。当自动释放池销毁时,其中的所有的对象都会调用一次release操作。
本质上,区别在于autorelease 方法不会改变调用者的引用计数,它只是改变了对象释放时机,不再让程序员负责释放这个对象,而是交给自动释放池去处理 。
autorelease 方法相当于把调用者注册到 autoreleasepool 中,ARC环境下不能显式地调用 autorelease 方法和显式地创建 NSAutoreleasePool 对象,但可以使用@autoreleasepool { }块代替(并不代表块中所有内容都被注册到了自动释放池中)。
对于所有调用过autorelease实例方法的对象,在废弃NSAutoreleasePool对象时,都将调用release实例方法。
eg:
int main(int argc, const char * argv[]) {
NSAutoreleasePool *pool = [[NSAutoreleasePool alloc]init];
id obj = [[NSObject alloc]init];
NSLog(@"%lu",(unsigned long)[obj retainCount]);
[obj autorelease];
NSLog(@"%lu",(unsigned long)[obj retainCount]);
[pool drain];
NSLog(@"%lu",(unsigned long)[obj retainCount]);
return 0;
}
我们在MRC中,有时可能会想要打印引用计数,但retainCount方法并不是很有用,由于对象可能会处于自动释放池中,这会导致打印的引用计数并不精准,而且其他程序库也很有可能自行保留或释放对象,这都会扰乱引用计数的具体值。
同时,retainCount也存在过大的问题。看看demo:
int main(int argc, const char * argv[]) {
NSString *firstString = @"你好";
NSString *secondString = [NSString stringWithFormat:@"hello"];
NSNumber *num1 = @2;
NSNumber *num2 = @100;
NSLog(@"%lu,%lu,%lu,%lu",(long)[firstString retainCount],(long)[secondString retainCount],(long)[num1 retainCount],(long)[num2 retainCount]);
return 0;
}
系统会尽可能把NSString实现成单例对象。如果字符串像例子上的那样,是一个编译期常量,那么就可以这样来实现了。在这种情况下,编译器会把NSString对象所表示的数据放在应用程序的二进制文件里,这样的话,运行程序时就可以直接用了,无需再创建NSString对象。
NSNumber也类似,它使用了一种叫做“标签指针”的概念来标注指定类型的数值。这种做法不使用NSNumber对象,而是把与数值有关的全部消息都放在指针值里。运行期系统会在消息派发期间检测到这种标签指针,并对它执行相应操作,使其行为看上去和真正的NSNumber对象一样。
对于上述所说的单例对象,其保留计数绝对不会改变。这种对象的release和retain操作都是“空操作”。
iOS内存管理方案有:
Tagged Pointer
:专门用来处理小对象,例如NSNumber、NSDate、小NSString等NONPOINTER_ISA
:非指针类型的isa,主要是用来优化64位地址。在 64 位架构下,isa 指针是占 64 比特位的,实际上只有 30 多位就已经够用了,为了提高利用率,剩余的比特位存储了内存管理的相关数据内容。nonpointer
: 表示是否对 isa 指针开启指针优化SideTables
:散列表,在散列表中主要有两个表,分别是引用计数表、弱引用表。通过 SideTables()结构来实现的,SideTables()结构下,有很多 SideTable 的数据结构。 而 sideTable 当中包含了自旋锁,引用计数表,弱引用表。 SideTables()实际上是一个哈希表,通过对象的地址来计算该对象的引用计数在哪个 sideTable 中。当ARC有效时,id类型和对象类型必须附加所有权修饰符,一共有如下四种。
__strong修饰符是id类型和对象类型默认的所有权修饰符。
id obj = [[NSObject alloc] init];
//在没有明确指定所有权修饰符时,默认为__strong
id __strong obj = [[NSObject alloc] init];
不论调用哪种方法,强引用修饰的变量会持有该对象,如果已经持有则引用计数不会增加。
__strong修饰符表示对对象的强引用。持有强引用的变量在超出其作用域时被废弃,随着强引用的失效,引用的对象会随之释放。
__strong修饰符的变量不仅只在变量作用域中,在赋值上也能够正确的管理其对象的所有者。
id __strong obj0 = [[NSObject alloc] init];//生成对象A
id __strong obj1 = [[NSObject alloc] init];//生成对象B
id __strong obj2 = nil;
obj0 = obj1;//obj0强引用对象B;而对象A不再被ojb0引用,被废弃
obj2 = obj0;//obj2强引用对象B(现在obj0,ojb1,obj2都强引用对象B)
obj1 = nil;//obj1不再强引用对象B
obj0 = nil;//obj0不再强引用对象B
obj2 = nil;//obj2不再强引用对象B,不再有任何强引用引用对象B,对象B被废弃
Test类中有一个强引用类型的成员变量obj_,设置其set方法。
@interface Test : NSObject {
id __strong obj_;
}
- (void)setObject:(id __strong)obj;
@end
#import "Test.h"
@implementation Test
- (id)init {
self = [super init];
return self;
}
- (void)setObject:(id __strong)obj {
obj_ = obj;
}
@end
声明两个对象test0和test1,他们内部也有自己的成员变量obj_。
int main(int argc, const char * argv[]) {
@autoreleasepool {
id test0 = [[Test alloc] init];//生成TestA
id test1 = [[Test alloc] init];//生成TestB
[test0 setObject:test1];
[test1 setObject:test0];
}
return 0;
}
如果通过set方法给两个对象的成员变量分别赋值另一个对象所持有的TestA/TestB对象,就会造成如下结果:
test0 持有 TestA,test0.obj 持有 TestB,test1 持有 TestB,test1.obj 持有 TestA。
由于两个对象互相强引用导致内存泄露,内存泄漏就是应当废弃的对象在超出其生命周期后继续存在。
如何解决这个问题呢?接下来看__weak修饰符。
弱引用表示并不持有对象,当所引用的对象销毁了,这个变量就自动设为nil。
可以利用__weak修饰符来解决循环引用问题。
@interface Test : NSObject {
id __weak obj_;
}
- (void)setObject:(id __strong)obj;
@end
@implementation Test
- (id)init {
self = [super init];
return self;
}
- (void)setObject:(id __strong)obj {
obj_ = obj;
}
@end
使用__weak之后,test0持有对象A,test1持有对象B,而test0的object和test1的object并不持有对象A和对象B,超出作用域后,就没有变量再持有对象A和对象B,对象A和对象B就会被释放,就解决了循环引用的问题。
__unsafe_unretained
和__weak
很像,唯一区别就是,__unsafe_unretained
变量引用的对象再被销毁以后,不会被自动设置为nil,仍然指向对象销毁前的内存地址。所以它的名字叫做unsafe,此时你再尝试通过变量访问这个对象的属性或方法就会crash。一旦对象释放,则会成为悬垂指针,程序崩溃,因此__unnsafe_unretained
修饰符的变量一定要在赋值的对象存在的情况下使用。
ARC无效时使用autorelease,在ARC下__autoreleasing的使用:
@autoreleasepool {
id __autoreleasing obj = [[NSObject alloc] init];
}
@autoreleasepool
块即相当于上文的NSAutoreleasePool
类生成、持有及废弃。
附有__autoreleasing
修饰符相当于变量调用了autorelease
方法。
以下为使用__weak
修饰符的例子,虽然__weak
修饰符是为了避免循环引用而使用的,但在访问附有__weak
修饰符的变量时,实际上必定要访问注册到AutoreleasePool
的对象。
id __weak obj_= obj;
NSLog(@"%@", [obj_ class]);
//等价于:
id __weak obj_ = obj;
id __autoreleasing tmp = obj_;
NSLog(@"%@", [tmp class]);
不能使用retain/release/retainCount/autorelease
不能使用NSAllocateObject/NSDeallocateObject
必须遵守内存管理的方法名规则
alloc/new/copy/mutableCopy,以上述名称开始的方法在返回对象时,必须返回给调用方所应当持有的对象。这在ARC下,依然没有变。
ARC下追加了一条命名规则:
init,以init开头的方法,必须是实例方法,并且返回对象,类型应为id或该类的的对象类型,抑或是超类或子类型。该方法基本上只是对alloc方法返回的对象进行初始化操作并返回。
注:initialize方法并不包含在上述命名规则里。
不要显式调用dealloc
多数情况下在dealloc中删除已注册的代理或观察者。不用书写[super dealloc],因为ARC已经自动处理了。
使用@autorelease块代替NSAutoreleasePool
不能使用区域(NSZone)
对象型变量不能作为C语言结构体的成员
因为ARC把内存管理的工作分配给了编译器,所以编译器必须能够知道并管理对象的生命周期。例如C语言的自动变量(局部变量)可以使用该变量的作用域来管理。但是对于结构体成员来说,是无法实现的。
显式转换id和void*
id obj = [[NSObject alloc] init];
void *p = (__bridge void *)obj;
ARC 的工作原理大致是这样:当我们编译源码的时候,编译器会分析源码中每个对象的生命周期,然后基于这些对象的生命周期,在合适的地方添加相应的引用计数操作代码retain
, release
和autorelease
。
具体一点就是, 前端编译器会为“拥有的”每一个对象插入相应的release
语句。如果对象的所有权修饰符是__strong
,那么它就是被拥有的。如果在某个方法内创建了一个对象(局部变量),前端编译器会在方法末尾自动插入release
语句以销毁它。而类拥有的对象(实例变量/属性)会在dealloc
方法内被释放。事实上,你并不需要写dealloc
方法或调用父类的dealloc
方法,ARC会自动帮你完成一切。此外,由编译器生成的代码甚至会比你自己写的release
语句的性能还要好,因为编辑器可以作出一些假设。在ARC中,没有类可以覆盖release
方法,也没有调用它的必要。ARC会通过直接使用objc_release
来优化调用过程。而对于retain
也是同样的方法。ARC会调用objc_retain
来取代保留消息。
ARC 是工作在编译期的一种技术方案,这样的好处是:
有时候需要。
因为ARC不会自动处理malloc/free、Core Foundation对象的生命周期管理、文件描述符等等,所以你仍然可以通过编写dealloc方法来释放这些资源。
你不必(实际上不能)释放实例变量,但可能需要对系统类和其他未使用ARC编写的代码调用[self setDelegate:nil]
。
ARC下的dealloc方法中不需要且不允许调用[super dealloc]
,Runtime会自动处理。
尝试不要去思考ARC将retains/releases调用的代码放在哪里,而是思考应用程序算法,思考对象的strong和weak指针、所有权、以及可能产生的循环引用。
是的,ARC自动retain/release,也继承了循环引用问题。幸运的是,迁移到ARC的代码很少开始泄漏,因为属性已经声明是否retain。