iOS執行器performSelector詳解

賣報的小畫家Sure發表於2019-05-06

1、performSelector簡單使用

performSelector(方法執行器),iOS中提供瞭如下幾種常用的呼叫方式

[self performSelector:@selector(sureTestMethod)];
[self performSelector:@selector(sureTestMethod)
           withObject:params];
[self performSelector:@selector(sureTestMethod)
           withObject:params
           withObject:params2];
......
複製程式碼

performSelector響應Objective-C動態性,將方法的繫結延遲到執行時,因此編譯階段不會檢測方法有效性,即方法不存在也不會提示報錯。反之因為此特性,performSelector也廣泛用於動態化和元件化的模組中。

如果方法名稱也是動態不確定的,會提示如下警告:

SEL selector = @selector(dynamicMethod);
[self performSelector:selector];
複製程式碼
⚠️ PerformSelector may cause a leak because its selector is unknown
複製程式碼

意為因為當前方法名未知可能會引起記憶體洩露相關問題。 可以通過如下程式碼忽略此警告

#pragma clang diagnostic push
#pragma clang diagnostic ignored "-Warc-performSelector-leaks"
    [self performSelector:selector];
#pragma clang diagnostic pop
複製程式碼

performSelector預設最多隻可傳遞兩個引數,若需多參可將引數封裝為NSArray、NSDictionary、NSInvocation進行傳遞。另外方法呼叫本質都是訊息機制,也可以通過msg_send實現。

id params;
id params2;
id params3;

SEL selector = NSSelectorFromString(@"sureTestMethod:params2:params3:");
objc_msgSend(self, selector,params,params2,params3);

- (void)sureTestMethod:(id)params params2:(id)params2 params3:(id)params3 {
    NSLog(@"sureTestMethod-multi-parameter");
}
複製程式碼

2、performSelector延遲呼叫

[self performSelector:@selector(sureTestMethod:)
           withObject:params
           afterDelay:3];
複製程式碼

此方法意為在當前Runloop中延遲3秒後執行selector中方法。 使用該方法需要注意以下事項: 在子執行緒中呼叫performSelector: withObject: afterDelay:預設無效,如下程式碼並不會列印sureTestMethodCall

dispatch_async(dispatch_get_global_queue(0, 0), ^{
    [self performSelector:@selector(sureTestMethod:)
               withObject:params
               afterDelay:3];
});
- (void)sureTestMethod:(id)objcet {
    NSLog(@"sureTestMethodCall");
}
複製程式碼

這是因為performSelector: withObject: afterDelay:是在當前Runloop中延時執行的,而子執行緒的Runloop預設不開啟,因此無法響應方法。

所以我們嘗試在GCD Block中新增 [[NSRunLoop currentRunLoop]run];

dispatch_async(dispatch_get_global_queue(0, 0), ^{
        [self performSelector:@selector(sureTestMethod:)
                   withObject:params
                   afterDelay:3];
        [[NSRunLoop currentRunLoop]run];
    });
複製程式碼

執行程式碼發現可以正常列印sureTestMethodCall。

這裡有個坑需要注意,曾經嘗試將 [[NSRunLoop currentRunLoop]run]新增在performSelector: withObject: afterDelay:方法前,但發現延遲方法仍然不呼叫,這是因為若想開啟某執行緒的Runloop,必須具有timer、source、observer任一事件才能觸發開啟。

簡言之如下程式碼在執行 [[NSRunLoop currentRunLoop]run]前沒有任何事件新增到當前Runloop,因此該執行緒的Runloop是不會開啟的,從而延遲事件不執行。

dispatch_async(dispatch_get_global_queue(0, 0), ^{
        [[NSRunLoop currentRunLoop]run];
        [self performSelector:@selector(sureTestMethod:)
                   withObject:params
                   afterDelay:3];
    });
複製程式碼

關於Runloop,可詳見:深入理解RunLoop

3、performSelector取消延遲

我們在View上放置一個Button,預期需求是防止暴力點選,只響應最後一次點選時的事件。

此需求我們可以通過cancelPreviousPerformRequestsWithTarget來進行實現。cancelPreviousPerformRequestsWithTarget的作用為取消當前延時任務。在執行延遲事件前取消當前存在的延遲任務即可實現如上效果。

- (IBAction)buttonClick:(id)sender {
    id params;
    [[self class]cancelPreviousPerformRequestsWithTarget:self
                                                selector:@selector(sureTestMethod:)
                                                  object:params];
    [self performSelector:@selector(sureTestMethod:)
               withObject:params
               afterDelay:3];
}

- (void)sureTestMethod:(id)objcet {
    NSLog(@"sureTestMethodCall");
}
複製程式碼

重複點選後,列印結果如下,只響應了一次點選

2019-05-06 11:29:50.352157+0800 performSelector[14342:457353] sureTestMethodCall
複製程式碼

4、performSelector模擬多執行緒

我們可以通過performSelectorInBackground將某selector任務放在子執行緒中

[self performSelectorInBackground:@selector(sureTestMethod:)
                           withObject:params];
- (void)sureTestMethod:(id)objcet {
    NSLog(@"%@",[NSThread currentThread]);
}
複製程式碼
//<NSThread: 0x600003854080>{number = 3, name = (null)}
複製程式碼

列印結果可見當前方法執行在子執行緒中。

回到主執行緒執行我們可以通過方法

[self performSelectorOnMainThread:@selector(sureTestMethod)
                       withObject:params
                    waitUntilDone:NO];
複製程式碼

waitUntilDone表示是否等待當前selector任務完成後再執行後續任務。示例如下,waitUntilDone為YES時,列印1,2,3。為NO時列印1,3,2。

    NSLog(@"1");
    [self performSelectorOnMainThread:@selector(test) withObject:nil waitUntilDone:NO];
    NSLog(@"3");
複製程式碼
- (void)test {
    sleep(3);
    NSLog(@"2");
}
複製程式碼

另外performSelector還提供了將任務執行在某個指定執行緒的操作

[self performSelector:@selector(sureTestMethod:)
                 onThread:thread
               withObject:params
            waitUntilDone:NO];
複製程式碼

使用該方法一定要注意所線上程生命週期是否正常,若thread已銷燬不存在,而performSelector強行執行任務在該執行緒,會導致崩潰:

NSThread *thread = [[NSThread alloc]initWithBlock:^{
    NSLog(@"do thread event");
}];
[thread start];
[self performSelector:@selector(sureTestMethod:)
             onThread:thread
           withObject:params
        waitUntilDone:YES];
複製程式碼

上述程式碼會導致崩潰,崩潰資訊為:

*** Terminating app due to uncaught exception 'NSDestinationInvalidException',
reason: '*** -[ViewController performSelector:onThread:withObject:waitUntilDone:modes:]:
target thread exited while waiting for the perform'
複製程式碼

因為thread開啟執行do thread event完畢後即退出銷燬,所以在等待執行任務時Thread已不存在導致崩潰。

好了,關於performSelector的內容暫時寫到這裡了,有其他補充歡迎評論~

iOS執行器performSelector詳解

相關文章