關(guān)于performSelector:系列API實(shí)現(xiàn)存在的問題解讀

先來看一個(gè)問題:performSelector:withObject:afterDelay:在子線程(沒有主動(dòng)開啟runloop)執(zhí)行,其中的selector方法是否會(huì)被執(zhí)行?該方法和performSelector:withObject:作對(duì)比,那么performSelector:withObject:在不添加到子線程的Runloop中時(shí)是否能執(zhí)行?

大多數(shù)人可能會(huì)有這樣的考慮:performSelector:withObject:方法和延遲方法類似,只不過是馬上執(zhí)行而已,所以也需要添加到子線程的RunLoop中。
其實(shí)不需要,原因如下:perfromSelector:withObject:只是普通的消息發(fā)送
1)關(guān)于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];

首先這個(gè)方法是在運(yùn)行時(shí)直接調(diào)用的,在編譯階段不會(huì)進(jìn)行語法檢查。其次:這個(gè)方法實(shí)質(zhì)就是直接發(fā)送了一個(gè)消息。跟runloop下的performSelector 是不同的,runloop分類中定義的方法,是需要添加到runloop中才會(huì)執(zhí)行。(前提是runloop存在且包含任意time or model or source 。這也是為什么:perfromSelector:withObject:afterDely:在子線程中不一定執(zhí)行的原因!)而performSelector:withObject:在子線程中不受限制。

2)總結(jié):
大家基本都使用過performSelector:系列的API,但是在使用時(shí)能否分清它們的異同卻未可知:
我們來看下perfromSelector:這個(gè)系列的API,總共有下面三類:
performSelector 系列API 分別位于不同的分類中:

  1. performSelector:withObject: (帶有返回值的)是位于NSObject.h 下
    NSObject.h文件下
  1. performSelector:withObject:afterDelay: (無返回值,可用來delay執(zhí)行的)是位于NSRunloop.h文件下。
    NSRunloop
  1. performSelector: onThread: withObject: (無返回值,但可在不同線程中執(zhí)行)位于NSThread.h文件下。
    NSThread.h文件下

performSelectorOnMainThread: withObject: waitUntilDone:(無返回值)
Runloop有關(guān)系的只有第二個(gè)分類中添加的方法,而NSObject分類下的performSelector下的方法跟Runloop沒有必然關(guān)系,調(diào)用后就是一次普通的消息發(fā)送。

3)警告
NSObject分類下perfromSelector API的作用通常是為了跳過編譯器的校驗(yàn),在運(yùn)行期直接調(diào)用selector方法。但是同樣的也會(huì)產(chǎn)生警告!


提示可能存在內(nèi)存泄漏

通常有以下幾種方式來消掉警告:
1)使用宏

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

缺點(diǎn):這種方式是最暴力簡單的方式,任何警告都不應(yīng)該直接忽略,應(yīng)該分析下警告出現(xiàn)的原因,消除掉警告!

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

缺點(diǎn):這種方式僅局限于不需要使用方法返回值的情況。

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

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

4)Apple還推薦使用NSInvocation方式(這種方式既可以傳遞多個(gè)參數(shù),也可以使用返回值),這里不再贅述。

提示內(nèi)存泄漏的原因:
關(guān)于內(nèi)存泄漏問題Apple官方文檔也有簡單描述:

內(nèi)存管理策略中有一條是:誰創(chuàng)建誰管理。假入我們調(diào)用的selector為copy等會(huì)創(chuàng)建實(shí)例的方法。而且這個(gè)操作在編譯期間是無法通過ARC來實(shí)現(xiàn)內(nèi)存管理的(也就是說創(chuàng)建的對(duì)象沒有被進(jìn)行自動(dòng)內(nèi)存管理),這時(shí)是存在內(nèi)存泄漏的可能的。所以會(huì)給出內(nèi)存泄漏的提示!

