Objective-C的内存管理艺术:入门

文章框架

前言

呀!!再也不三心二意了,从今往后专心搞iOS!嗯!
从大一开始就嚷嚷对iOS热爱的很,结果呢,就毕设写了个垃圾,咦!
所以现在起,iOS就是我的主线了!先写这篇垃圾文章壮壮气势!

本篇内容非常浅显的谈谈Objective-C中的内存管理,适合那些会写点简单的App但从未认真对待过内存管理的初级iOS工程师阅读,在整个内存管理的艺术之路上入个门。


传统内存管理

传统的内存管理我想从C语言说起。当你用malloc函数申请一片空间的时候(或是C++的new),就一定要用free函数释放掉(或是C++的delete)。

但是在面向对象的世界,我们一切打交道的东西是对象,这玩意你可不能说来就来说走就走,来就得初始化一下(构造函数,空间申请+变量初始化),走就得垂死挣扎一下(析构函数,遗产管理+空间释放)。

Objective-C是一门面向对象的语言,所以其与之对应的方法就是allocinitdeallocdealloc就是析构函数。其构造函数相当于先调用allocinit,说白了就是他把空间申请和变量初始化分的很清楚,alloc就是空间申请,init就是变量初始化,这俩通常是成对儿出现的。你问只allocinit行不行,可以!没问题!只不过你的私有成员表示很郁闷:谁来给我们赋初值啊?

定义

  • 构造函数
    也就是构造的时候,在objective-c中定义构造函数时不用你去申请空间,只是写好初始化方法init就好了
1
2
3
4
5
6
- (instanceType) init {
[super init]; // 别忘了要把祖宗也都构造上
if(self){
// 需要初始化的东西
}
}
  • 析构函数
    析构函数,在对象被释放之前,对象总要那么挣扎几下才会安心的去的。不过你也可以让你的对象没有任何想说的直接挂掉,这就可能会出现问题:这财产归谁啊?还是彻底不要了?不能扔那不管啊!
1
2
3
4
- (void) dealloc {
// 遗嘱
[super dealloc]; // 株连九族
}

注意,这里超类的析构函数要最后调用。

调用

  • 构造函数
1
ClassName* instance = [[ClassName alloc] init]; // 先构造再初始化

如果说你的构造函数不需要什么参数,直接走默认

1
ClassName* instance = [ClassName new]; //  等价于上面的

如果你很奔放的不需要初始化

1
ClassName* instance = [ClassName alloc]; // 完全没问题! 没毛病~
  • 析构函数
1
[instance dealloc];

至此,在Objective-C中最基本的动态内存管理算是介绍完了。你要是不看后面的直接这么玩,没问题!

那么问题来了:遇到复杂的情况,这种传统模式就很捉急。
比如我要在指针A中引用空间M引用B也要引用对象M对象C也来凑热闹引用了对象M。哇!这对象M简直是香饽饽。A说了:“这盘香饽饽是我做的,我要吃,谁最后吃完谁收拾剩饭”;B说了:“这盘香饽饽我要吃,谁最后吃完谁收拾剩饭”;C说:“我不吃,我就看看。”

好,最后这盘香饽饽什么时候收拾?对应在程序里面也就是什么时候调用[M dealloc]

谁最后吃完谁收拾呗,说得好听!谁知道程序里面谁最后吃完啊,对应着用上面的方式A B C里面都引用了MAB谁最后吃完那完全是看心情的,都任性的很呢!


引用计数管理

引用计数是个好方法,能解决上面的香饽饽问题。也就是这种机制可以让程序知道到底是谁最后吃完了香饽饽。
其基本思想:用一个计数器,来一个吃香饽饽的就+1,吃完一个就-1,减到0了就释放。

手动引用计数(MRC, Manual Reference Counting)

或称为MRR(Manual Retain Release)。这算是最基本的引用计数操作。

  • +1
    对应的方法就是retain。但除此之外,由allocnewcopymutableCopy这样直接生成对象的方法会直接让引用计数自动+1,加给谁?就是=左边的那位。
    所以像前面的这种
    1
    ClassName* instance = [[ClassName alloc] init];

instance所指向的对象引用计数会直接+1。
你说我没有左边那位是不是也要+1呀?是!
如果你直接这样写:

1
[[ClassName alloc] init]

等你去编译的时候,编译器就给你改成

1
2
id tmp = [[ClassName alloc] init]
[tmp autorelease]

是的,他就偏往=左边临时放一位,id相当于C语言中的void *指针,这里不谈。autorelease方法本篇也不谈,你可以暂时把它看成release也就是我下面-1部分要说的。
就像前面的香饽饽情形中,香饽饽M是A做的,A和B都要吃,C只看不吃:

1
2
3
ClassName* pointerA = [[ClassName alloc] init];  // 引用计数+1
ClassName* pointerB = [pointerA retain]; // 引用计数+1
ClassName* pointerC = pointerA; // 引用计数不改变,C:我就静静的看着你们吃

引用计数为2,也就是有两位正在享用香饽饽。

  • -1
    吃完了,就别直接收拾了,得看看其他人还有没有吃完是我们中华民族的传统美德,为了经这种美德延续下去,你调用个release就行啦(但别忘了,赋nil)。
    A吃完了:
    1
    2
    [instanceA release];  // 引用计数-1
    instanceA = nil;

B吃完了:

1
2
[instanceB release];  // 引用计数-1
instance = nil;

