浅析在项目开发(使用Delegate回调时)如何正确使用ARC
ARC(自动引用计数)是2011年伴随iOS5来的一项技术。简单来说就是通过LLVM3.0编译器帮助程序处理“一大部分”OC中的内存管理。为什么是“一大部分”,这个等会儿解释。
一直以来内存管理这个话题都是初学iOS开发,初学OC语言必须要面对的知识点,也是大家容易出错的地方。对象释放后调用会造成crash、不释放的对象会造成内存泄漏这些问题困扰着初学者。ARC的到来按理说应该是福音,不需要自己管理内存了嘛,多简单。但是随之而来的两个问题:1,我发现周围有些拥有2-5年开发经验的“半老手”(要谦虚- -#)不愿意使用ARC,包括半年前的我。因为当年MRC(手动引用计数)花了好些精力去慢慢习惯培养起来,熟练了后,反而对新的ARC有不放心、不习惯;2,新手在使用ARC时,以为编译器已经全部为你做好了内存管理,而不去做相应的操作,最终导致内存泄漏。另外,在工作中我甚至发现,一些PL,都会在ARC上面犯错,或许他们真的没在意,但问题不能就这么放着!就是以上原因让我有动力在空闲时间来好好做个Demo来总结一下。
好了,说了半天废话,首先如果你还对“内存管理”、“MRC”、“ARC”的原理有些模糊说不清的话,这里有一篇好文章你可以借鉴一下:请戳http://onevcat.com/2012/06/arc-hand-by-hand/ 作者大牛对ARC的解释非常详细,包括后面对MRC项目转换为ARC项目都有详细说明。我就不班门弄斧写教程了(也没那个实力呵呵)。
先说说我对ARC的理解:
1,一个对象,只要有大于或等于一个Strong类型指针指向它,该对象就不会被释放;
2,一个对象,假如只剩下一个或多个weak指针指向它,那么对不起,该对象就自动被释放,然后这些一个或多个weak指针都会被自动设置为nil;
3,默认条件下,指针都是Strong,除非你特别声明,如在最前面加__weak;
4,使用ARC,不代表和内存管理完全拜拜了,你仅仅不需要retain、release、autorelease、[super dealloc],但是在必要的时候,还是得向一个NSObject setNil,才能完成对内存的释放。
下面用一个demo来简单演示普通情况下的UIView子类内存释放以及使用delegate回调时如何正确释放。demo很简单,在这里可以看到源代码https://github.com/pigpigdaddy/ARCTestDemo
一:普通情况下删除一个UIView子类,正确释放内存
我创建了一个空项目,又创建了两个类,一个是ViewController,用于window的rootViewController。一个是ARCTestView,继承自UIView,我们就是要看看它的实例在删除时候是否按照我们的想法正确被释放。有一点要先说明一下,在ARC下,仍然可以显示地写出dealloc函数,并且实例只有自动调用dealloc函数了,才说明它被正确释放了!
1 - (void)viewDidLoad 2 { 3 [super viewDidLoad]; 4 // Do any additional setup after loading the view. 5 6 self.button = [UIButton buttonWithType:UIButtonTypeSystem]; 7 self.button.frame = CGRectMake(20, 20, 40, 30); 8 self.button.backgroundColor = [UIColor blueColor]; 9 [self.button addTarget:self action:@selector(removeTestView:) forControlEvents:UIControlEventTouchUpInside]; 10 [self.view addSubview:self.button]; 11 12 ARCTestView *testView = [[ARCTestView alloc] init]; 13 testView.frame = CGRectMake(0, 80, 320, 350); 14 testView.backgroundColor = [UIColor redColor]; 15 testView.tag = 100; 16 [self.view addSubview:testView]; 17 }
在viewDidLoad函数中,我创建了一个ARCTestView的实例,并把它addSubview,请注意按照之前所说的默认状态下指向该对象的指针是Strong,因此testView指向的实例在该作用域(函数viewDidLoad)中是被持有的,该实例在该作用域结束前不会被释放。一会儿说一下“作用域”的情况。ARCTestView的.m文件中的dealloc函数内,我打了断点(或者你可以写一个NSLog函数在里面)用来检查是否ARCTestView的实例被释放。另外,viewdidload中我创建了一个button,绑定了点击事件,实现了将ARCTestView实例从界面上删除的功能。
1 - (void)removeTestView:(id)sender 2 { 3 UIView *view = [self.view viewWithTag:100]; 4 if (view) { 5 [view removeFromSuperview]; 6 } 7 }
OK,现在是解释“作用域”的好时机:由于临时变量(指针)哪怕它是Strong的,只要出了作用域,就必然失效。所以,如果此时该实例没有被其他Strong指针所指,那么就会被释放。这符合之前的归纳的要点:1,一个对象,只要有任何大于或等于一个Strong类型指针指向它,该对象就不会被释放;2,一个对象,假如只剩下一个或多个weak指针指向它,那么对不起,该对象就被释放,然后这些一个或多个weak指针都会被设置为nil。因此你不用担心viewDidLoad和removeTestView里的临时变量会不被释放,或者需要手动设置为nil。因为在这个例子中,这些临时指针指向的变量,没有其他Strong指针指向它们了。
验证方法:你可以将viewDidLoad中的最后一句注释掉 1 [self.view addSubview:testView]; ,让self.view不去持有testView指向的实例。这样新创建的ARCTestView实例,在作用域结束时,testView指针失效,另外再也没有指针持有它了,因此viewDidLoad结束,实例被释放,进入ARCTestView的dealloc函数。
我之所以要着重强调这一点,因为是想解释一种机制:原本在MRC中,临时变量创建后(alloc init),用完后是需要手动release的。而既然ARC不能release,那么肯定是在作用域结束后,随即结束该临时指针的使命。这呼应了ARC的机制!
运行程序你会看到:
蓝色按钮、红色的ARCTestView。至于红色内部的黄色块,不要着急,那是“二”里面的内容。
现在点击蓝色按钮,看看效果,红色的界面被删除了。同时,进入了ARCTestView的dealloc函数。有人要问,假如我创建的ARCTestView不是临时的而是一个Strong类型的@property呢。答案是:只有在删除界面后(removeFromSuperView)再将该@property setNil才能真正释放。当然实际项目中你可能不会这么做,因为既然是@property,那么你一定是希望它的生命一直持续到本实例消亡为止。
1 @property (nonatomic, strong)ARCTestView *testView;
这就是为什么文章开头说的“一大部分”,因为在有些(很少的)地方,你还是得自己setNil。
1 - (void)removeTestView:(id)sender 2 { 3 [self.testView removeFromSuperview]; 4 self.testView = nil; 5 }
二:delegate回调时,删除一个UIView子类,正确释放内存
我创建了一个新的类叫SubView,声明了一个SubViewDelegate(里面没有任何东西)
在开发中,我们经常给界面设置回调,本界面只处理本界面的事情,数据获取,或是事件处理都以回调的形式让上层做处理。这是iOS开发中很常见的设计。在MRC中,我和我周围的人习惯性的会重写一个自定义View的初始化方法,在初始化的时候,就将subView的delegate设好。而不是在创建好后再用类似someView.delegate = self;来设置回调对象。这样做的好处是有两个,一是整洁了代码(个人认为),二是可以在subView的初始化方法中直接调用回调。
1 @interface SubView : UIView 2 { 3 id<SubViewDelegate> _delegate; 4 }
1 - (id)initWithFrame:(CGRect)frame withDelegate:(id<SubViewDelegate>)delegate;
1 @implementation SubView 2 3 - (id)initWithFrame:(CGRect)frame withDelegate:(id<SubViewDelegate>)delegate 4 { 5 self = [super initWithFrame:frame]; 6 if (self) { 7 // Initialization code 8 _delegate = delegate; 9 } 10 return self; 11 }
在MRC中,默认情况下,id<SubViewDelegate> _delegate;就是一个一般的指针,当你在初始化方法中使用_delegate = delegate;不会对delegate(上层ARCTestView)进行任何retain操作,因此没有任何潜在的问题。但是,问题来了,如果现在变成了ARC,那这个_delegate会被解读为Strong类型,所以delegate(上层ARCTestView)就多了一个Strong指针指向它。从而在viewController中删除上册界面ARCTestView时,由于上册界面ARCTestView的subView内有一个Strong指向ARCTestView,因此ARCTestView不会被释放!呵呵有些绕吧?打个比方,就是儿子用Strong把爸爸劫持了,爸爸不能出去玩了!请结合代码,在把我写的这段话看一遍。
那么解决的方法是什么呢?有两种:
1,如果一定要使用自定义的初始化方法,将_delegate = delegate,那么请这样声明你的_delegate:
1 @interface SubView : UIView 2 { 3 __weak id<SubViewDelegate> _delegate; 4 }
对了就是加上__weak关键字,这样显示的表明,_delegate是个weak指针,不会持有“爸爸”即ARCTestView。
2,使用原生的初始化方法,然后将delegate声明为weak的@property
1 @property (nonatomic, weak)id<SubViewDelegate> delegate;
其实以上两种方法并不互斥,你可以使用自定义的初始化方法,同时也讲delegate设置为@property,但不管怎么样切记一定要是weak!
这样,当我点击蓝色按钮时,界面被删除,同时进入dealloc函数,表明释放正确!
有人说,为什么不用assign呢?呵呵,你当然可以用,不过assign是MRC年代的东西了。ARC使用strong和weak。而且,还记得吗,weak指针可以自动设置为nil哦,assign确没有这个特性。
好了,总结一下本文:
1,请看一下http://onevcat.com/2012/06/arc-hand-by-hand/ 真的是大牛的好文章!
2,我对ARC的理解
1,一个对象,只要有大于或等于一个Strong类型指针指向它,该对象就不会被释放;2,一个对象,假如只剩下一个或多个weak指针指向它,那么对不起,该对象就自动被释放, 然后这些一个或多个weak指针都会被自动设置为nil;3,默认条件下,指针都是Strong,除非你特别声明,如在最前面加__weak;4,使用ARC,不代表和内存管理完全拜拜了, 你仅仅不需要retain、release、autorelease、[super dealloc],但是在必要的时候,还是得向一个NSObject setNil,才能完成对内存的释放(如第一个例子中的self.textView = nil;)。
3,使用delegate模式时一定要注意,weak的使用,不然你的界面是不能正确释放的!从MRC转过来的“半老手”一定要注意!
写的不对的地方请多包涵,并帮忙指出,谢谢!