1. ARC之前
最近买了本ios多线程和内存管理,对就是日本人写的那本书,然后看了目录就深深的鄙视了下,我都会嘛,然后看了下就傻逼了。感觉日本人对有的东西不是一般的严谨啊,非得要挖地三尺,这让我想到了当年金刚石大会的那个日本教授。
言归正传,看了这本书我才深深的感受到没有经历MRC时代的人是多么的幸福。ARC是多么的方便。
对于内存管理这部分,我想从头开始思考下,在N年前,在没有ARC(自动管理计数)的时候,我们是怎么写OC的呢?
{
id o1 = [[NSObject alloc] init];
id o2 = [o1 retain];
id o3 = [o1 retain];
// use o1,o2,o3
// free o1
[o1 release];
[o2 release];
[o3 release];
NSLog(@"%@",o3);
}
由于oc采用的计数规则来控制内存的,这个应该大多数都明白。简单来讲,每一个对象的地址里面都存有一个retainCount的变量,retain操作使得retainCount+1 , release操作使得retainCount-1 , 当retainCount等于0的时候,就会自动调用delloc方法,这个变量所占用的内存就释放了。
通常来讲,我们将allloc,copy,new方法生成出来的对象会被自己持有。简单来讲,如果我们用alloc去创建一个对象然后赋值给一个变量,那么这个对象就归变量所持有,retainCount就变为1,如果别的变量想持有这个对象要怎么做呢?retain一下就好了。
so,按照原则,当我们释放对象的时候,如果我们retain了一次,那么就必须release一次。
如果release少了,那么内存得不到释放,也就是内存泄露了;如果release多了,那么编译器会报错:
objc[33744]: NSObject object 0x100600fd0 overreleased while already deallocating; break on objc_overrelease_during_dealloc_error to debug
真是太麻烦了,这样看来在上古时代的代码里面 retain和release的出现频率应该是最高的吧。
然后楼主发现,最后那个nslog居然能打出来对象,纳尼,最后楼主才反应过来,释放内存并不是把内存的东西置为空啊,而是让其标记为可用啊!基础学的不好真蛋疼~~~
id o4 = [[NSObject alloc] init];
[o4 release];
id o5 = [[NSObject alloc] init];
NSLog(@"%@",o4);
NSLog(@"%@",o5);
//2016-06-02 21:43:54.763 arc[33946:2099866] <NSObject: 0x1001024c0>
//2016-06-02 21:43:54.763 arc[33946:2099866] <NSObject: 0x1001024c0>
看到了吧,内存的确是被释放了。来看下下一段代码
id getObj(){
id a = [[NSObject alloc] init];
return a ;
}
int main(int argc, const char * argv[]) {
{
id o4 = getObj();
NSLog(@"%lu",(unsigned long)[o4 retainCount]);
[o4 release];
}
}
纳尼,这是什么sb代码,没问题啊 retainCount是1。
但是按照我们的规则,o4去持有getOBj()返回的对象的时候,必须去retain一下,所以上面的代码应该改成
id o4 = [getObj() retain];
然后一看log出来的信息,SB了吧,是2!按照我们的理想不是应该是1么。回过头来我们看下getObj方法,a变量持有了对象,但是在作用域的最后并没有release,所以按照我们上面的分析,会造成内存泄露。但是最开始的错误代码阴差阳错的对了,可是这是我们不希望看到了。但是如果我们把a变量release掉,那么返回值就没有了啊~~~!@#@¥@#%¥……¥%……¥%@#¥@啊啊啊啊啊。现在至此,就可以引出下一个oc中重要的东西了autoreleasePool; 很多人都把这个东西和ARC联系在一起了,其实不然啊在MRC时代这个东西就已经很普及了。
2.自动释放池
在没有autorelease的时候,我们释放一个对象需要调用release,这个release是会立刻执行的,一个作用域的最后我们需要将所有的作用域变量都release一次,但是就像上面的例子,一样,如果是有返回值的作用域,怎么办?我们需要让它先作为返回值先返回,然后再某一个时间进行一次release,autorelease就可以做到。
我们改造一下我们的代码。
id getObj(){
id a = [[NSObject alloc] init];
return [a autorelease];
}
int main(int argc, const char * argv[]) {
id o2 = nil;
@autoreleasepool {
id o4 = [getObj() retain];
o2 = [o4 retain];
NSLog(@"retainCount : %lu",(unsigned long)[o2 retainCount]);
[o4 release];
//_objc_autoreleasePoolPrint();
}
NSLog(@"retainCount : %lu",(unsigned long)[o2 retainCount]);
//2016-06-02 22:29:40.023 arc[34421:2135815] retainCount : 3
//2016-06-02 22:29:40.024 arc[34421:2135815] retainCount : 1
看到了吗?当我们使用了autoreleasePool的时候,整个世界就光明了,完全按照我们最初的逻辑去走了,最后对象只有一个o2持有,retainCount=1。
autorelease方法会把对象注册到autoreleasePool里面,然后在autoreleasePool的最后调用release方法。所以这个问题解决了,我们把a变量release的时间推迟了,使得我们函数返回了所需要的对象,然而又没有破坏我们的规则。所以很多时候我们都不习惯直接release,而是autorelease让自动释放池去帮我们处理这些事情,带来了很多方便。
3. ARC的出现
接下来ARC出场了--自动管理计数,在采用ARC之后,你的代码里面就再也看不到retain release autorelease这样的方法了,因为编译器会自动帮我们在需要的地方添加这些方法,来轻松的管理我们的内存。
这里就不得不提到ARC引入的几个变量修饰符了
__Strong , __weak, __unsafe_unretained, __autoreleasing
1.__strong
先说一说Strong(强引用),这也是默认的修饰符,如果我们不给一个对象变量指定修饰符的话,那么默认是strong类型,strong类型意味着对象会被变量所持有,意味着引用计数会被加1。
{
id __strong a =[ [NSObject alloc] init]; //retainCount=1
id __strong b = a; //retainCount=2
}
编译器在__strong类型持有某个对象的时候自动的把这个对象retain一次(这里的retain是优化过的),而在代码块的最后,将自动release一次所有的strong变量。由于我们在第一节就说过alloc,new,copy,mutablecopy方法所出来的对象会被自己持有。但是不是通过上面方法返回的对象呢,比如在2中函数返回值所返回的对象?
{
id getObj(){
id a = [[NSMutableArray alloc] init];
return a;
}
int main(int argc, const char * argv[]) {
@autoreleasepool {
id __strong o4 = getObj();
_objc_autoreleasePoolPrint();
}
/*objc[1722]: ##############
objc[1722]: AUTORELEASE POOLS for thread 0x10007f000
objc[1722]: 2 releases pending.
objc[1722]: [0x101800000] ................ PAGE (hot) (cold)
objc[1722]: [0x101800038] ################ POOL 0x101800038
objc[1722]: [0x101800040] 0x1003001f0 __NSArrayM
objc[1722]: ##############
*/
}
通过这段代码我们可以看到ARC帮我们做了什么,在print的第一个自动释放池时候,0x1003001f0 __NSArrayM对象存在于池子中,这是因为由于给strong变量赋值是通过函数返回值的形式,为了保证strong变量可以接收到返回值,并且保证函数中alloc出来的对象还应该release一次,ARC将这个变量注册到了自动释放池里面去,那么在自动释放池的最后,这个变量会执行一次release操作,来确保不会发生内存泄露。
再看下一个嵌套的例子~
id getSubObj(){
id a = [[NSMutableArray alloc] initWithObjects:@"a", nil];
_objc_autoreleasePoolPrint();
return a;
}
id getObj(){
id a = getSubObj();
_objc_autoreleasePoolPrint();
return a;
}
int main(int argc, const char * argv[]) {
@autoreleasepool {
id __strong o4 = getObj();
_objc_autoreleasePoolPrint();
}
按照我们前面提到的逻辑,执行完 id __strong o4 = getObj();之后,obj应该在自动释放池里面注册了两次,然而实际上却只有一次,这就是ARC所做的工作,他会自动优化自动释放池,能够让对象不过度的注册到自动释放池里面,并且不会出现内存的问题(具体可以参考mac ox多线程管理的P67)。
2.__weak
如果一个变量使用了__weak修饰,那么这个变量就不持有这个对象。也就是说不会增加对象的引用计数,变量销毁的时候也不会调用对象的release方法。什么是不持有呢?就如同下面这段代码,weak变量既不能增加对象的引用计数,也不能销毁对象,当所指的对象被销毁之后,会被自动置为空!!
@autoreleasepool {
id __strong o4 = [[NSObject alloc] init];
id __weak o3 = o4;
o3 = nil;
NSLog(@"%@",o4); //<NSObject: 0x100103b30>
o3 = o4;
o4 = nil;
NSLog(@"%@",o3); //null
}
那么weak是怎么实现的呢~其实程序中会有一张weak的表,一旦有变量被声明为weak,也就将这个变量注册到了这张表中,被废弃则从这张表中删除,所以如果一旦使用了大量的__weak,势必会对程序的资源有所消耗,正如书中所告诉我们的,只需要在避免循环引用的时候使用__weak,而避免滥用。
3.__autoreleasing
和strong一样我们一般不用为变量显式的添加autoreleasing修饰符。编译器会在合适的地方自动的添加这个符号,而这个修饰符的实际用途就是将对象在自动释放池里面去注册一次。
比如在使用对象的指针的时候,如果没有显式的制定修饰符,那么会默认加上__autoreleasing修饰符。比如在cocoa框架中,NSError这个类通常是用对象指针去传递的。而不是函数的返回值。
void sendError(id *a){
*a = [[NSError alloc] initWithDomain:@"abc" code:1 userInfo:nil];
_objc_autoreleasePoolPrint();
}
int main(int argc, const char * argv[]) {
@autoreleasepool {
NSError *error = [[NSError alloc] init];
sendObj(&error);
objc[10274]: 2 releases pending.
objc[10274]: [0x101800000] ................ PAGE (hot) (cold)
objc[10274]: [0x101800038] ################ POOL 0x101800038
objc[10274]: [0x101800040] 0x100603660 NSError
而在sendError函数中id的指针类型被自动声明成了autoreleasing,从而将error对象加入注册进了自动释放池,其实不难理解为什么要这么做,这样能够确保在自动释放池作用域内,对象不会被释放掉,引起程序的错误。
4. 总结
其实现在苹果一直在简化开发难度,ARC的引入正式证明了这么一点,一般来将,你不需要特别的深入了解内存管理机制,因为在大多数情况下ARC都能帮助我们正确的管理内存。只要我们按常规来写代码,程序应该不会出现很大的问题,像循环引用这种造成内存泄露的事情,使用weak修饰符就可以很好的解决了。另外就是内存释放造成空指针,只要搞清楚对象的作用域范围就不会出现这种问题了,比如在函数作用域中创建的对象,如果我们希望他的生命周期和父对象一样,那么一定要用父对象的Strong属性去持有她。如果想了解下更多底层的懂西,那么《Objective-C高级编程:iOS与OS+X多线程和内存管理 》这本是应该是不二的选择。
网友评论