关于performSelector:系列API实现存在的问题解读

先来看一个问题:performSelector:withObject:afterDelay:在子线程(没有主动开启runloop)执行,其中的selector方法是否会被执行?该方法和performSelector:withObject:作对比,那么performSelector:withObject:在不添加到子线程的Runloop中时是否能执行?

大多数人可能会有这样的考虑:performSelector:withObject:方法和延迟方法类似,只不过是马上执行而已,所以也需要添加到子线程的RunLoop中。
其实不需要,原因如下:perfromSelector:withObject:只是普通的消息发送
1)关于NSObject下的解释:

The performSelector: method is equivalent to sending an aSelectormessage directly to the receiver. For example, the following messages all do the same thing:

id aClone = [anObject copy];
id aClone = [anObject performSelector:@selector(copy)];
id aClone = [anObject performSelector:sel\_getUid("copy")];

The performSelector: method allows you to send messages that aren’t determined until run-time. This means that you can pass a variable selector as the argument:

SEL aSelector = findTheAppropriateSelectorForTheCurrentSituation();
id returnedObject = [anObject performSelector:aSelector];

首先这个方法是在运行时直接调用的,在编译阶段不会进行语法检查。其次:这个方法实质就是直接发送了一个消息。跟runloop下的performSelector 是不同的,runloop分类中定义的方法,是需要添加到runloop中才会执行。(前提是runloop存在且包含任意time or model or source 。这也是为什么:perfromSelector:withObject:afterDely:在子线程中不一定执行的原因!)而performSelector:withObject:在子线程中不受限制。

2)总结:
大家基本都使用过performSelector:系列的API,但是在使用时能否分清它们的异同却未可知:
我们来看下perfromSelector:这个系列的API,总共有下面三类:
performSelector 系列API 分别位于不同的分类中:

  1. performSelector:withObject: (带有返回值的)是位于NSObject.h 下
    NSObject.h文件下
  1. performSelector:withObject:afterDelay: (无返回值,可用来delay执行的)是位于NSRunloop.h文件下。
    NSRunloop
  1. performSelector: onThread: withObject: (无返回值,但可在不同线程中执行)位于NSThread.h文件下。
    NSThread.h文件下

performSelectorOnMainThread: withObject: waitUntilDone:(无返回值)
Runloop有关系的只有第二个分类中添加的方法,而NSObject分类下的performSelector下的方法跟Runloop没有必然关系,调用后就是一次普通的消息发送。

3)警告
NSObject分类下perfromSelector API的作用通常是为了跳过编译器的校验,在运行期直接调用selector方法。但是同样的也会产生警告!


提示可能存在内存泄漏

通常有以下几种方式来消掉警告:
1)使用宏

#pragma clang diagnostic push
#pragma clang diagnostic ignored "-Warc-performSelector-leaks"
//code
 #pragma clang diagnostic pop

缺点:这种方式是最暴力简单的方式,任何警告都不应该直接忽略,应该分析下警告出现的原因,消除掉警告!

2) 使用runloop 分类下的perfromSelector:替换:

[self performSelector:@selector(xxxx) withObject:nil afterDelay:0]; // 1
[ss performSelectorOnMainThread:NSSelectorFromString(@"copy") withObject:nil waitUntilDone:NO]; //2

其中2是Apple文档中给出的建议:

To avoid the warning, if you know that a<wbr>Selector has no return value, you might be able to use performSelector:OnMainThread:withObject:waitUntilDone: or one of the related methods available in NSObject

缺点:这种方式仅局限于不需要使用方法返回值的情况。

3)使用runtime方式去除警告:

//runtime 去除警告
    SEL selector = NSSelectorFromString(@"testSelector:");
    IMP imp = [self methodForSelector:selector];
    void(*customFunc)(id,SEL,NSString *) = (void *)imp;
    customFunc(self,selector,@"参数1");

4)Apple还推荐使用NSInvocation方式(这种方式既可以传递多个参数,也可以使用返回值),这里不再赘述。

