内存管理在Objective-C是一件简单又麻烦的事情。
简单是因为所谓的内存管理不过是两件事情:一块内存我们要用,还是不用,该用的时候用,不用的时候就释放。** 麻烦**的就是我们很容易疏忽而造成用与不用不匹配。不匹配,就造成两种结果:如果用完不释放内存,就会造成软件占用一堆没用到的内存,造成内存飙升,然后iOS上的系统会强行终止我们的应用程序,这种状况我们称为内存泄漏(Memory Leak)。
但是,如果一块内存已经被释放掉,我们还以为这个内存还存在,还是可以调用的对象,所以当我们尝试调用时候,会发现这个内存存在的对象已经不在了,这种状况就叫做over-release或者是invalid memory reference,会造成应用程序的crash,crash log 上会告诉你错误类型是EXC_BAD_ACCESS
。
Reference Count/Retain/Release
Objective-C 语言里面每一个对象,都是指向某块内存的指针,在C语言中,会使用像malloc
、calloc
这些函数来设置内存,用完之后调用free
。但是我们如何知道一块内存被多少地方用到,之后这些地方又用不到呢?所以Objective-C,有了计算有多少地方用到某个对象的简单机制,叫做reference count(引用计数),意思很简单:只要一个对象被用到某一个地方一次,这个地方就对这个对象+1,反之—1,如果数字减到0,那就是释放这块内存。
如果一个对象使用alloc
、init
········产生,初始的引用计数为1。接着可以使用retain增加引用计数。
[anObject retain]; // 加 1
反之就用release
:
[anObject release]; // 減 1
可以用retainCount
检查某个对象被retain 多少次:
NSLog(@"Retain count: %d", [anObject retainCount]);
以下的程序会出现哪些问题呢?
id a = [[NSObject alloc] init];
[a release];
[a release];
因为在第二行,a所指向的内存已经被释放掉了,所以第三行想要再释放一次会造成错误。同理:
id a = [[NSObject alloc] init];
id b = [[NSObject alloc] init];
b = a;
[a release];
[b release];
在第三行中,由于b指向了a原本所指向的内存,但是b原本所指向的内存却没有被释放掉,同时也没有任何的变量指向b原本所指向的内存,因此这块内存就会发生内存泄漏。接着,在第四行调用[a release]
时候,这块内存就已经被释放掉了,由于a与b都已经指向同一块内存,所以第五行的[b release]
也是操作同一块内存,于是就发生了EXC_BAD_ACCESS
的错误。
Auto-Release
如果我们今天有一个方法,会返回一个Objective-C对象,假如写成如下:
- (NSNumber *)one
{
return [[NSNumber alloc] initWithInt:1];
}
那么,每次用到由这个one产生出来的对象,用完之后,都需要记得release这个对象,很容易造成疏忽。所以,我们会让这个方法返回auto-release的对象。像是写成这样:
- (NSNumber *)one
{
return [[[NSNumber alloc] initWithInt:1] autorelease];
}
所谓的auto-release实际上也没有多么自动,而是,,在这一轮的runloop中我们先不释放这个对象,让这个对象可以在这一轮的runloop中可以使用,但是先打上标记,到了下一轮的runloop开始时候,让runtime判断有哪一些前一轮runloop中被标记成是auto-release的对象,这时候才减少retain count 决定是否释放对象。
在建立Foundation对象的时候,除了可以调用alloc
、init
以及new
之外(new这个方法其实就相当于调用了alloc
以及init
;比方说我们调用了[NSObject new]
,就等同于调用了[[NSObject alloc] init]
),还可以调用另外一组与对象名称相同的方法。
以NSString
为例子,有一个叫做initWithString
实例方法,就有了一个对应的类方法叫做stringWithFormat
,使用这一组方法,就会产生auto-release 的对象。也就是说,调用了[NSString stringWithFormat:...]
,相当于调用了[[[NSString alloc] initWithFormat:...] autorelease]
使用这一组方法,就可以是代码量变少。
基本原则
- 如果是
init
、new
、copy
这些方法产生出来的对象,用完之后就应该调用release
。 - 如果是其他一般的方法产生出来的对象,就应该返回auto-release对象或者是
Singleton
对象,就不用另外调用release
。
而调用retain 与release的时候有以下几点: - 如果是一段代码用了某个对象,用完就要 release或者是auto-release。
- 如果是要将某个 Objective-C 对象,变成是另外一个对象的成员变量,就要将对象 retain起来。但是delegate对象不该 retain。
- 在一个对象被释放的时候,要同时释放自己的成员变量,也就是要在实现dealloc 的时候,释放自己的成员变量。要将某个对象设为另外一个对象的成员变量,需要写一对getter/setter。
Getter/Setter与Getter/Setter语法
getter 就是用来取得某个对象的某个成员变量的方法。如果某个成员变量是C的类型,比如说int,我们就可以这样写:
@interface MyClass:NSObject
{
int number;
}
- (int)number;
- (void)setNumber:(int)inNumber;
@end
这里建立的setter 叫做setNumber:
,而getter 叫做number
。请注意,按照其他语言的惯例,getter可能会取名叫做getNumber
,但是Objective-C只能取名number
,例如:
- (int)number
{
return number;
}
- (void)setNumber:(int)inNumber
{
number = inNumber;
}
如果是 Objective-C对象,我们则将原来的成员变量已经指向内存的位置释放掉,然后返回的对象retain起来,可能这样:
- (id)myVar
{
return myVar;
}
- (void)setMyVar:(id)inMyVar
{
[myVar release];
myVar = [inMyVar retain];
}
假如在一个程序中有很多的线程,而在不同的线程中,同时用到myVar,这么其实并不安全:在某个线程中调用了[myVar release]
之后,到mvVar指定到 inMyVar的位置之间,假如另外一条线程也刚好用到myVar,这时候myVar刚好指向一个已经被释放的内存,于是造成EXC_BAD_ACCESS
的错误。
为了避免注意状态的出现,一种方法是加上一些锁lock,当程序在调用setMyVar:的时候,不让其他的线程可以使用myVar;另外一个简单的方法是,只要一直不让myVar指定到可能被释放的内存的位置。我们可以这样改成:
- (void)setMyVar:(id)inMyVar
{
id tmp = myVar;
myVar = [inMyVar retain];
[tmp release];
}
我们先将myVar原本指向的内存的位置,暂存在一个变量中,接着直接将myVar指向返回的内存的位置,接着再释放tmp变量中所记住、原本内存的位置。由于每次都要这样写,写的时间长了会觉得很烦,通常就会写成个macro,或者直接用Objective-C 2.0的property语法。
例如:
@interface MyClass:NSObject
{
id myVar;
int number;
}
@property (retain, nonatomic) id myVar;
@property (assign, nonatomic) int number;
@end
@implementation MyClass
- (void)dealloc
{
[myVar release];
[super dealloc];
}
@synthesize myVar;
@synthesize number;
@end
在这里使用了@synthesize
的语法,在编译的程序的时候,其实就是会编译成getter/setter,而要设置 myVar的内容时候,除了可以调用setMyVar:
之外,还可以调用点语法,像是myObject.myVar = someObject
。
要注意的是,在释放内存的时候,myVar = nil
与self.myVar = nil
这两段程序是不一样的,前者是单纯的将myVar 指向nil,但是并没有释放掉原本的所指向的内存的位置,所以就会造成内存泄漏,但是后河却是等同于调用[self setMyVar:nil]
,会先释放掉myVar原本所指向的位置,然后将myVar设置为nil。
假如:想要知道一个view的x坐标是在哪里,会写出self.view.frame.origin.x
的代码,就需要知道,view
是self的property, frame
也是view
的property,但是x
却是````origin这个
CGPoint里面的变量。而是
origin也是
frame里面的变量。 要取的x,可以这样写
self.view.frame.origin.x```,但是要设定x的位置,如果写成:
self.view.frame.orgin.x = 0.0;
会发生编译上的错误。self.view.frame.origin.x
其实会被编译成[[self view] frame].origin.x
,这也没事,但是,如果要改变view的frame,还是要通过setFrame:
,所以即使我们要改变x的坐标的位置,还得这样写:
CGRect originalFrame = self.view.frame;
originalFrame.origin.x = 0.0;
self.view.frame = originalFrame;
使用property 语法可以精简代码。
ARC
由于ARC通过静态分析,在编译时决定该让程序的什么地方加入retain、release,所以,要使用ARC基本上很简单,就是把原来要手动管理的地方,把retain、release都拿掉,在dealloc的地方,也把[super dealloc]
拿掉。
但是,有了ARC,也不代表就可以在开发iOS的时候,就不需要了解内存管理了。例如,我们任然有很多的程序会使用Objetive-C 开发,但是还会用到C语言,我们还要了解C语言的里面的内存管理。
而且,有时候,ARC也会把retain、release 加错地方,ARC这里面提到的大多数问题。
即使使用ARC,还是必须要注意的内存管理问题。
ARC可能会错误的释放内存的的时候
“#import <QuartzCore/QuartzCore.h>
@implementation ViewController
- (CGColorRef)redColor
{
UIColor *red = [UIColor redColor];
CGColorRef colorRef = red.CGColor;”
//编译器可能在这里会自动产生[red release]
return colorRef;
}
- (void)viewDidLoad
{
[super viewDidLoad];
CGColorRef red = [self redColor];
self.view.layer.backgroundColor = red;
//正确释放内存的时候应该算是在这里
}
@end
哪些地方需要弱应用
ARC有些地方没做retain,结果却又自动多做一次release ,最后导致Bad Access
的错误。我们可以将target/action与必要的参数合起来成为另外一个对象,叫做NSInvocation,在ARC 环境下从 NSInvocation拿出参数的时候,就必须要额外注意内存管理问题。
比如,我们现在要把对UIApplication 要求开放指定的 URL这件事情,变成一个Invocation。
NSURL *URL = [NSURL URLWithString:@"http://www.baidu.com"];
NSMethodSignature *sig = [UIApplication instanceMethodSignatureForSelector:
@selector(openURL:)];
NSInvocation *invocation = [NSInvocation invocationWithMethodSignature:sig];
[invocation setTarget:[UIApplication sharedApplication]];
[invocation setSelector:@selector(openURL:)];
[invocation setArgument:&URL atIndex:2];
假如我们要用以下这段代码的方式,把invocation拿出参数的对象的时候,就会遇到Bad Access 错误:
NSURL *arg = nil;
[invocation getArgument:&arg atIndex:2];
NSLog(@"arg:%@", arg);
//这里会崩溃crash
之所以这里会crash的原因是,我们通过getArgument:atIndex:
拿到参数的时候,getArgument:atIndex:
并不会把arg多retain一次,而到了用NSLog 打印出arg 之后,ARC认为我们已经用不到arg了,所以对arg多了一次release,于是retain与release不成对。
要解决这个问题的方法就是把arg设置为弱应用(Weak Reference)或者Unsafe Unretained,让 arg 这个Objetive-C 对象的指针不被ARC管理,要求ARC不要对这个对象做任何自动的retain和release,在这里使用__weak或者__unsafe_unretained关键字。例如:
__weak NSURL *arg = nil;
[invocation getArgument:&arg atIndex:2];
NSLog(@"arg:%@", arg);
循环Retain
ARC也不会排除循环retain(Retain Cycle)的状况,遇到了循环Retain,还是会造成内存泄漏。循环retain就是,A对象本身retainB对象,但是B对象又retain了A对象,结果只有在释放A的时候才有办法释放B,但是B有得在B被释放的时候才可以释放A,最后导致A和B都没办法释放。这种状况一般出现在:
1.把delegate设为强引用。
2.某个对象的某个property是一个block,但是在这个block 里面把对象本身retain了一份.
3.使用timer的时候,到了dealloc 的时候才停止timer。
假如现在有一个 viewController,我们希望这个viewController可以定时更新,那么可能会使用的方法就是
+scheduledTimerWithTimeInterval:target:selector:userInfo:repeats:
在这个方法建立timer对象,指定定时执行某个selector。特别注意:
在建立这个timer的时候,我们要指定给timer的target,也被timer retain一份,因此,我们想要在viewController 在dealloc的时候,才停止timer就会出现问题:因为viewController已经被timer retain起来了,所以只要 timer 还在执行的时候,viewController就不可能走到dealloc 的地方。
@import UIKit;
@interface ViewController : UIViewController
@property (strong, nonatomic)
“NSTimer *timer;
@end
@implementation ViewController
- (void)dealloc
{
[self.timer invalidate];
}
- (void)timer:(NSTimer *)timer
{
// Update views..
}
- (void)viewDidLoad
{
[super viewDidLoad];
self.timer = [NSTimer scheduledTimerWithTimeInterval:1.0
target:self
selector:@selector(timer:)
userInfo:nil
repeats:YES];
}
@end
要改掉这些问题,应该改成在viewDidDisappear: 的时候,就要停止timer。
对象桥接(Toll-Free Bridged)
Foundation Framework里面的每个对象,都有对应的C实现,这一层的C实现叫做Core Foundation。当我们在使用Core Foundation里面的C类型时,像是CFString、CFArray 等,我们可以让这些类型变成可以接受ARC的管理。这种C类型也可以被当做Objetive-C 对象,接受ARC的内存管理方式,叫做对象桥接(Toll-Free Bridged)。
Toll-Free Bridged有三个关键字:
-
__bridge
、__bridge_retained
以及__bridge_transfer
。 -
__bridge
会把 Core Foundation 的C数据类型换成Objetive-C 的对象,但是不会多做retain与release。 -
__bridge_retained
会把 Core Foundation的数据类型转换成Objetive-C 对象,并且会做一次retain,但是之后必须由自己手动调用CFRelease
,释放内存。 -
__bridge_transfer
会把Core Foundation对象转换成Objective-C 对象,并且会让ARC主动添加 retain 与release。
更详细的请移步苹果官方文档
其他使用须
Objetive-C语言有了ARC之后,除了禁止使用 retain、release这些关键字之外,也禁止我们手动创建NSAutoreleasePool,同时禁止了一些我们在ARC没出现之前用的写法(或黑魔法),包括不可以把Objective-C放在C Structure里面,编译器会告诉我们是语法错误。
在有了ARC 之前,我们之所以会把Objective-C对象放在C结构体中,大概的目的,其中之一,假如我们有一个类有很多的成员变量,那么我们可能会以下这种写法将成员变量分组:
@interface MyClass : NSObject
{
struct {
NSString *memberA;
NSString *memberB;
} groupA;
struct {
NSString *memberA;
NSString *memberB;
} groupB;
}
@end
这样,如果我们想要使用groupA中的memberA,可以用self.groupA.memberA
调用。
另外一个目的,有时候,我们可能会很刻意隐藏某个Objective-C类里面的有哪些成员变量。像是下面一段代码:原本有一个类叫做MyClass,里面有一个privateMemberA与privateMemberB两个成员变量,原本应该直接写在MyClass的声明里面,但是我们却刻意吧两个成员变量包在_Privates 这个C结构体中,而原本放在MyClass的成员变量声明的地方,只剩下了一个叫做privates 的指针,光看这个指针,让人很难理解这个类中到底有什么东西。
@interface MyClass : NSObject
{
void *privates;
}
@end
typedef struct {
NSString *privateMemberA;
NSString *privateMemberB;
} _Privates;
@implementation MyClass
- (void)dealloc
{
_Privates *privateMembers = (_Privates *)privates;
[privateMembers->privateMemberA release];
[privateMembers->privateMemberB release];
free(privates);
privates = NULL;
[super dealloc];
}
- (instancetype)init
{
self = [super init];
if (self) {
privates = calloc(1, sizeof(_Privates));
_Privates *privateMembers = (_Privates *)privates;
privateMembers->privateMemberA = @"A";
privateMembers->privateMemberB = @"B";
}
return self;
}
@end
其实这样写是为了保护程序的技巧,主要是防止class-dump.class-dump可以让编译好的二进制中还原出每一个类的头文件,当我们从一个class-dump抽出别人的头文件,看出有哪些类,每个类有哪些成员变量、有哪些方法,就可以看出整个APP的大致的架构如何。这样的写法就是让别人用
class-dump导出我们的APP头文件的时候,不太容易了解我们是用哪些类实现的,不过对于做软件破解的人来说,其实只要花时间,所有的软件都可以破解。
UIViewController的life cycle。
建立一个UIViewController之后,Xcode会给我们的模板中,会叫我们去实现一个叫做didReceiveMemoryWarning:
的方法,当系统内存不够的时候,这个方法可能会帮我们释放掉一些内存。
内存不足警告(Memory Warnings)
在内存不足额时候,除了会对UIApplication的delegate(也就是AppDelegate)调用applicationDidReceiveMemoryWarning:
之外,也会对系统中所的UIViewController调用didReceiveMemoryWarning:
。
UIViewController与View的关系
UIViewController负责管理在应用程序中每个会用到的界面,主要的属性就是view,而这个属性是使用懒加载模式就是:我们用到某个对象的时候才去建立某个对象,避免在对象初始化的时候就建立所有的property,而达到让初始化对象这些动作加速的效果。当我们通过alloc、init或者initWithNibName:bundle:建立ViewController的时候,并不会马上建立view,而当我们调用view这属性的时候才会建立。如下:
//建立MyViewController的实例,这时候没有建立view
MyViewController *controller = [[MyViewController alloc]
initWithNibName:NSStringFromClass([MyViewController class]) bundle:nil];
//在被加入到navigation时候才会去调用 [controller view]
//这时候view才会被建立起来
[navigationController pushViewController:controller animated:YES];
[controller release];
用懒加载的方式作为实现一个getter的方式大致如下,在程序中想要使用有效的内存,可以这样写:
- (UIView *)view
{
if (!_view) {
_view = [[UIView alloc]
initWithFrame:[UIScreen mainScreen].bounds];
}
return view;
}
不过,UIViewController在还没有view,而要去建立view的时候,会调用的其实是loadView
这个方法,在View成功载入之后,则会调用viewDidLoad
.但是还是不知道苹果到底如何实现UIViewController。
- (UIView *)view
{
if (!_view) {
[self loadView];
if (_view) {
[self viewDidLoad];
}
}
return view;
}
所以,如果有一天不小心写成这样,就会进入无限循环:因为调用[self view]
的时候发现没有view就会调用loadView,但是调用loadView又会调用[self view] 。
- (void)loadView
{
[self view];
}
所以要注意的是,viewDidLoad
并不是UIViewController
的Initializer,虽然我们在使用某个ViewController的时候,一定会调用到一次viewDidLoad
,我们也通常在这个地方,做一些初始化的这个ViewController的事情一旦viewDidLoad是有机会在ViewController的Life Cycle中被重复的调用好几遍在建立了view之后,view也可指向nil,所以ViewController可能被重复释放与载入view,viewDidLoad也会被重复调用。
如何知道哪一个ViewController位于最上层?
那么,ViewController自己怎么知道自己位置在最前面呢?很简单:ViewController被放到最上层的时候,会被调用viewWillAppear:
以及viewDidAppear:
,离开最上层的时候,会调用viewWillDisappear:
与viewDidDisappear:
。
只有调用了viewWillAppear:
以及viewDidAppear:
,而没有调用viewWillDisappear:
的ViewController,就是位于最前面的ViewController。
我们经常会重写viewWillAppear:
这些方法,在做重写的时候,应该要调用一次super 的实现,因为super 的viewWillAppear:
这些方法实现了一些必要的事情。
所以应该在didReceiveMemoryWarning:做什么?
当内存不足时,我们可以选择性的决定要不要释放view,像是web view这种内存在没用到的时候是该释放掉。
以下是苹果给的规范代码:
- (void)didReceiveMemoryWarning
{
[super didReceiveMemoryWarning];
if ([self.view window] == nil) {
self.view = nil
}
}