美文网首页iOS CollectioniOSiOS开发
[爆栈热门 iOS 问题] performSelector ma

[爆栈热门 iOS 问题] performSelector ma

作者: 戴仓薯 | 来源:发表于2016-02-14 16:42 被阅读6030次

    系列文集:爆栈热门 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 返回值

    相关文章

      网友评论

      • BlueMouse:666, 楼主翻译地非常好,支持一下
      • 天下无贼_:我能问一下 SuppressPerformSelectorLeakWarning 这个宏,为什么要写do-while吗:grin: :grin: 这个不太懂
        visual_:一般宏里面有代码块的时候都这么写吧,以防使用者出现意料之外的情况
      • keshiim:cool
      • Natsusama:博主你好,当我调用的是“copy”等类似的会使引用计数加1的方法时,在Instruments工具的检测下,会发现还是存在内存泄漏,请问有什么好的解决方法吗?
        755f3c191771:返回int型也是避免不了内存泄漏。
        Natsusama:@戴仓薯 谢谢
        戴仓薯:@Natsusama 就我所知,没有好的解决方法。我从来不用 @selector 调用 init 和 copy 方法,建议尽量避免。
      • fengshaobo:加油

      本文标题:[爆栈热门 iOS 问题] performSelector ma

      本文链接:https://www.haomeiwen.com/subject/cjyocttx.html