提示内存泄漏的原因:
关于内存泄漏问题Apple官方文档也有简单描述:

内存管理策略中有一条是:谁创建谁管理。假入我们调用的selector为copy等会创建实例的方法。而且这个操作在编译期间是无法通过ARC来实现内存管理的(也就是说创建的对象没有被进行自动内存管理),这时是存在内存泄漏的可能的。所以会给出内存泄漏的提示!

但这里推荐stackoverflow上高赞的一个解答:performSelector may cause a leak because its selector is unknown.很透彻的讲解了产生警告的原因以及如何避免该类问题。
(注:关于ARC下对于返回值内存管理和编译器添加的优化,会在另一篇博客中讲)

内存泄漏情形总结

结论:selectoralloc\new\copymutableCopy等创建对象的方法时,会出现内存泄漏。

1)例如下面调用new代码会导致出现“内存泄漏”:

[NSObject performSelector:@selector(new) withObject:nil];

我们可以看下最终的汇编结果:


存在内存泄露调用

上图中我们可以看到并没有objc_release()的调用。创建的对象没有被释放掉,我们看下正常情况下汇编后的结果(可以看到最后是有callq objc_release 操作):

[NSObject new];
正常调用汇编结果.png

我们再看下使用performSelector调用new方法创建对象时的内存图:(可以看到方法执行完后,内存中仍然存在一个对象未被释放)

内存图

2)alloc方法同上,这里不再赘述!

3)调用copy\mutableCopy方法

先来看下下面的代码:

Person *pp = [Person new];
[pp performSelector:NSSelectorFromString(@"copy")];//直接使用@selector(copy)会报编译错误

最终汇编结果如下:


存在内存泄露的汇编

我们在汇编完成后的结果中没有找到objc_release()函数,这时对象在创建后将会无法释放。
这时查看内存图,如下(可以看到person对象仍然停在内存中):

内存图

综上可以看出调用:使用performSelector系列API调用alloc\new\copy\mutableCopy等创建对象的方法时会导致存在内存泄漏,我们应该禁止使用performSelector系列API时调用此类方法!

补充说明:这里有一个特例对于字符串类型,执行[ss performSelector:NSSelectorFromString(@"copy")];不会产生内存泄漏,分析原因可能跟系统对字符串做特殊的优化处理有关!(有更好的解答,欢迎告知分享!)

perfromSelector:afterDelay:使用的注意事项

1、在子线程执行perfromSelector: afterDelay:selector会在对应的子线程被调用.

2、子线程执行perfromSelector: afterDelay:存在selector不被执行的问题。
原因:该API的实质是要执行的selector以message方式交给当前线程的Runloop。由Runloop来执行该selector,如果此时执行函数的子线程没有主动开启Runloop,selector就不会诶执行。

3、使用了NSRunloop.h下的performSelector相关API后,需要在dealloc中执行cancle移除掉未执行的selector,(也是performSelector系列API中唯一有cancle功能的API.) 防止出现内存泄漏或者crash。

NSObject下的带有返回值的performSelector:
1、函数的返回值为结构体时

OC的函数调用,当方法的返回值是结构体时Runtime调用的不是objc_msgSend 而是使用objc_msgSend_stret

官方文档中对此的描述为:

Methods that have data structures as return values
are sent using objc_msgSendSuper_stret and objc_msgSend_stret.

而performSelector:在运行时会转换成objc_msgSend()调用,如果此时方法的返回值为结构体,而这时通过objc_msgSend()来调用,编译器会认为返回值为为一个对象类型,因此会为返回值添加优化: objc_retainAutoreleasedReturnValue。

我们用demo来看下优化处理得到的最终汇编结果:

id rect = [self performSelector:@selector(testPerformSelector) withObject:nil];

- (CGRect)testPerformSelector {
    return CGRectMake(0, 0, 0, 0);
}

上述代码经过编译优化后的结果如下:


汇编后结果.png

可以看到编译器添加objc_retainAutoreleaseReturnValue函数
下面来看下该函数的实现:


