關於 performSelector 的一些小探討

ShenYuanLuo發表於2019-02-13

本文首發在我的個人部落格: blog.shenyuanluo.com,喜歡的朋友歡迎訂閱。

考慮以下程式碼,最終會輸出什麼?

  1. 例子①:
    - (void)viewDidLoad
    {
        [super viewDidLoad];
    
        NSLog(@"1 - %@", [NSThread currentThread]);
    
        dispatch_async(dispatch_get_global_queue(0, 0), ^{
        
            NSLog(@"2 - %@", [NSThread currentThread]);
            [self performSelector:@selector(test)
                       withObject:nil];
            NSLog(@"4 - %@", [NSThread currentThread]);
        });
    }
    
    - (void)test
    {
        NSLog(@"3 - %@", [NSThread currentThread]);
    }
    複製程式碼
    • 輸出結果:1,2,3,4
    • 原因: 因為 performSelector:withObject: 會在當前執行緒立即執行指定的 selector 方法。
  2. 例子②:
    - (void)viewDidLoad
    {
        [super viewDidLoad];
        
        NSLog(@"1 - %@", [NSThread currentThread]);
        
        dispatch_async(dispatch_get_global_queue(0, 0), ^{
            
            NSLog(@"2 - %@", [NSThread currentThread]);
            [self performSelector:@selector(test)
                       withObject:nil
                       afterDelay:0];
            NSLog(@"4 - %@", [NSThread currentThread]);
        });
    }
    
    - (void)test
    {
        NSLog(@"3 - %@", [NSThread currentThread]);
    }
    複製程式碼
    • 輸出結果:1,2,4
    • 原因: 因為 performSelector:withObject:afterDelay: 實際是往 RunLoop 裡面註冊一個定時器,而在子執行緒中,RunLoop 是沒有開啟(預設)的,所有不會輸出 3。官網 API 作如下解釋:
      關於 performSelector 的一些小探討
  3. 例子③:
    - (void)viewDidLoad
    {
        [super viewDidLoad];
        
        NSLog(@"1 - %@", [NSThread currentThread]);
        
        dispatch_async(dispatch_get_global_queue(0, 0), ^{
            
            NSLog(@"2 - %@", [NSThread currentThread]);
            [self performSelector:@selector(test)
                       withObject:nil
                       afterDelay:0];
            [[NSRunLoop currentRunLoop] run];
            NSLog(@"4 - %@", [NSThread currentThread]);
        });
    }
    	
    - (void)test
    {
        NSLog(@"3 - %@", [NSThread currentThread]);
    }
    複製程式碼
    • 輸出結果:1,2,3,4
    • 原因: 由於 [[NSRunLoop currentRunLoop] run]; 會建立的當前子執行緒對應的 RunLoop 物件並啟動了,因此可以執行 test 方法;並且 test 執行完後,RunLoop 中註冊的定時器已經無效,所以還可以輸出 4 (對比 例子⑥例子)。
  4. 例子④:
    - (void)viewDidLoad
    {
        [super viewDidLoad];
        
        NSLog(@"1 - %@", [NSThread currentThread]);
        
        dispatch_async(dispatch_get_global_queue(0, 0), ^{
            
            NSLog(@"2 - %@", [NSThread currentThread]);
            [self performSelector:@selector(test)
                         onThread:[NSThread currentThread]
                       withObject:nil
                    waitUntilDone:YES];
            NSLog(@"4 - %@", [NSThread currentThread]);
        });
    }
    	
    - (void)test
    {
        NSLog(@"3 - %@", [NSThread currentThread]);
    }
    複製程式碼
    • 輸出結果:1,2,3,4
    • 原因: 因為 performSelector:onThread:withObject:waitUntilDone: 會在指定的執行緒執行,而執行的策略根據引數 wait 處理,這裡傳 YES 表明將會立即阻斷 指定的執行緒 並執行指定的 selector。官網 API 解釋如下:
      關於 performSelector 的一些小探討
  5. 例子⑤:
    - (void)viewDidLoad
    {
        [super viewDidLoad];
        
        NSLog(@"1 - %@", [NSThread currentThread]);
        
        dispatch_async(dispatch_get_global_queue(0, 0), ^{
            
            NSLog(@"2 - %@", [NSThread currentThread]);
            [self performSelector:@selector(test)
                         onThread:[NSThread currentThread]
                       withObject:nil
                    waitUntilDone:NO];
            NSLog(@"4 - %@", [NSThread currentThread]);
        });
    }
    	
    - (void)test
    {
        NSLog(@"3 - %@", [NSThread currentThread]);
    }
    複製程式碼
    • 輸出結果:1,2,4
    • 原因: 因為 performSelector:onThread:withObject:waitUntilDone: 會在指定的執行緒執行,而執行的策略根據引數 wait 處理,這裡傳 NO 表明不會立即阻斷 指定的執行緒 而是將 selector 新增到指定執行緒的 RunLoop 中等待時機執行。(該例子中,子執行緒 RunLoop 沒有啟動,所有沒有輸出 3)官網 API 解釋如下:
  6. 例子⑥:
    - (void)viewDidLoad
    {
        [super viewDidLoad];
        
        NSLog(@"1 - %@", [NSThread currentThread]);
        
        dispatch_async(dispatch_get_global_queue(0, 0), ^{
            
            NSLog(@"2 - %@", [NSThread currentThread]);
            [self performSelector:@selector(test)
                         onThread:[NSThread currentThread]
                       withObject:nil
                    waitUntilDone:NO];
            [[NSRunLoop currentRunLoop] run];
            NSLog(@"4 - %@", [NSThread currentThread]);
        });
    }
    	
    - (void)test
    {
        NSLog(@"3 - %@", [NSThread currentThread]);
    }
    複製程式碼
    • 輸出結果:1,2,3
    • 原因: 由於 [[NSRunLoop currentRunLoop] run]; 已經建立的當前子執行緒對應的 RunLoop 物件並啟動了,因此可以執行 test 方法;但是 test 方法執行完後,RunLoop 並沒有結束(使用這種啟動方式,RunLoop 會一直執行下去,在此期間會處理來自輸入源的資料,並且會在 NSDefaultRunLoopMode 模式下重複呼叫 runMode:beforeDate: 方法)所以無法繼續輸出 4
  7. 例子⑦:
    - (void)viewDidLoad
    {
        [super viewDidLoad];
        
        NSLog(@"1 - %@", [NSThread currentThread]);
        
        dispatch_async(dispatch_get_global_queue(0, 0), ^{
            
            NSLog(@"2 - %@", [NSThread currentThread]);
            [self performSelector:@selector(test)
                         onThread:[NSThread currentThread]
                       withObject:nil
                    waitUntilDone:NO];
            [[NSRunLoop currentRunLoop] runUntilDate:[NSDate distantFuture]];
            NSLog(@"4 - %@", [NSThread currentThread]);
        });
    }
    	
    - (void)test
    {
        NSLog(@"3 - %@", [NSThread currentThread]);
    }
    複製程式碼
    • 輸出結果:1,2,3
    • 原因: 由於 [[NSRunLoop currentRunLoop] runUntilDate:[NSDate distantFuture]]; 已經建立的當前子執行緒對應的 RunLoop 物件並啟動了,因此可以執行 test 方法;但是 test 方法執行完後,RunLoop 並沒有結束(使用這種啟動方式,可以設定超時時間,在超時時間到達之前,runloop會一直執行,在此期間runloop會處理來自輸入源的資料,並且會在 NSDefaultRunLoopMode 模式下重複呼叫 runMode:beforeDate: 方法)所以無法繼續輸出 4
  8. 例子⑧:
    - (void)viewDidLoad
    {
        [super viewDidLoad];
        
        NSLog(@"1 - %@", [NSThread currentThread]);
        
        dispatch_async(dispatch_get_global_queue(0, 0), ^{
            
            NSLog(@"2 - %@", [NSThread currentThread]);
            [self performSelector:@selector(test)
                         onThread:[NSThread currentThread]
                       withObject:nil
                    waitUntilDone:NO];
            [[NSRunLoop currentRunLoop] runMode:NSDefaultRunLoopMode
                                     beforeDate:[NSDate distantFuture]];
            NSLog(@"4 - %@", [NSThread currentThread]);
        });
    }
    	
    - (void)test
    {
        NSLog(@"3 - %@", [NSThread currentThread]);
    }
    複製程式碼
    • 輸出結果:1,2,3,4
    • 原因: 由於 [[NSRunLoop currentRunLoop] runMode:NSDefaultRunLoopMode beforeDate:[NSDate distantFuture]]; 已經建立的當前子執行緒對應的 RunLoop 物件並啟動了,因此可以執行 test 方法;而且 test 方法執行完後,RunLoop 立刻結束(使用這種啟動方式 ,RunLoop 會執行一次,超時時間到達或者第一個 input source 被處理,則 RunLoop 就會退出)所以可以繼續輸出 4

