[爆栈热门 iOS 问题] performSelector may cause a leak because its selector is unknown

系列文集:爆栈热门 iOS 问题目录在此。仓薯翻译,欢迎指正:)

问题

我在 ARC 模式下编译出了这个 warning:

"performSelector may cause a leak because its selector is unknown".

我的代码是这么写的:

[_controller performSelector:NSSelectorFromString(@"someMethod")];

为什么会有这个 warning 呢?我知道编译器无法检查实际上有没有这个 selector,不过这为什么会造成内存泄漏呢?代码应该怎么改才能消除这个 warning?


答案

答案1:单纯消除 warning

Scott Thompson,1100 票

LLVM 3.0 编译器可以用以下代码消除 warning:

#pragma clang diagnostic push
#pragma clang diagnostic ignored "-Warc-performSelector-leaks" [self.ticketTarget performSelector: self.ticketAction withObject: self];
#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(
    [_target performSelector:_action withObject:self]
);

如果需要返回值:

id result;
SuppressPerformSelectorLeakWarning(
    result = [_target performSelector:_action withObject:self]
);

答案2:详细解释和正统解决

wbyoung,768 赞

解决方案

编译器报这个 warning 是有原因的,一般不应该直接忽略,而且消除这个 warning 并不难。如下即可:

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);

代码解释

这一堆代码在做的事情其实是,向 controller 请求那个方法对应的 C 函数指针。所有的NSObject都能响应methodForSelector:这个方法,不过也可以用 Objective-C runtime 里的class_getMethodImplementation(只在 protocol 的情况下有用,id<SomeProto>这样的)。这种函数指针叫做IMP,就是typedef过的函数指针(id (*IMP)(id, SEL, ...)[1])。它跟方法签名(signature)比较像,虽然可能不是完全一样。

得到IMP之后,还需要进行转换,转换后的函数指针包含 ARC 所需的那些细节(比如每个 OC 方法调用都有的两个隐藏参数self_cmd)。这就是代码第 4 行干的事(右边的那个(void *)只是告诉编译器,不用报类型强转的 warning)。

最后一步,调用函数指针[2]

更复杂的例子

如果 selector 接收参数,或者有返回值,代码就需要改改:

SEL selector = NSSelectorFromString(@"processRegion:ofView:");
IMP imp = [_controller methodForSelector:selector];
CGRect (*func)(id, SEL, CGRect, UIView *) = (void *)imp;
CGRect result = _controller ?
  func(_controller, selector, someRect, someView) : CGRectZero;

为什么会有这个 warning

原因是这样的:我们在 ARC 下调一个方法,runtime 需要知道对于返回值该怎么办。返回值可能有各种类型:voidintcharNSString *id等等。ARC 一般是根据返回值的头文件来决定该怎么办的[3],一共有以下 4 种情况[4]

  1. 直接忽略(如果是基本类型比如 voidint这样的)。
  2. 把返回值先 retain,等到用不到的时候再 release(最常见的情况)。
  3. 不 retain,等到用不到的时候直接 release(用于 initcopy 这一类的方法,或者标注ns_returns_retained的方法)。
  4. 什么也不做,默认返回值在返回前后是始终有效的(一直到最近的 release pool 结束为止,用于标注ns_returns_autoreleased的方法)。

而调performSelector:的时候,系统会默认返回值并不是基本类型,但也不会 retain、release,也就是默认采取第 4 种做法。所以如果那个方法本来应该属于前 3 种情况,都有可能会造成内存泄漏。

对于返回void或者基本类型的方法,就目前而言你可以忽略这个 warning,但这样做不一定安全。我看过 Clang 在处理返回值这块儿的几次迭代演进。一旦开着 ARC,编译器会觉得从performSelector:返回的对象没理由不能 retain,不能 release。在编译器眼里,它就是个对象。所以,如果返回值是基本类型或者void,编译器还是存在会 retain、release 它的可能,然后直接导致 crash。

带参数调用

类似地,performSelector:withObject:也会报同一个 warning,因为不指明怎么处理参数也会有同样的问题。ARC 允许为方法参数标注consumed,如果你调的方法有这种标注,最终可能导致把消息发给僵尸对象然后 crash。要解决这个问题可以用桥接(bridged casting),但是最好最简单的方法还是我上面写的用IMP和函数指针的方法。不过给参数标 consumed 是比较少见的,所以这个问题也不容易发生。

静态 selector

有趣的是,下面这种静态声明的 selector 就不会出 warning:

[_controller performSelector:@selector(someMethod)];

原因是,这种情况下编译器就能在编译阶段得到关于这个 selector 的全部信息,不需要默认任何事情。

仓薯注:后面作者还写了点历史,我就没有翻译了,感兴趣请前往原文阅读。


原文地址:performSelector may cause a leak because its selector is unknown

本文地址:http://www.jianshu.com/p/6517ab655be7

系列文集:爆栈热门 iOS 问题

译者:@戴仓薯


  1. 所有的 Objective-C 方法都有两个隐藏的参数,self_cmd,调用时自动加的。

  2. 在 C 里调用NULL方法是不安全的。而if (!_controller) { return; }这一句保证controller不为空,所以我们一定能从methodForSelector:得到一个IMP(虽然可能只是_objc_msgForward,进入消息转发系统)。基本上,有了这行检查,就能保证我们有方法可调。

  3. 实际上,如果返回值的类型是id,而你又没 import 对应的头文件,它是有可能做出错误处理的。有可能会 crash 在一块编译器以为安全的代码里。这种情况很罕见,但还是有发生的可能。一般来说,如果编译器不知道该选哪个方法签名,它会报一个 warning 的。

  4. 更多细节请参考 ARC 的文档 retain 返回值不 retain 返回值

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 158,847评论 4 362
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 67,208评论 1 292
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 108,587评论 0 243
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 43,942评论 0 205
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,332评论 3 287
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,587评论 1 218
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,853评论 2 312
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,568评论 0 198
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,273评论 1 242
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,542评论 2 246
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,033评论 1 260
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,373评论 2 253
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 33,031评论 3 236
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,073评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,830评论 0 195
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,628评论 2 274
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,537评论 2 269

推荐阅读更多精彩内容

  • 转至元数据结尾创建: 董潇伟,最新修改于: 十二月 23, 2016 转至元数据起始第一章:isa和Class一....
    40c0490e5268阅读 1,635评论 0 9
  • 问题描述 项目中使用到了从字符串创建选择器,编译时发现警告:"performSelector may cause ...
    ttdiOS阅读 544评论 0 1
  • __block和__weak修饰符的区别其实是挺明显的:1.__block不管是ARC还是MRC模式下都可以使用,...
    LZM轮回阅读 3,204评论 0 6
  • *面试心声:其实这些题本人都没怎么背,但是在上海 两周半 面了大约10家 收到差不多3个offer,总结起来就是把...
    Dove_iOS阅读 27,036评论 29 470
  • 火车站,多的是一些离别的人和故事。记得大学毕业那年,我的大学闺蜜kk到火车站送我。本以为会上演一幕生离死别、泪水涟...
    七萧萧阅读 432评论 0 1