// Accept a value returned through a +0 autoreleasing convention for use at +1.

id objc_retainAutoreleasedReturnValue(id obj)
{
    if (acceptOptimizedReturn() == ReturnAtPlus1) return obj; //1
    return objc_retain(obj);
}

我们看到有一个判断acceptOptimizedReturn()检查该返回值是否已经优化过,若果是直接返回对象,否则将返回值做retain操作!
通过编译运行源码进行断点调试,以及查看上面的汇编结果可以确定:在此之前返回值并没有进过优化,所以会执行objc_retain()操作。
这时因为返回值是一个结构体(非对象类型)类型,因此会crash!!

(注:有关 acceptOptimizedReturn() 以及 objc_autoreleaseReturnValue() 实现会在其他博客中详细讲解!)

2、执行返回值为结构的函数崩溃的不同现象分析

函数返回值为结构体类型时,接受返回值不接收返回值崩溃的原因是不同的:
1)接受返回值情形:(id xx = [self performSelector:@selector(testStruct) withObject:nil];)

crash原因是:编译器为xx添加了objc_retainAutoreleased优化操作,在其内部对xx做了retain导致crash。

2)不接收返回值:[self performSelector:@selector(testStruct) withObject:nil];

crash原因是:函数返回值被认为是一个对象,所以会在其不被使用的时候会调用objc_release()对其释放,而事实上返回值是一个结构体,因此crash。(暂且这么解释!)
这种情况下的崩溃位置并不在函数调用处,很不利于排查问题!
我们看图来佐证一下:

在普通VC中调用某个类中的方法:


调用该类中的方法

通过以上截图可以看到可以看到崩溃的位置是在外面的函数中,而不是执行performSelector处。

再来看下调用的堆栈:

3.png

可以看到堆栈也只是定位到最外层。如果我们线上项目有类似代码被执行并且产生了崩溃,依靠上报的崩溃日志想要精确定位到出问题得地方是很困难的。

再来看下运行源码得到的崩溃堆栈(分析下原因):


可以看到是调用performSelector的对象释放时出现crash。这里我们看一下是哪个对象的释放引起crash。

而这个 0x102423f70 是什么呢?打印一下看:

竟然是调用“返回结构体函数”的对象,这里本人也有点奇怪,为什么会在释放这个对象时产生crash??? 实在没有想通,如有了解的同学请一定要告知下。感谢

结论:
1、不要使用performSelector 调用返回值为结构体的函数。(出现问题不好定位)
2、函数返回值为非对象类型的,也不要使用使用perfromSelector调用。

3、拓展:

既然函数返回值为结构体时会产生crash,那如果返回值是“其他非对象类型”时是否会产生crash呢?(这里直接说结论)

1)不接收函数返回值时:(ex:[self performSelector:@selector(testPerformselector) withObject:nil])

通过performSelector调用返回值为intfloatbool 类型的函数时,不会发生crash。

2)接收返回值时:(ex: id xx = [self performSelector:@selector(testPerformselector): withObject:nil])

通过performSelector调用返回值为int、bool 函数会crash,调用 返回值为float类型的函数 不会产生crash!

调用返回值为float的函数会有两种情况:
在源码编译运行,会发现得到的返回值xx是函数的调用对象。但是普通demo上测试 xx是null。这里为什么是这样,作者也是没想通(可能还是欠缺某些关联知识点,有了解的同学希望留言告知)

4、关于OC消息发送的不同情况:

oc的消息发送并不都是转化为objc_msgSend,编译器会根据不同情况选择不同方法:
1)当返回值为结构体时,会使用objc_msgSend_stret:(当返回值为较大的结构体时,无法使用寄存器来传递返回值,而是通过栈来传递。)
2 )返回浮点数时,还会调用objc_msgSend_fpret(objc_msgSend_fp2ret)
3 )给父类发送消息,会调用objc_msgSendSuper

参考:
perfromSelector一点注意
stackoverflow-perfromSelector-warning
perfromSelector内存泄漏问题

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