小結:

  1. 常用 performSelector 方法
    • 常用的 perform,是 NSObject.h 標頭檔案下的方法:
    - (id)performSelector:(SEL)aSelector;
    - (id)performSelector:(SEL)aSelector withObject:(id)object;
    - (id)performSelector:(SEL)aSelector withObject:(id)object1 withObject:(id)object2;
    複製程式碼
    • 可以 delay 的 perform,是 NSRunLoop.h 標頭檔案下的方法:
    - (void)performSelector:(SEL)aSelector withObject:(nullable id)anArgument afterDelay:(NSTimeInterval)delay inModes:(NSArray<NSRunLoopMode> *)modes;
    - (void)performSelector:(SEL)aSelector withObject:(nullable id)anArgument afterDelay:(NSTimeInterval)delay;
    複製程式碼
    • 可以 指定執行緒 的 perform,是 NSThread 標頭檔案下的方法:
    - (void)performSelectorOnMainThread:(SEL)aSelector withObject:(nullable id)arg waitUntilDone:(BOOL)wait modes:(nullable NSArray<NSString *> *)array;
    - (void)performSelectorOnMainThread:(SEL)aSelector withObject:(nullable id)arg waitUntilDone:(BOOL)wait;
    - (void)performSelector:(SEL)aSelector onThread:(NSThread *)thr withObject:(nullable id)arg waitUntilDone:(BOOL)wait modes:(nullable NSArray<NSString *> *)array;
    - (void)performSelector:(SEL)aSelector onThread:(NSThread *)thr withObject:(nullable id)arg waitUntilDone:(BOOL)wait;
    - (void)performSelectorInBackground:(SEL)aSelector withObject:(nullable id)arg;
    複製程式碼
  2. RunLoop 退出方式:
    • 使用 - (void)run; 啟動,RunLoop 會一直執行下去,在此期間會處理來自輸入源的資料,並且會在 NSDefaultRunLoopMode 模式下重複呼叫 runMode:beforeDate: 方法;
    • 使用 - (void)runUntilDate:(NSDate *)limitDate; 啟動,可以設定超時時間,在超時時間到達之前,RunLoop 會一直執行,在此期間 RunLoop 會處理來自輸入源的資料,並且也會在 NSDefaultRunLoopMode 模式下重複呼叫 runMode:beforeDate: 方法;
    • 使用 - (void)runMode:(NSString *)mode beforeDate:(NSDate *)limitDate; 啟動,RunLoop 會執行一次,超時時間到達或者第一個 input source 被處理,則 RunLoop 就會退出。
  3. 更多關於 NSRunLoop的退出方式 可以看這篇博文

參考

  1. NSRunLoop的退出方式

相關文章