当引用计数减为0时,就释放空间,也就是收拾剩饭。
为什么要后面赋nil:因为release之后,指针还是指向那片空间,如果等那个空间被释放了,一不小心又通过那个指针(这时这个指针有了一个响当当的名字:悬垂指针,详见注释2)访问了一下那片空间,那就很危险了。如果那片被回收的区域没有被其他程序覆写,那还好,你可能还侥幸能访问到那里的东西。但如果那片空间被其他人覆写了,这就是非法访问了,非法访问的后果就是crash!
C说了:我要搞事情!

1
2
[instanceC release];
instanceC = nil; // 搞完事就跑路真刺激!

行不行?还真可以!无论是编译阶段还是运行时他都不会报错的。这就等于C在饭局中冒充一个用餐的喊了句:我吃完啦!最后计数器一算:噢,客人都吃完走了,可以收拾饭桌了。结果最后真正还在吃的那位:MMP!
程序中是这样的:

1
2
3
4
5
6
[instanceA release];  // 引用计数-1
instanceA = nil;
[instanceC release]; // 引用计数-1,此时引用计数已经为0了,自动调用dealloc
instanceB = nil;
[instanceB release]; // 很有可能还没执行到这一步程序已经崩溃了,因为B先生开始发疯:我的饭呢?!
instanceC = nil;

就算B先生恰好也吃完了,在他说“我吃完了”的时候,发现没人鸟他了,这时B先生又疯了!所以程序又崩了。
这个疯了在程序里指的就是B先生变成了悬垂指针。

另外,如果你想查看引用计数就调用[instance retainCount],它会返回一个无符号整形,也就是引用计数的值。
你会发现当减到0的时候,如果你再调用retainCount的话,其值仍然为1,首先你这样做是非常危险的,因为他已经被释放了,你现在是在访问悬垂指针。如果能访问到值而没有崩溃当然是因为那块被回收的空间还没有被覆写,其次它的计数值没有变为0是因为没有必要了,这块空间已经被释放了,也没必要去给它计算多一次,而且这个1本是你不该知道的!

至此,你可以发现MRC还是很屌的,只要你别犯贱,一般不会出现什么问题了,也完美解决了之前的香饽饽问题。

自动引用计数(ARC, Automatic Reference Counting)

自动引用计数的出现减少了很多的内存管理代码,从而交给编译器自动去做。是的,这玩意儿是编译器层面的(但不完全是,也需要运行时的帮助,但认识它,透过它编译时所做的事就够了)。

所有权修饰符
  • __strong
    强修饰符,默认修饰。修饰对象属性的时候写成@property(strong)
    1
    2
    3
    4
    5
    {
    id creator = [[NSObject alloc] init]; // 这个也是__strong修饰的,默认修饰
    id __strong strongRef = creator;
    // something else
    }

相当于MRC中的

1
2
3
4
5
6
7
{
id creator = [[NSObject alloc] init];
id strongRef = [creator retain];
// something else
[strongRef release];
[creator release];
}

意思就是,只要是给我strong引用赋值的,我都会retain那个对象。
而且那个__strong可以不写出来,因为它是在ARC下默认的所有权修饰符。
而且在这个修饰的变量在其生命周期结束后,会自动调用release。并不是动态的调用release,而是ARC会帮你在那个生命周期结束的位置自动写上release

  • __weak
    弱修饰符,修饰对象属性的时候写成@property(weak)。带有此修饰符的指针变量所指向的对象被释放后自动赋nil。本修饰符在iOS4以上支持,若是之前的版本就用__unsafe_unretain修饰符代替(对应的属性修饰为@property(assign))。但是它不会自动赋nil,所以它是不安全的。
    1
    2
    3
    4
    5
    {
    id creator = [[NSObject alloc] init];
    id __weak weakRef = creator;
    // something else
    }

相当于MRC中的

1
2
3
4
5
6
7
{
id creator = [[NSObject alloc] init];
id weakRef = creator;
// something else
weakRef = nil; // 若引用所指向的对象的引用计数为0后就自动赋nil
[creator release];
}

总的来说就是ARC帮你在编译时写了MRC要写的东西,MRC中的那些方法在ARC是不支持的(retainreleaseautoreleasedeallocretainCount不可以人为调用,因为ARC已经帮你写好了)。所以并不是说有了ARC就完全不用担心内存管理,有些需要在运行时管理的内存还是不能太依赖ARC。它只是帮你从那些一般的MRC操作中释放出来。


结语

可以发现从传统到MRC再到ARC是一层一层进化而来,在传统上添加一个引用计数器就是MRC,编译器接手引用计数就变成了ARC。
除此之外上述之外,还有autoreleasepool,本文不提。有兴趣的朋友自行翻资料吧,这篇文章只是在Objective-C内存管理方面带你入个门。


注释

  1. 野指针(Wild Pointer):在声明指针变量时,有些开发语言并不会帮你自动赋nil。所以这个指针可能在刚声明的时候是指向一个迷の区域的。这样的指针就是野指针。
  2. 悬垂指针(Dangling Pointer):悬垂指针就是本指向一个曾经有对象(该指针曾经指向的对象)的空间,但现在是被回收的空间。
  3. 内存泄漏(Memory Leak):内存泄漏指的是内存中存在一些对象实体,他们占用空间,却没有任何指针变量指向他们,所以他们不会被释放掉。