但這里推薦stackoverflow上高贊的一個(gè)解答:performSelector may cause a leak because its selector is unknown.很透徹的講解了產(chǎn)生警告的原因以及如何避免該類問題。
(注:關(guān)于ARC下對(duì)于返回值內(nèi)存管理和編譯器添加的優(yōu)化,會(huì)在另一篇博客中講)

內(nèi)存泄漏情形總結(jié)

結(jié)論:當(dāng)selectoralloc\new\copymutableCopy等創(chuàng)建對(duì)象的方法時(shí),會(huì)出現(xiàn)內(nèi)存泄漏。

1)例如下面調(diào)用new代碼會(huì)導(dǎo)致出現(xiàn)“內(nèi)存泄漏”:

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

我們可以看下最終的匯編結(jié)果:


存在內(nèi)存泄露調(diào)用

上圖中我們可以看到并沒有objc_release()的調(diào)用。創(chuàng)建的對(duì)象沒有被釋放掉,我們看下正常情況下匯編后的結(jié)果(可以看到最后是有callq objc_release 操作):

[NSObject new];
正常調(diào)用匯編結(jié)果.png

我們?cè)倏聪率褂胮erformSelector調(diào)用new方法創(chuàng)建對(duì)象時(shí)的內(nèi)存圖:(可以看到方法執(zhí)行完后,內(nèi)存中仍然存在一個(gè)對(duì)象未被釋放)

內(nèi)存圖

2)alloc方法同上,這里不再贅述!

3)調(diào)用copy\mutableCopy方法

先來看下下面的代碼:

Person *pp = [Person new];
[pp performSelector:NSSelectorFromString(@"copy")];//直接使用@selector(copy)會(huì)報(bào)編譯錯(cuò)誤

最終匯編結(jié)果如下:


存在內(nèi)存泄露的匯編

我們?cè)趨R編完成后的結(jié)果中沒有找到objc_release()函數(shù),這時(shí)對(duì)象在創(chuàng)建后將會(huì)無法釋放。
這時(shí)查看內(nèi)存圖,如下(可以看到person對(duì)象仍然停在內(nèi)存中):

內(nèi)存圖

綜上可以看出調(diào)用:使用performSelector系列API調(diào)用alloc\new\copy\mutableCopy等創(chuàng)建對(duì)象的方法時(shí)會(huì)導(dǎo)致存在內(nèi)存泄漏,我們應(yīng)該禁止使用performSelector系列API時(shí)調(diào)用此類方法!

補(bǔ)充說明:這里有一個(gè)特例對(duì)于字符串類型,執(zhí)行[ss performSelector:NSSelectorFromString(@"copy")];不會(huì)產(chǎn)生內(nèi)存泄漏,分析原因可能跟系統(tǒng)對(duì)字符串做特殊的優(yōu)化處理有關(guān)!(有更好的解答,歡迎告知分享!)

perfromSelector:afterDelay:使用的注意事項(xiàng)

1、在子線程執(zhí)行perfromSelector: afterDelay:selector會(huì)在對(duì)應(yīng)的子線程被調(diào)用.

2、子線程執(zhí)行perfromSelector: afterDelay:存在selector不被執(zhí)行的問題。
原因:該API的實(shí)質(zhì)是要執(zhí)行的selector以message方式交給當(dāng)前線程的Runloop。由Runloop來執(zhí)行該selector,如果此時(shí)執(zhí)行函數(shù)的子線程沒有主動(dòng)開啟Runloop,selector就不會(huì)誒執(zhí)行。

3、使用了NSRunloop.h下的performSelector相關(guān)API后,需要在dealloc中執(zhí)行cancle移除掉未執(zhí)行的selector,(也是performSelector系列API中唯一有cancle功能的API.) 防止出現(xiàn)內(nèi)存泄漏或者crash。

NSObject下的帶有返回值的performSelector:
1、函數(shù)的返回值為結(jié)構(gòu)體時(shí)

