iOS套路面试题之内存管理

内存管理在Objective-C是一件简单又麻烦的事情。
简单是因为所谓的内存管理不过是两件事情:一块内存我们要用,还是不用,该用的时候用,不用的时候就释放。** 麻烦**的就是我们很容易疏忽而造成用与不用不匹配。不匹配,就造成两种结果:如果用完不释放内存,就会造成软件占用一堆没用到的内存,造成内存飙升,然后iOS上的系统会强行终止我们的应用程序,这种状况我们称为内存泄漏(Memory Leak)。
但是,如果一块内存已经被释放掉,我们还以为这个内存还存在,还是可以调用的对象,所以当我们尝试调用时候,会发现这个内存存在的对象已经不在了,这种状况就叫做over-release或者是invalid memory reference,会造成应用程序的crash,crash log 上会告诉你错误类型是EXC_BAD_ACCESS

Reference Count/Retain/Release

Objective-C 语言里面每一个对象,都是指向某块内存的指针,在C语言中,会使用像malloccalloc这些函数来设置内存,用完之后调用free。但是我们如何知道一块内存被多少地方用到,之后这些地方又用不到呢?所以Objective-C,有了计算有多少地方用到某个对象的简单机制,叫做reference count(引用计数),意思很简单:只要一个对象被用到某一个地方一次,这个地方就对这个对象+1,反之—1,如果数字减到0,那就是释放这块内存。
如果一个对象使用allocinit········产生,初始的引用计数为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对象的时候,除了可以调用allocinit以及new之外(new这个方法其实就相当于调用了alloc以及init;比方说我们调用了[NSObject new],就等同于调用了[[NSObject alloc] init]),还可以调用另外一组与对象名称相同的方法。
NSString为例子,有一个叫做initWithString实例方法,就有了一个对应的类方法叫做stringWithFormat,使用这一组方法,就会产生auto-release 的对象。也就是说,调用了[NSString stringWithFormat:...],相当于调用了[[[NSString alloc] initWithFormat:...] autorelease]使用这一组方法,就可以是代码量变少。

基本原则

  • 如果是initnewcopy这些方法产生出来的对象,用完之后就应该调用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 = nilself.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
    }
}
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 213,928评论 6 493
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 91,192评论 3 387
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 159,468评论 0 349
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 57,186评论 1 286
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 66,295评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 50,374评论 1 292
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,403评论 3 412
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,186评论 0 269
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,610评论 1 306
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,906评论 2 328
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,075评论 1 341
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,755评论 4 337
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,393评论 3 320
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,079评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,313评论 1 267
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,934评论 2 365
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,963评论 2 351

推荐阅读更多精彩内容

  • *面试心声:其实这些题本人都没怎么背,但是在上海 两周半 面了大约10家 收到差不多3个offer,总结起来就是把...
    Dove_iOS阅读 27,136评论 30 470
  • 1.Difference between shallow copy and deep copy? 浅复制和深复制的...
    用心在飞阅读 990评论 0 9
  • 37.cocoa内存管理规则 1)当你使用new,alloc或copy方法创建一个对象时,该对象的保留计数器值为1...
    如风家的秘密阅读 838评论 0 4
  • OC的理解与特性OC作为一门面向对象的语言,自然具有面向对象的语言特性:封装、继承、多态。它既具有静态语言的特性(...
    LIANMING_LI阅读 512评论 0 0
  • 找一处偏远小山岭,搭个小矮房, 山岭里的天显得宽,房衬着白 , 地上处处有绿色, 屋前栽头木瓜,屋后种几根甘蔗, ...
    星日记阅读 305评论 0 1