在开发过程中,遇到这样一个编译器警告:
图片可能看不清,源代码如下:
[self performSelector:NSSelectorFromString(item[@"kAction"])];
这行代码会引出一个Warning:performSelector may cause a leak because its selector is unknown
其实解决办法,百度一下就有很多,也确实能通过一些奇技淫巧来解决这个问题。问题的根本原因解释却很少,最后还是找了一篇stackoverflow上的的文章做了学习。
以下内容参考stackoverflow原文,并加上了一些自己的理解:
警告原因
这个警告会出现在ARC中,与内存管理有关系。runtime需要知道运行这行代码后,方法的返回值是什么,是void无返回值还是NSString*返回字符串等,情况很多。通常来说,ARC会通过方法声明来获取到这些信息,然后去进行相应内存管理。
ARC在处理方法的返回值时有以下四种情况:(需要学习MRC的一些相关知识)
- 1 忽略无返回值或返回值不是一个指针对象的情况。
- 2 不retain返回值(不做引用计数处理),如果没有对象引用该返回值,返回值release(以NS_RETURNS_NOT_RETAINED做标识)。
- 3 retain返回值(引用计数+1),用于init、coFR5py家族方法或者标记有NS_RETURNS_RETAINED的方法)
- 4 标记为autorelease,放入一个autoreleasepool中,并且假设对象在某个范围内都不会释放(方法以NS_RETURNS_ATUORELEASED做标识,自动释放池释放后,进行autorelease操作)
以上四种情况翻译的不太好,这块内容也不是非常理解。看了objc arc的简单探索学习了下,再回头解释几个概念。
Method family
An Objective-C method may fall into a method family, which is a conventional set of behaviors ascribed to it by the Cocoa conventions.
指的是命名上表示一类型的方法,比如- init和- initWithMark:都属于init的family,同样类似的还有copy,mutableCopy,new等。
ns_returned__
在ARC中用来标记方法的返回值内存管理类型,有以下三种:
#define NS_RETURNS_RETAINED __attribute__((ns_returns_retained))
#define NS_RETURNS_NOT_RETAINED __attribute__((ns_returns_not_retained))
#define NS_RETURNS_INNER_POINTER __attribute__((objc_returns_inner_pointer))
一般不需要我们自己对方法进行内存管理的定义,以上定义对应的使用情况也在前文做了列举。
根据这个标记,其实在编译器眼里,我们声明的方法都是长这样的:
@interface Sark : NSObject
- (instancetype)doSomeThing NS_RETURNS_NOT_RETAINED; // 1
- (instancetype)initWithMark:(NSString *)mark NS_RETURNS_RETAINED; // 2
@end
这也就是为什么在ARC中用new开头定义属性是不行的,编译器会报错:
@property (nonatomic, strong) NSString *newName;
因为在编译器眼里,new开头的方法作为new家族的方法,内存管理需要区分对待,如此声明编译器并不能懂。
对于NS_RETURNS_INNER_POINTER:
对于NS_RETURNS_INNER_POINTER这个标识,主要使用在返回的是一个对象的内部C指针的情况,
如
NSString的方法:
- (__strong const char *)UTF8String NS_RETURNS_INNER_POINTER;
中途学习结束,再回头研究一下警告原因:
那行报警告的代码,编译器并不能判断我们调用方法的返回类型,ARC也不能够准确的进行上述四种内存管理情况的判断,所以默认会按照情况2进行非retain/release处理,但是如果我们调用的方法是init家族或者copy家族这种返回一个新对象的方法时,系统仍然按照非retain/release处理,那么虽然该方法的调用结果是开辟了一块内存空间,但是我们已经无法再去释放这块空间了,因为没有任何指针指向这块空间。
另外对于无返回值(void)的,或者返回值不是一个指针对象的方法,尽管我们可以忽略编译器的警告,但是仍然有些危险。
比如当用这行代码调用一个无返回值的方法时,并且使用一个指针试图这个返回值的时候会发生崩溃,如下:
NSNumber *aNumber = [self performSelector:NSSelectorFromString(@"someVoidMethod") withObject:nil];
NSLog(@"%@",aNumber);
- (CGFloat )someFloatMethod{
return 5.f;
}
综上,编译器报警告“May Cause a Leak”是对的。
解决办法
方法一:
if (!_controller) { return; }
SEL selector = NSSelectorFromString(@"someMethod");
IMP imp = [_controller methodForSelector:selector];
void (*func)(id, SEL) = (void *)imp;
func(_controller, selector);
或者简洁一点但是稍显复杂:
SEL selector = NSSelectorFromString(@"someMethod");
((void (*)(id, SEL))[_controller methodForSelector:selector])(_controller, selector);
简单来说就是自己完善一个方法的完整信息,包括方法名,方法的实现和方法的返回值。以便让编译器知道我们自己清楚正在使用什么样的方法来规避警告。
方法二:
如果你清楚你在做什么,可以直接使用#pragma clang来忽视编译器的警告:
#pragma clang diagnostic push
#pragma clang diagnostic ignored "-Warc-performSelector-leaks"
[self performSelector:NSSelectorFromString(item[@"kAction"])];
#pragma clang diagnostic pop
为了后续使用方便,也可以定义一个宏函数:
#define SuppressPerformSelectorLeakWarning(Stuff) \
do { \
_Pragma("clang diagnostic push") \
_Pragma("clang diagnostic ignored \"-Warc-performSelector-leaks\"") \
Stuff; \
_Pragma("clang diagnostic pop") \
} while (0)
用的时候:
SuppressPerformSelectorLeakWarning(
[self performSelector:NSSelectorFromString(item[@"kAction"])]
);
需要返回值则如下:
id result;
SuppressPerformSelectorLeakWarning(
result = [self performSelector:NSSelectorFromString(item[@"kAction"])]
);
方法三:
投机取巧的方法,当方法无返回值的时候并且你并不在意方法在下一个Runloop中执行(runloop这块我也不是很懂),可以采用下面这一种方法:
[self performSelector:NSSelectorFromString(item[@"kAction"]) withObject:nil afterDelay:0];
个人感觉是一种类似旁门左道的方法,不是很推荐使用。
总得来说,遇到这个警告,如果你知道内存关系,那么其实无视警告也可以。但是作为严谨的程序员,还是要采用一种方法去解决它,解决时候也是一样的道理,清楚内存关系。
方法一相对来说比较麻烦,但是手动操作一遍可以理顺这个方法的返回值,SEL,IMP等信息,也不失为一次思想上的避险。
方法二、三纯粹是为了消除警告,前提是确保无视之后不会引起内存问题。