OC的函數(shù)調(diào)用,當(dāng)方法的返回值是結(jié)構(gòu)體時(shí)Runtime調(diào)用的不是objc_msgSend 而是使用objc_msgSend_stret

官方文檔中對(duì)此的描述為:

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

而performSelector:在運(yùn)行時(shí)會(huì)轉(zhuǎn)換成objc_msgSend()調(diào)用,如果此時(shí)方法的返回值為結(jié)構(gòu)體,而這時(shí)通過objc_msgSend()來調(diào)用,編譯器會(huì)認(rèn)為返回值為為一個(gè)對(duì)象類型,因此會(huì)為返回值添加優(yōu)化: objc_retainAutoreleasedReturnValue。

我們用demo來看下優(yōu)化處理得到的最終匯編結(jié)果:

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

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

上述代碼經(jīng)過編譯優(yōu)化后的結(jié)果如下:


匯編后結(jié)果.png

可以看到編譯器添加objc_retainAutoreleaseReturnValue函數(shù)
下面來看下該函數(shù)的實(shí)現(xiàn):


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

我們看到有一個(gè)判斷acceptOptimizedReturn()檢查該返回值是否已經(jīng)優(yōu)化過,若果是直接返回對(duì)象,否則將返回值做retain操作!
通過編譯運(yùn)行源碼進(jìn)行斷點(diǎn)調(diào)試,以及查看上面的匯編結(jié)果可以確定:在此之前返回值并沒有進(jìn)過優(yōu)化,所以會(huì)執(zhí)行objc_retain()操作。
這時(shí)因?yàn)榉祷刂凳且粋€(gè)結(jié)構(gòu)體(非對(duì)象類型)類型,因此會(huì)crash!!

(注:有關(guān) acceptOptimizedReturn() 以及 objc_autoreleaseReturnValue() 實(shí)現(xiàn)會(huì)在其他博客中詳細(xì)講解!)

2、執(zhí)行返回值為結(jié)構(gòu)的函數(shù)崩潰的不同現(xiàn)象分析

函數(shù)返回值為結(jié)構(gòu)體類型時(shí),接受返回值不接收返回值崩潰的原因是不同的:
1)接受返回值情形:(id xx = [self performSelector:@selector(testStruct) withObject:nil];)

crash原因是:編譯器為xx添加了objc_retainAutoreleased優(yōu)化操作,在其內(nèi)部對(duì)xx做了retain導(dǎo)致crash。

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

crash原因是:函數(shù)返回值被認(rèn)為是一個(gè)對(duì)象,所以會(huì)在其不被使用的時(shí)候會(huì)調(diào)用objc_release()對(duì)其釋放,而事實(shí)上返回值是一個(gè)結(jié)構(gòu)體,因此crash。(暫且這么解釋!)
這種情況下的崩潰位置并不在函數(shù)調(diào)用處,很不利于排查問題!
我們看圖來佐證一下:

在普通VC中調(diào)用某個(gè)類中的方法:


調(diào)用該類中的方法

通過以上截圖可以看到可以看到崩潰的位置是在外面的函數(shù)中,而不是執(zhí)行performSelector處。

再來看下調(diào)用的堆棧:

3.png

可以看到堆棧也只是定位到最外層。如果我們線上項(xiàng)目有類似代碼被執(zhí)行并且產(chǎn)生了崩潰,依靠上報(bào)的崩潰日志想要精確定位到出問題得地方是很困難的。

再來看下運(yùn)行源碼得到的崩潰堆棧(分析下原因):


可以看到是調(diào)用performSelector的對(duì)象釋放時(shí)出現(xiàn)crash。這里我們看一下是哪個(gè)對(duì)象的釋放引起crash。

而這個(gè) 0x102423f70 是什么呢?打印一下看:

竟然是調(diào)用“返回結(jié)構(gòu)體函數(shù)”的對(duì)象,這里本人也有點(diǎn)奇怪,為什么會(huì)在釋放這個(gè)對(duì)象時(shí)產(chǎn)生crash??? 實(shí)在沒有想通,如有了解的同學(xué)請(qǐng)一定要告知下。感謝

