iOS之mrc转arc
一、前言
项目简介
需要转换的Objective-C文件数量:1000个左右。
开发工具:Xcode 8.0.1
转换方式
我使用的是Xcode本身提供的ARC转换功能。当然你也可以手动手动转换,那不属于本文范畴,而且其工作量绝对能让你崩溃。
二、转换过程
代码备份
在进行如此大规模的更改之前,一定要先进行代码备份:直接在本地将代码复制一份,或者记住更改前代码在VCS上的版本号。
过滤无需转换的文件
找出项目中引用的仍使用手动内存管理的第三方库,或者某些你不希望转换的文件,对其添加-fno-objc-arc标记。
Xcode自动转换工具只针对Objective-C对象,只会处理Objective-C/Objective-C++即后缀名为.m/.mm的两种文件,因此其他的C/C++对应的.c/.cpp都无需理会。
执行检查操作
使用Xcode转换工具入口如图所示:
点击Convert to Objective-C ARC后会进入检查操作入口,如图:
该步骤要选择哪些文件需要转换,Xcode会自动帮你识别哪些文件需要转换,这里可以全选。
点击check按钮后Xcode会帮助我们检查代码中存在的不符合ARC使用规则的错误或警告,只有所有的错误都解决以后才能执行真正的转换操作。
解决错误/告警
执行完check操作后,会给出提示:
三百多个错误,同时还有一千两百多个警告信息,都要哭了。。。
错误和警告的解决内容较多,后面会单独介绍。
执行转换操作
解决完所有的error后,会弹出下述提示界面:
大意是Xcode将要将你的工程转换成使用ARC管理内存,所有更改的代码在真正更改之前会在一个review界面展示。同时所有的更改完成以后,Xcode会讲项目Target对应的工程设置的使用ARC设置(Objective-C Automatic Reference Counting)会被置成YES(上图右上角的警告标识就是在告诉我们项目已经支持ARC了,但工程中有文件还不支持):
这时离成功就不远了,胜利在望!
点击next按钮后跳转到review界面,样式类似于使用Xcode提交SVN的确认提交界面,如下图所示:
该界面列出了所有需要有代码更改的文件,同时能够直接对比转换前和转换后的代码变化。为了稳妥起见,我选择了每个文件都点进去扫了一眼,这也给我们一次机会检查是否漏掉了不能转换的文件。确定一切无误以后,点击右下角的save按钮,一切就大功告成了!
错误/警告解决
错误
ARC forbids synthesizing a property of an Objective-C object with unspecified ownership or storage attribute
property属性必须指定一个内存管理关键字,在属性定义处增加strong关键字即可。
ARC forbids explicit message send of ‘release’
这种情况通常是使用包含release的宏定义,将该宏和使用该宏的地方删除即可。
Init methods must return a type related to the receiver type
错误原因是A类里的一个方法以init开头,而且返回的是B类型,好吧,乖乖改方法名。
Cast of C pointer type ‘ivPointer’ (aka ‘void ’) to Objective-C pointer type ‘iFlyTTSManager_old ’ requires a bridged cast
cast_pointer_objective-c
这是Toll-Free Bridging转换问题,在ARC下根据情况使用对应的转换关键字就行了,后文会专门介绍。
警告
解决警告的目的是消除警告处代码存在的隐患,既然Xcode给了提示,那么每一个警告信息都值得我们认真对待。
Capturing self in this block is likely to lead to a retain cycle
这是典型的block循环引用问题,将block中的self改成使用指向self的weak指针即可。
Using ‘initWithArray:’ with a literal is redundant
好吧,原来是没必要的alloc操作,直接按Xcode提示将alloc删除即可:
Init methods must return a type related to the receiver type
原来是A类里的一个方法以init开头,而且返回的是B类型,好吧,乖乖改方法名。
Property follows Cocoa naming convention for returning ‘owned’ objects
这是因为@property属性的命名以new开头了,可恶。。。修改方法是将对应的getter方法改成非new开头命名的:
ARC下方法名如果是以new/alloc/init等开头的,而且还不是类的初始化方法,就该小心了,要么报错,要么警告,原因你懂的。
Block implicitly retains ‘self’; explicitly mention ‘self’ to indicate this is intended behavior
意思是block中使用了self的实例变量_selectedModeMarkerView,因此block会隐式的retain住self。Xcode认为这可能会给开发者造成困惑,或者因此而因袭循环引用,所以警告我们要显示的在block中使用self,以达到block显示retain住self的目的。
该警告有两种改法: ①按照Xcode提示,改成self->_selectedModeMarkerView:
②直接将该警告关闭 警告名称为:Implicit retain of ‘self’ within blocks 对应的Clang关键字是:-Wimplicit-retain-self
Weak property may be unpredictably set to nil 和 Weak property ‘delegate’ is accessed multiple times in this method but may be unpredictably set to nil; assign to a strong variable to keep the object alive
这是工程中数目最多的警告,这是因为所有的delegate属性都是weak的,Xcode默认开启了下图中的两个警告设置,将其关闭即可:
Capturing ‘self’ strongly in this block is likely to lead to a retain cycle
这是明显的block导致循环引用内存泄露的情况,之前代码中坑啊!修改方案:
Method parameter of type ‘NSError __autoreleasing ’ with no explicit ownership
这种就不用说了,按警告中的提示添加__autoreleasing关键字即可。
以上列出的错误和警告只是数量较多的,还有很多其他就不在这里一一列举了。
另外,推荐 Mattt Thompson 大神关于Clang中几乎所有warning的名称和对应报错提示语的网站:http://fuckingclangwarnings.com/,以后解决warning类问题就简单多了!
Xcode自动转换
关键字转换
Xcode会自动将某些关键字自动转换成ARC的对应版本。
retain自动转成strong,如图:
assign关键字转成weak
修饰Objective-C对象或者id类型对象的assign关键字会被转成weak,如图:
但是修饰Int/bool等数值型变量的assign不会自动转换成weak,如图:
关键字删除
和手动内存管理相关的几个关键字,比如:release/retain/autorelease/super dealloc等会被删除;
dealloc方法中如果除了release/super dealloc语句外,如果别的代码,dealloc方法会保留,如图:
如果没有整个方法都会被删除:
关键字替换
在转换时block关键字会被自动替换成weak:
@autoreleasepool
NSAutoreleasePool不支持ARC,会被替换成@autoreleasepool:
关于被宏注释代码
使用宏定义的对象释放代码
宏定义如下所示:
1
2
|
#define RELEASE_SAFELY(__POINTER) { \ [(__POINTER) release]; (__POINTER) = nil; } |
在执行ARC转换检查操作时,Xcode会在使用该宏的地方报错:
将该宏和使用该宏的地方删除即可。
被宏注释掉的代码,Xcode在转换时是不会处理的,如图:
PS:这是相当坑的一点,因为你根本预料不到工程中使用了多少宏,注释掉了多少代码。当你执行完转换操作,以为就大功告成的时候,却在某天因为一个宏的开启遇到了一堆新的转ARC不彻底的问题。这种问题也没招,只能遇到一个改一个了。
ARC和block
不管是手动内存管理还是ARC,block循环引用导致的内存泄露都是一个令人头疼的问题。在MRC中,解决block循环引用只需要使用__block关键字,在ARC下解决与block的使用就略显复杂了:
__block关键字
block内修改外部定义变量
和手动内存管理一样,ARC如果在block中需要修改block之外定义的变量需要使用__block关键字修饰,比如:
1
2
3
4
|
__block NSString *name = @ "foggry" ; self.expireCostLabel.completionBlock = ^(){ name = @ "wangzz" ; }; |
上例中name变量需要在block中修改,因此必须使用__block关键字。
__block在MRC和ARC中的区别
在ARC下的block中使用__block关键字修饰的对象时,block会retain该对象;而在MRC下却不会retain。关于这点在官方文档Transitioning to ARC Release Notes中有详细的描述:
In manual reference counting mode, block id x; has the effect of not retaining x. In ARC mode, block id x; defaults to retaining x (just like all other values).
下面的代码不管在MRC还是ARC中myController对象都是有内存泄露的:
1
2
3
4
5
|
MyViewController *myController = [[MyViewController alloc] init…]; // ... myController.completionHandler = ^(NSInteger result) { [myController dismissViewControllerAnimated:YES completion:nil]; }; |
内存泄露问题在MRC中可以按如下方式更改:
1
2
3
4
5
|
MyViewController * __block myController = [[MyViewController alloc] init…]; // ... myController.completionHandler = ^(NSInteger result) { [myController dismissViewControllerAnimated:YES completion:nil]; }; |
然而在ARC中这么改就不行了。正如开始所说的那样,在ARC中myController.completionHandler的block会retainmyController对象,使得内存泄露问题仍然存在!!
在ARC中该问题有两种解决方案,第一种:
1
2
3
4
5
6
|
MyViewController * __block myController = [[MyViewController alloc] init…]; // ... myController.completionHandler = ^(NSInteger result) { [myController dismissViewControllerAnimated:YES completion:nil]; myController = nil; }; |
该方法在block中使用完myController时,是它指向nil。没有strong类型的指针指向myController指向的对象时,对象会被释放掉。
第二种种解决方案,直接使用weak代替block关键字:
1
2
3
4
5
6
|
MyViewController *myController = [[MyViewController alloc] init…]; // ... MyViewController * __weak weakMyViewController = myController; myController.completionHandler = ^(NSInteger result) { [weakMyViewController dismissViewControllerAnimated:YES completion:nil]; }; |
该方法直接避免了对block对myController对象的retain。
存在循环引用关系
如果self直接或者间接的对block存在强引用,在block中又需要使用self关键字,此时self和block就存在循环引用的关系。此时必须使用__weak关键字定义一个指针指向self,在block中使用该指针来引用self:
1
2
3
4
|
MessageListController * __weak weakSelf = self; self.messageLogic.loadMoreBlock = ^(IcarMessage * theMessage) { [weakSelf.tableView setPullTableIsLoadingMore:YES]; }; |
需要说明的是,尽管上例中weakSelf指针对self只是弱引用,但是self对block却是强引用,self的生命周期一定是长于block的,因此不用担心在block中使用weakSelf指针时,其指向的self会被释放掉。
不存在循环引用关系
下面的例子:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
|
MyViewController *myController = [[MyViewController alloc] init…]; // ... MyViewController * __weak weakMyController = myController; myController.completionHandler = ^(NSInteger result) { MyViewController *strongMyController = weakMyController; if (strongMyController) { // ... [strongMyController dismissViewControllerAnimated:YES completion:nil]; // ... } else { // Probably nothing... } }; |
如前面所说,myController.completionHandler的block中不能直接使用myController对象,会造成内存泄露,因此需要先用一个weak的指针指向myController对象,然后在block中使用该weak指针。但是为了确保在block执行的时候myController对象没有被释放掉,就在block一开始的地方定义了一个临时的strong类型的指针strongMyController指向weak指针weakMyController,其实最终的结果就是block中对myController对象强引用了。在block执行完被销毁的时候,strongMyController指针变量会被销毁,其最终指向的myController对象因此也会被销毁。这样在使用一个对象的时候做就保证了该对象是存在的,使用完了再放弃该对象的所有权。
ARC和Toll-Free Bridging
MRC下的Toll-FreeBridging不涉及内存管理的转移,Objective-C(后文简称OC)和Core Foundation(后文简称CF)各自管理各自的内存,相互之间可以直接交换使用,比如:
1
2
|
NSLocale *gbNSLocale = [[NSLocale alloc] initWithLocaleIdentifier:@ "en_GB" ]; CFLocaleRef gbCFLocale = (CFLocaleRef)gbNSLocale; |
而在ARC下,事情就会变得复杂一些。因为ARC能够管理OC对象的内存,却不能管理CF对象,CF对象依然需要我们手动管理内存。在CF和OC之间bridge对象的时候,问题就出现了,编译器不知道该如何处理这个同时有OC指针和CF指针指向的对象。这时候,需要使用__bridge, __bridge_retained, __bridge_transfer等修饰符来告诉编译器该如何去做。
__bridge
它告诉编译器仍然负责管理好在OC一端的引用计数的事情,开发者也继续负责管理好在CF一端的事情,比如:
1
2
3
4
|
CFStringRef cfString = CFStringCreateWithCString(kCFAllocatorDefault, "CFString" , kCFStringEncodingUTF8); NSString *ocString = (__bridge NSString *)cfString; CFRelease(cfString); NSLog(@ "%@" ,ocString); |
__bridge_retained 或 CFBridgingRetain
二者作用是一样的,只是用法不同。
告诉编译器需要retain对象,而开发者在CF一端负责释放。这样就算对象在OC一端被释放,只要开发者不释放CF一端的对象, 对象就不会被真的销毁。
1
2
3
4
5
6
|
NSArray *ocArray = [[NSArray alloc] initWithObjects:@ "foggry" , nil]; CFArrayRef cfArray = (__bridge_retained CFArrayRef)ocArray; /** 使用cfArray **/ CFRelease(cfArray); |
__bridge_transfer 或 CFBridgingRelease
二者作用也是一样的,只是用法不同。
该关键字告诉编译器bridge的同时,也转移了对象的所有权,比如:
1
2
3
4
|
CFStringRef cfString = CFStringCreateWithCString(kCFAllocatorDefault, "CFString" , kCFStringEncodingUTF8); NSString *ocString = (__bridge_transfer NSString *)cfString; //CFRelease(cfString); //不再需要释放操作 NSLog(@ "%@" ,ocString); |
转换过程中大家只需要根据具体需求选用适当的关键字即可。
另外,在ARC中id和void *也不能直接相互转换了,必须通过Toll-FreeBridging使用适当的关键字修饰。
ARC和IBOutLet
对于IBOutLet属性应该用strong还是weak一直都有疑惑。关于这一点官方文档是这么介绍的:
From a practical perspective, in iOS and OS X outlets should be defined as declared properties. Outlets should generally be weak, except for those from File’s Owner to top-level objects in a nib >>>file (or, in iOS, a storyboard scene) which should be strong. Outlets that you create should therefore typically be weak.
那么长的一段英文想说的是:如果nib文件构建的view是直接被Controller引用的顶层view,对应的IBOutLet属性应该是strong;
如果view是顶层view上的一个子view,那么该view的属性应该是weak,因为顶层view被Controller使用strong属性引用了,而顶层view本身又持有该view;
如果Controller对某个view需要单独引用,或者Controller没有引用某个view的父view,那么其属性也应该是strong。
好吧,其实我能说如果你实在懒得区分什么时候用strong,什么时候用weak,那就将所以后的IBOutLet属性都设成strong吧!在Controller销毁的时候,对应的IBOutLet实例变量也会被销毁,strong指针会被置成nil,因此也不会有内存问题。
参考文档
Transitioning to ARC Release Notes