結(jié)論:
1、不要使用performSelector 調(diào)用返回值為結(jié)構(gòu)體的函數(shù)。(出現(xiàn)問題不好定位)
2、函數(shù)返回值為非對(duì)象類型的,也不要使用使用perfromSelector調(diào)用。

3、拓展:

既然函數(shù)返回值為結(jié)構(gòu)體時(shí)會(huì)產(chǎn)生crash,那如果返回值是“其他非對(duì)象類型”時(shí)是否會(huì)產(chǎn)生crash呢?(這里直接說結(jié)論)

1)不接收函數(shù)返回值時(shí):(ex:[self performSelector:@selector(testPerformselector) withObject:nil])

通過performSelector調(diào)用返回值為intfloatbool 類型的函數(shù)時(shí),不會(huì)發(fā)生crash。

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

通過performSelector調(diào)用返回值為int、bool 函數(shù)會(huì)crash,調(diào)用 返回值為float類型的函數(shù) 不會(huì)產(chǎn)生crash!

調(diào)用返回值為float的函數(shù)會(huì)有兩種情況:
在源碼編譯運(yùn)行,會(huì)發(fā)現(xiàn)得到的返回值xx是函數(shù)的調(diào)用對(duì)象。但是普通demo上測(cè)試 xx是null。這里為什么是這樣,作者也是沒想通(可能還是欠缺某些關(guān)聯(lián)知識(shí)點(diǎn),有了解的同學(xué)希望留言告知)

4、關(guān)于OC消息發(fā)送的不同情況:

oc的消息發(fā)送并不都是轉(zhuǎn)化為objc_msgSend,編譯器會(huì)根據(jù)不同情況選擇不同方法:
1)當(dāng)返回值為結(jié)構(gòu)體時(shí),會(huì)使用objc_msgSend_stret:(當(dāng)返回值為較大的結(jié)構(gòu)體時(shí),無法使用寄存器來傳遞返回值,而是通過棧來傳遞。)
2 )返回浮點(diǎn)數(shù)時(shí),還會(huì)調(diào)用objc_msgSend_fpret(objc_msgSend_fp2ret)
3 )給父類發(fā)送消息,會(huì)調(diào)用objc_msgSendSuper

參考:
perfromSelector一點(diǎn)注意
stackoverflow-perfromSelector-warning
perfromSelector內(nèi)存泄漏問題

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。
  • 序言:七十年代末,一起剝皮案震驚了整個(gè)濱河市,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 228,505評(píng)論 6 533
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場(chǎng)離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 98,556評(píng)論 3 418
  • 文/潘曉璐 我一進(jìn)店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 176,463評(píng)論 0 376
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經(jīng)常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 63,009評(píng)論 1 312
  • 正文 為了忘掉前任,我火速辦了婚禮,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 71,778評(píng)論 6 410
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 55,218評(píng)論 1 324
  • 那天,我揣著相機(jī)與錄音,去河邊找鬼。 笑死,一個(gè)胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,281評(píng)論 3 441
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場(chǎng)噩夢(mèng)啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 42,436評(píng)論 0 288
  • 序言:老撾萬榮一對(duì)情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個(gè)月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 48,969評(píng)論 1 335
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 40,795評(píng)論 3 354
  • 正文 我和宋清朗相戀三年,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 42,993評(píng)論 1 369
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,537評(píng)論 5 359
  • 正文 年R本政府宣布,位于F島的核電站,受9級(jí)特大地震影響,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 44,229評(píng)論 3 347
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 34,659評(píng)論 0 26
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 35,917評(píng)論 1 286
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個(gè)月前我還...
    沈念sama閱讀 51,687評(píng)論 3 392
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對(duì)象是個(gè)殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 47,990評(píng)論 2 374