iOS 查漏補缺 - PerformSelector

leejunhui發表於2020-03-12

performSelector 系列的函式我們都不陌生,但是對於它不同的變種以及底層原理在很多時候還是容易分不清楚,所以筆者希望通過 runtime 原始碼以及 GUNStep 原始碼來一個個抽絲剝繭,把不同變種的 performSelector 理順,並搞清楚每個方法的底層實現,如有錯誤,歡迎指正。

本文的程式碼已放在 Github ,歡迎自取

一、NSObject 下的 PerformSelector

1.1 探索

  • performSelector:(SEL)aSelector

performSelector 方法是最簡單的一個 api,使用方法如下

- (void)jh_performSelector
{
     [self performSelector:@selector(task)];
}

- (void)task
{
    NSLog(@"%s", __func__);
}

// 輸出
2020-03-12 11:13:26.321254+0800 PerformSelectorIndepth[61807:828757] -[ViewController task]
複製程式碼

performSelector: 方法只需要傳入一個 SEL,在 runtime 底層實現為:

- (id)performSelector:(SEL)sel {
    if (!sel) [self doesNotRecognizeSelector:sel];
    return ((id(*)(id, SEL))objc_msgSend)(self, sel);
}
複製程式碼

  • performSelector:(SEL)aSelector withObject:(id)object

performSelector:withObject: 方法相比於上一個方法多了一個引數,使用起來如下:

- (void)jh_performSelectorWithObj
{
    [self performSelector:@selector(taskWithParam:) withObject:@{@"param": @"leejunhui"}];
}

- (void)taskWithParam:(NSDictionary *)param
{
    NSLog(@"%s", __func__);
    NSLog(@"%@", param);
}

// 輸出
2020-03-12 11:12:34.473153+0800 PerformSelectorIndepth[61790:827408] -[ViewController taskWithParam:]
2020-03-12 11:12:34.473381+0800 PerformSelectorIndepth[61790:827408] {
    param = leejunhui;
}
複製程式碼

performSelector:withObject: 方法底層實現如下:

- (id)performSelector:(SEL)sel withObject:(id)obj {
    if (!sel) [self doesNotRecognizeSelector:sel];
    return ((id(*)(id, SEL, id))objc_msgSend)(self, sel, obj);
}
複製程式碼

  • performSelector:(SEL)aSelector withObject:(id)object1 withObject:(id)object2

這個方法相比上一個方法又多了一個引數:

- (void)jh_performSelectorWithObj1AndObj2
{
    [self performSelector:@selector(taskWithParam1:param2:) withObject:@{@"param1": @"lee"} withObject:@{@"param2": @"junhui"}];
}

- (void)taskWithParam1:(NSDictionary *)param1 param2:(NSDictionary *)param2
{
    NSLog(@"%s", __func__);
    NSLog(@"%@", param1);
    NSLog(@"%@", param2);
}

// 輸出
2020-03-12 11:17:52.889731+0800 PerformSelectorIndepth[61859:833076] -[ViewController taskWithParam1:param2:]
2020-03-12 11:17:52.889921+0800 PerformSelectorIndepth[61859:833076] {
    param1 = lee;
}
2020-03-12 11:17:52.890009+0800 PerformSelectorIndepth[61859:833076] {
    param2 = junhui;
}
複製程式碼

performSelector:withObject:withObject: 方法底層實現如下:

- (id)performSelector:(SEL)sel withObject:(id)obj1 withObject:(id)obj2 {
    if (!sel) [self doesNotRecognizeSelector:sel];
    return ((id(*)(id, SEL, id, id))objc_msgSend)(self, sel, obj1, obj2);
}
複製程式碼

1.2 小結

方法 底層實現
performSelector: ((id(*)(id, SEL))objc_msgSend)(self, sel)
performSelector:withObject: ((id(*)(id, SEL, id))objc_msgSend)(self, sel, obj)
performSelector:withObject:withObject: ((id(*)(id, SEL, id, id))objc_msgSend)(self, sel, obj1, obj2)

這三個方法應該是使用頻率很高的 performSelector 系列方法了,我們只需要記住這三個方法在底層都是執行的訊息傳送即可。

二、Runloop 相關的 PerformSelector

iOS 查漏補缺 - PerformSelector

如上圖所示,在 NSRunLoop 標頭檔案中,定義了兩個的分類,分別是

  • NSDelayedPerforming 對應於 NSObject
  • NSOrderedPerform 對應於 NSRunLoop

2.1 NSObject 分類 NSDelayedPerforming

2.1.1 探索

  • performSelector:WithObject:afterDelay:
- (void)jh_performSelectorwithObjectafterDelay
{
    [self performSelector:@selector(taskWithParam:) withObject:@{@"param": @"leejunhui"} afterDelay:1.f];
}

- (void)taskWithParam:(NSDictionary *)param
{
    NSLog(@"%s", __func__);
    NSLog(@"%@", param);
}

// 輸出
2020-03-12 11:25:01.475634+0800 PerformSelectorIndepth[61898:838345] -[ViewController taskWithParam:]
2020-03-12 11:25:01.475837+0800 PerformSelectorIndepth[61898:838345] {
    param = leejunhui;
}
複製程式碼

This method sets up a timer to perform the aSelector message on the current thread’s run loop. The timer is configured to run in the default mode (NSDefaultRunLoopMode). When the timer fires, the thread attempts to dequeue the message from the run loop and perform the selector. It succeeds if the run loop is running and in the default mode; otherwise, the timer waits until the run loop is in the default mode.

這個方法會在當前執行緒所對應的 runloop 中設定一個定時器來執行傳入的 SEL。定時器需要在 NSDefaultRunLoopMode 模式下才會被觸發。當定時器啟動後,執行緒會嘗試從 runloop 中取出 SEL 然後執行。 如果 runloop 已經啟動並且處於 NSDefaultRunLoopMode 的話,SEL 執行成功。否則,直到 runloop 處於 NSDefaultRunLoopMode 前,timer 都會一直等待

通過斷點除錯如下圖所示,runloop 底層最終是通過 __CFRUNLOOP_IS_CALLING_OUT_TO_A_TIMER_CALLBACK_FUNCTION__ ()來觸發任務的執行。

iOS 查漏補缺 - PerformSelector

因為 NSRunLoop 並沒有開源,所以我們只能通過 GNUStep 來窺探底層實現細節,如下所示:

- (void) performSelector: (SEL)aSelector
	      withObject: (id)argument
	      afterDelay: (NSTimeInterval)seconds
{
  NSRunLoop		*loop = [NSRunLoop currentRunLoop];
  GSTimedPerformer	*item;

  item = [[GSTimedPerformer alloc] initWithSelector: aSelector
					     target: self
					   argument: argument
					      delay: seconds];
  [[loop _timedPerformers] addObject: item];
  RELEASE(item);
  [loop addTimer: item->timer forMode: NSDefaultRunLoopMode];
}


/*
 * The GSTimedPerformer class is used to hold information about
 * messages which are due to be sent to objects at a particular time.
 */
@interface GSTimedPerformer: NSObject
{
@public
  SEL		selector;
  id		target;
  id		argument;
  NSTimer	*timer;
}

- (void) fire;
- (id) initWithSelector: (SEL)aSelector
		 target: (id)target
	       argument: (id)argument
		  delay: (NSTimeInterval)delay;
- (void) invalidate;
@end
複製程式碼

我們可以看到,在 performSelector:WithObject:afterDelay: 底層

  • 獲取當前執行緒的 NSRunLoop 物件。
  • 通過傳入的 SELargumentdelay 初始化一個 GSTimedPerformer 例項物件,GSTimedPerformer 型別裡面封裝了 NSTimer 物件。
  • 然後把 GSTimedPerformer 例項加入到 RunLoop 物件的 _timedPerformers 成員變數中
  • 釋放掉 GSTimedPerformer 物件
  • default modetimer 物件加入到 runloop

  • performSelector:WithObject:afterDelay:inModes

performSelector:WithObject:afterDelay:inModes 方法相比上個方法多了一個 modes 引數,根據官方文件的定義,只有當 runloop 處於 modes 中的任意一個 mode 時,才會執行任務,如果 modes 為空,那麼將不會執行任務。

- (void)jh_performSelectorwithObjectafterDelayInModes
{
    [self performSelector:@selector(taskWithParam:) withObject:@{@"param": @"leejunhui"} afterDelay:1.f inModes:@[NSRunLoopCommonModes]];
}

- (void)taskWithParam:(NSDictionary *)param
{
    NSLog(@"%s", __func__);
    NSLog(@"%@", param);
}

// 列印如下
2020-03-12 11:38:58.479152+0800 PerformSelectorIndepth[62006:851520] -[ViewController taskWithParam:]
2020-03-12 11:38:58.479350+0800 PerformSelectorIndepth[62006:851520] {
    param = leejunhui;
}
複製程式碼

這裡我們如果把 modes 引數改為 UITrackingRunLoopMode,那麼就只有在 scrollView 發生滾動的時候才會觸發 timer

我們再看一下 GNUStep 對應的實現:

- (void) performSelector: (SEL)aSelector
	      withObject: (id)argument
	      afterDelay: (NSTimeInterval)seconds
		 inModes: (NSArray*)modes
{
  unsigned	count = [modes count];

  if (count > 0)
    {
      NSRunLoop		*loop = [NSRunLoop currentRunLoop];
      NSString		*marray[count];
      GSTimedPerformer	*item;
      unsigned		i;

      item = [[GSTimedPerformer alloc] initWithSelector: aSelector
						 target: self
					       argument: argument
						  delay: seconds];
      [[loop _timedPerformers] addObject: item];
      RELEASE(item);
      if ([modes isProxy])
	{
	  for (i = 0; i < count; i++)
	    {
	      marray[i] = [modes objectAtIndex: i];
	    }
	}
      else
	{
          [modes getObjects: marray];
	}
      for (i = 0; i < count; i++)
	{
	  [loop addTimer: item->timer forMode: marray[i]];
	}
    }
}

@end
複製程式碼

可以看到,相比於上一個方法的底層實現不同的是,這裡會迴圈新增不同 modetimer 物件到 runloop 中。


  • cancelPreviousPerformRequestsWithTarget:
  • cancelPreviousPerformRequestsWithTarget:selector:object:

cancelPreviousPerformRequestsWithTarget: 方法和 cancelPreviousPerformRequestsWithTarget:selector:object: 方法是兩個類方法,它們的作用是取消執行之前通過 performSelector:WithObject:afterDelay: 方法註冊的任務。使用起來如下所示:

- (void)jh_performSelectorwithObjectafterDelayInModes
{
    // 只有當 scrollView 發生滾動時,才會觸發timer
//    [self performSelector:@selector(taskWithParam:) withObject:@{@"param": @"leejunhui"} afterDelay:1.f inModes:@[UITrackingRunLoopMode]];
    
    [self performSelector:@selector(taskWithParam:) withObject:@{@"param": @"leejunhui"} afterDelay:5.f inModes:@[NSRunLoopCommonModes]];
}

- (IBAction)cancelTask {
    NSLog(@"%s", __func__);
    [ViewController cancelPreviousPerformRequestsWithTarget:self selector:@selector(taskWithParam:) object:@{@"param": @"leejunhui"}];
    
    // [ViewController cancelPreviousPerformRequestsWithTarget:self];
}

// 輸出
2020-03-12 11:52:33.549213+0800 PerformSelectorIndepth[62172:865289] -[ViewController cancelTask]
複製程式碼

這裡有一個區別,就是 cancelPreviousPerformRequestsWithTarget: 類方法會取消掉 target 上所有的通過 performSelector:WithObject:afterDelay: 例項方法註冊的定時任務,而 cancelPreviousPerformRequestsWithTarget:selector:object: 只會通過傳入的 SEL 取消匹配到的定時任務

GNUStepcancelPreviousPerformRequestsWithTarget: 方法底層實現如下:

/*
 * Cancels any perform operations set up for the specified target
 * in the current run loop.
 */
+ (void) cancelPreviousPerformRequestsWithTarget: (id)target
{
    NSMutableArray	*perf = [[NSRunLoop currentRunLoop] _timedPerformers];
    unsigned		count = [perf count];
    
    if (count > 0)
    {
        GSTimedPerformer	*array[count];
        
        IF_NO_GC(RETAIN(target));
        [perf getObjects: array];
        while (count-- > 0)
        {
            GSTimedPerformer	*p = array[count];
            
            if (p->target == target)
            {
                [p invalidate];
                [perf removeObjectAtIndex: count];
            }
        }
        RELEASE(target);
    }
}

// GSTimedPerformer 例項方法
- (void) invalidate
{
    if (timer != nil)
    {
        [timer invalidate];
        DESTROY(timer);
    }
}

複製程式碼

這裡的邏輯其實很清晰:

  • 取出當前 runloop 物件的成員變數 _timedPerformers
  • 判斷定時任務陣列是否為空,不為空才會繼續往下走
  • 初始化一個區域性的空的任務陣列,然後通過 getObjects 從成員變數中取出任務
  • 通過 while 迴圈遍歷所有的任務,如果匹配到了對應的 target,則呼叫任務的 invalidate 方法,在這個方法內部會把定時器停掉然後銷燬。接著還需要把成員變數 _timedPerformers 中對應的任務移除掉

另一個取消任務的方法底層實現如下:

/*
 * Cancels any perform operations set up for the specified target
 * in the current loop, but only if the value of aSelector and argument
 * with which the performs were set up match those supplied.<br />
 * Matching of the argument may be either by pointer equality or by
 * use of the [NSObject-isEqual:] method.
 */
+ (void) cancelPreviousPerformRequestsWithTarget: (id)target
                                        selector: (SEL)aSelector
                                          object: (id)arg
{
    NSMutableArray	*perf = [[NSRunLoop currentRunLoop] _timedPerformers];
    unsigned		count = [perf count];
    
    if (count > 0)
    {
        GSTimedPerformer	*array[count];
        
        IF_NO_GC(RETAIN(target));
        IF_NO_GC(RETAIN(arg));
        [perf getObjects: array];
        while (count-- > 0)
        {
            GSTimedPerformer	*p = array[count];
            
            if (p->target == target && sel_isEqual(p->selector, aSelector)
                && (p->argument == arg || [p->argument isEqual: arg]))
            {
                [p invalidate];
                [perf removeObjectAtIndex: count];
            }
        }
        RELEASE(arg);
        RELEASE(target);
    }
}

複製程式碼

這裡的實現不一樣的地方就是除了判斷 target 是否匹配外,還會判斷 SEL 是否匹配,以及引數是否匹配。


2.1.2 小結

  • performSelector:WithObject:afterDelay:
    • 在該方法所線上程的 runloop 處於 default mode 時,根據給定的時間觸發給定的任務。底層原理是把一個 timer 物件以 default mode 加入到 runloop 物件中,等待喚醒。
  • performSelector:WithObject:afterDelay:inModes:
    • 在該方法所線上程的 runloop 處於給定的任一 mode 時,根據給定的時間觸發給定的任務。底層原理是迴圈把一個 timer 物件以給定的 mode 加入到 runloop 物件中,等待喚醒。
  • cancelPreviousPerformRequestsWithTarget:
    • 取消 target 物件通過 performSelector:WithObject:afterDelay: 方法或 performSelector:WithObject:afterDelay:inModes: 方法註冊的所有定時任務
  • cancelPreviousPerformRequestsWithTarget:selector:object:
    • 取消 target 物件通過 performSelector:WithObject:afterDelay: 方法或 performSelector:WithObject:afterDelay:inModes: 方法註冊的指定的定時任務

這四個方法是作為 NSObjectNSDelayedPerforming 分類存在於 NSRunLoop 原始碼中,所以我們在使用的時候要注意一個細節,那就是執行這些方法的執行緒是否是主執行緒,如果是主執行緒,那麼執行起來是沒有問題的,但是,如果是在子執行緒中執行這些方法,則需要開啟子執行緒對應的 runloop 才能保證執行成功

- (void)jh_performSelectorwithObjectafterDelay
{
    dispatch_async(dispatch_get_global_queue(0, 0), ^{
        [self performSelector:@selector(taskWithParam:) withObject:@{@"param": @"leejunhui"} afterDelay:1.f];
    });
    
//    [self performSelector:@selector(taskWithParam:) withObject:@{@"param": @"leejunhui"} afterDelay:1.f];
}
    
- (void)taskWithParam:(NSDictionary *)param
{
    NSLog(@"%s", __func__);
    NSLog(@"%@", param);
}    


// 沒有輸出
複製程式碼

如上所示的程式碼,通過 GCD 的非同步執行函式在全域性併發佇列上執行任務,並沒有任何列印輸出,我們加入 runloop 的啟動程式碼後結果將完全不一樣:

iOS 查漏補缺 - PerformSelector

對於 performSelector:WithObject:afterDelay:inModes 方法,如果遇到這樣的情況,也是一樣的解決方案。

2.2 NSRunLoop 的分類 NSOrderedPerform

2.2.1 探索

  • performSelector:target:argument:order:modes:

performSelector:target:argument:order:modes: 方法的呼叫者是 NSRunLoop 例項,然後需要傳入要執行的 SEL,以及 SEL 對應的 target,和 SEL 要接收的引數 argument,最後是此次任務的優先順序 order,以及一個 執行模式集合 modes,目的是當 runloopcurrentMode 處於這個執行模式集合中的其中任意一個 mode 時,就會按照優先順序 order 來觸發 SEL 的執行。具體使用如下:

- (void)jh_performSelectorTargetArgumentOrderModes
{
    NSRunLoop *runloop = [NSRunLoop currentRunLoop];
    [runloop performSelector:@selector(runloopTask5) target:self argument:nil order:5 modes:@[NSRunLoopCommonModes]];
    [runloop performSelector:@selector(runloopTask1) target:self argument:nil order:1 modes:@[NSRunLoopCommonModes]];
    [runloop performSelector:@selector(runloopTask3) target:self argument:nil order:3 modes:@[NSRunLoopCommonModes]];
    [runloop performSelector:@selector(runloopTask2) target:self argument:nil order:2 modes:@[NSRunLoopCommonModes]];
    [runloop performSelector:@selector(runloopTask4) target:self argument:nil order:4 modes:@[NSRunLoopCommonModes]];
}

- (void)runloopTask1
{
    NSLog(@"runloop 任務1");
}

- (void)runloopTask2
{
    NSLog(@"runloop 任務2");
}

- (void)runloopTask3
{
    NSLog(@"runloop 任務3");
}

- (void)runloopTask4
{
    NSLog(@"runloop 任務4");
}

- (void)runloopTask5
{
    NSLog(@"runloop 任務5");
}

// 輸出
2020-03-12 14:23:27.088636+0800 PerformSelectorIndepth[62976:972980] runloop 任務1
2020-03-12 14:23:27.088760+0800 PerformSelectorIndepth[62976:972980] runloop 任務2
2020-03-12 14:23:27.088868+0800 PerformSelectorIndepth[62976:972980] runloop 任務3
2020-03-12 14:23:27.088964+0800 PerformSelectorIndepth[62976:972980] runloop 任務4
2020-03-12 14:23:27.089048+0800 PerformSelectorIndepth[62976:972980] runloop 任務5
複製程式碼

可以看到輸出結果就是按照我們傳入的 order 引數作為任務執行的順序。

GUNStep 中這個底層的底層實現如下:

- (void) performSelector: (SEL)aSelector
                  target: (id)target
                argument: (id)argument
                   order: (NSUInteger)order
                   modes: (NSArray*)modes
{
    unsigned		count = [modes count];
    
    if (count > 0)
    {
        NSString			*array[count];
        GSRunLoopPerformer	*item;
        
        item = [[GSRunLoopPerformer alloc] initWithSelector: aSelector
                                                     target: target
                                                   argument: argument
                                                      order: order];
        
        if ([modes isProxy])
        {
            unsigned	i;
            
            for (i = 0; i < count; i++)
            {
                array[i] = [modes objectAtIndex: i];
            }
        }
        else
        {
            [modes getObjects: array];
        }
        while (count-- > 0)
        {
            NSString	*mode = array[count];
            unsigned	end;
            unsigned	i;
            GSRunLoopCtxt	*context;
            GSIArray	performers;
            
            context = NSMapGet(_contextMap, mode);
            if (context == nil)
            {
                context = [[GSRunLoopCtxt alloc] initWithMode: mode
                                                        extra: _extra];
                NSMapInsert(_contextMap, context->mode, context);
                RELEASE(context);
            }
            performers = context->performers;
            
            end = GSIArrayCount(performers);
            for (i = 0; i < end; i++)
            {
                GSRunLoopPerformer	*p;
                
                p = GSIArrayItemAtIndex(performers, i).obj;
                if (p->order > order)
                {
                    GSIArrayInsertItem(performers, (GSIArrayItem)((id)item), i);
                    break;
                }
            }
            if (i == end)
            {
                GSIArrayInsertItem(performers, (GSIArrayItem)((id)item), i);
            }
            i = GSIArrayCount(performers);
            if (i % 1000 == 0 && i > context->maxPerformers)
            {
                context->maxPerformers = i;
                NSLog(@"WARNING ... there are %u performers scheduled"
                      @" in mode %@ of %@\n(Latest: [%@ %@])",
                      i, mode, self, NSStringFromClass([target class]),
                      NSStringFromSelector(aSelector));
            }
        }
        RELEASE(item);
    }
}

@interface GSRunLoopPerformer: NSObject
{
@public
    SEL		selector;
    id		target;
    id		argument;
    unsigned	order;
}
複製程式碼

我們已經知道了 performSelector:WithObject:afterDelay: 方法底層實現使用一個包裹 timer 物件的資料結構的方式,而這裡是使用了一個包裹了 selectortargetargument 以及優先順序 order 的資料結構的方式來實現。同時在 context 上下文的成員變數 performers 中儲存了要執行的任務佇列,所以這裡實際上就是一個簡單的插入排序的過程。


  • cancelPerformSelector:target:argument:
  • cancelPerformSelectorsWithTarget:

cancelPerformSelector:target:argument:cancelPerformSelectorsWithTarget: 使用起來比較簡單,一個需要傳入 selectortargetargument,另一個只需要傳入 target。它們的作用分別是根據給定的三個引數或 target 去 runloop 底層的 performers 任務佇列中查詢任務,找到了就從佇列中移除掉。

而底層具體實現具體如下:

/**
 * Cancels any perform operations set up for the specified target
 * in the receiver.
 */
- (void) cancelPerformSelectorsWithTarget: (id) target
{
    NSMapEnumerator	enumerator;
    GSRunLoopCtxt		*context;
    void			*mode;
    
    enumerator = NSEnumerateMapTable(_contextMap);
    
    while (NSNextMapEnumeratorPair(&enumerator, &mode, (void**)&context))
    {
        if (context != nil)
        {
            GSIArray	performers = context->performers;
            unsigned	count = GSIArrayCount(performers);
            
            while (count--)
            {
                GSRunLoopPerformer	*p;
                
                p = GSIArrayItemAtIndex(performers, count).obj;
                if (p->target == target)
                {
                    GSIArrayRemoveItemAtIndex(performers, count);
                }
            }
        }
    }
    NSEndMapTableEnumeration(&enumerator);
}

/**
 * Cancels any perform operations set up for the specified target
 * in the receiver, but only if the value of aSelector and argument
 * with which the performs were set up match those supplied.<br />
 * Matching of the argument may be either by pointer equality or by
 * use of the [NSObject-isEqual:] method.
 */
- (void) cancelPerformSelector: (SEL)aSelector
                        target: (id) target
                      argument: (id) argument
{
    NSMapEnumerator	enumerator;
    GSRunLoopCtxt		*context;
    void			*mode;
    
    enumerator = NSEnumerateMapTable(_contextMap);
    
    while (NSNextMapEnumeratorPair(&enumerator, &mode, (void**)&context))
    {
        if (context != nil)
        {
            GSIArray	performers = context->performers;
            unsigned	count = GSIArrayCount(performers);
            
            while (count--)
            {
                GSRunLoopPerformer	*p;
                
                p = GSIArrayItemAtIndex(performers, count).obj;
                if (p->target == target && sel_isEqual(p->selector, aSelector)
                    && (p->argument == argument || [p->argument isEqual: argument]))
                {
                    GSIArrayRemoveItemAtIndex(performers, count);
                }
            }
        }
    }
    NSEndMapTableEnumeration(&enumerator);
}
複製程式碼

2.2.2 小結

  • performSelector:target:argument:order:modes:
    • 在該方法所線上程的 runloop 處於給定的任一 mode 時,且處於下一次 runloop 訊息迴圈的開頭的時候觸發給定的任務。底層原理是迴圈把一個類似於 timer 的物件加入到 runloop 的上下文的任務佇列中,等待喚醒
  • cancelPerformSelector:target:argument:
    • 取消 target 物件通過 performSelector:target:argument:order:modes: 方法方法註冊的指定的任務
  • cancelPerformSelectorsWithTarget:
    • 取消 target 物件通過 performSelector:target:argument:order:modes: 方法方法註冊的所有任務

這裡同樣的也需要注意,如果是在子執行緒中執行這些方法,則需要開啟子執行緒對應的 runloop 才能保證執行成功

三、Thread 相關的 performSelector

iOS 查漏補缺 - PerformSelector

如上圖所示,在 NSThread 中定義了 NSObject 的分類 NSThreadPerformAdditions,其中定義了 5 個 performSelector 的方法。

3.1 探索

  • performSelector:onThread:withObject:waitUntilDone:
  • performSelector:onThread:withObject:waitUntilDone:modes:

根據官方文件的解釋,第一個方法相當於呼叫了第二個方法,然後 mode 傳入的是 kCFRunLoopCommonModes。我們這裡只研究第一個方法。

這個方法需要相比於 performSeletor:withObject: 多了兩個引數,分別是要哪個執行緒執行任務以及是否阻塞當前執行緒。但是使用這個方法一定要小心,如下圖所示是一個常見的錯誤用法:

iOS 查漏補缺 - PerformSelector

這裡報的錯是 target thread exited while waiting for the perform,就是說已經退出的執行緒無法執行定時任務。 熟悉 iOS 多執行緒的同學都知道 NSThread 例項化之後的執行緒物件在 start 之後就會被系統回收,而之後呼叫的 performSelector:onThread:withObject:waitUntilDone: 方法又在一個已經回收的執行緒上執行任務,顯然就會崩潰。這裡的解決方案就是給這個子執行緒對應的 runloop 啟動起來,讓執行緒具有 『有事來就幹活,沒事幹就睡覺』 的功能,具體程式碼如下:

iOS 查漏補缺 - PerformSelector

對於 waitUntilDone 引數,如果我們設定為 YES:

iOS 查漏補缺 - PerformSelector

如果設定為 NO:

iOS 查漏補缺 - PerformSelector

所以這裡的 waitUntilDone 可以簡單的理解為控制同步或非同步執行。

在探索 GNUStep 對應實現之前,我們先熟悉一下 GSRunLoopThreadInfo

/* Used to handle events performed in one thread from another.
 */
@interface      GSRunLoopThreadInfo : NSObject
{
  @public
  NSRunLoop             *loop;
  NSLock                *lock;
  NSMutableArray        *performers;
#ifdef _WIN32
  HANDLE	        event;
#else
  int                   inputFd;
  int                   outputFd;
#endif	
}
複製程式碼

GSRunLoopThreadInfo 是每個執行緒特有的一個屬性,儲存了執行緒和 runloop 之間的一些資訊,可以通過下面的方式獲取:

GSRunLoopThreadInfo *
GSRunLoopInfoForThread(NSThread *aThread)
{
    GSRunLoopThreadInfo   *info;
    
    if (aThread == nil)
    {
        aThread = GSCurrentThread();
    }
    if (aThread->_runLoopInfo == nil)
    {
        [gnustep_global_lock lock];
        if (aThread->_runLoopInfo == nil)
        {
            aThread->_runLoopInfo = [GSRunLoopThreadInfo new];
        }
        [gnustep_global_lock unlock];
    }
    info = aThread->_runLoopInfo;
    return info;
}
複製程式碼

然後是另一個 GSPerformHolder:

/**
 * This class performs a dual function ...
 * <p>
 *   As a class, it is responsible for handling incoming events from
 *   the main runloop on a special inputFd.  This consumes any bytes
 *   written to wake the main runloop.<br />
 *   During initialisation, the default runloop is set up to watch
 *   for data arriving on inputFd.
 * </p>
 * <p>
 *   As instances, each  instance retains perform receiver and argument
 *   values as long as they are needed, and handles locking to support
 *   methods which want to block until an action has been performed.
 * </p>
 * <p>
 *   The initialize method of this class is called before any new threads
 *   run.
 * </p>
 */
@interface GSPerformHolder : NSObject
{
    id			receiver;
    id			argument;
    SEL			selector;
    NSConditionLock	*lock;		// Not retained.
    NSArray		*modes;
    BOOL                  invalidated;
@public
    NSException           *exception;
}
+ (GSPerformHolder*) newForReceiver: (id)r
                           argument: (id)a
                           selector: (SEL)s
                              modes: (NSArray*)m
                               lock: (NSConditionLock*)l;
- (void) fire;
- (void) invalidate;
- (BOOL) isInvalidated;
- (NSArray*) modes;
@end
複製程式碼

GSPerformHolder 封裝了任務的細節(receiver, argument, selector)以及執行模式(mode)和一把條件鎖( NSConditionLock )。

接著我們目光聚焦到原始碼 performSelector:onThread:withObject:waitUntilDone:modes: 具體實現上:

- (void) performSelector: (SEL)aSelector
                onThread: (NSThread*)aThread
              withObject: (id)anObject
           waitUntilDone: (BOOL)aFlag
                   modes: (NSArray*)anArray
{
    GSRunLoopThreadInfo   *info;
    NSThread	        *t;
    
    if ([anArray count] == 0)
    {
        return;
    }
    
    t = GSCurrentThread();
    if (aThread == nil)
    {
        aThread = t;
    }
    info = GSRunLoopInfoForThread(aThread);
    if (t == aThread)
    {
        /* Perform in current thread.
         */
        if (aFlag == YES || info->loop == nil)
        {
            /* Wait until done or no run loop.
             */
            [self performSelector: aSelector withObject: anObject];
        }
        else
        {
            /* Don't wait ... schedule operation in run loop.
             */
            [info->loop performSelector: aSelector
                                 target: self
                               argument: anObject
                                  order: 0
                                  modes: anArray];
        }
    }
    else
    {
        GSPerformHolder   *h;
        NSConditionLock	*l = nil;
        
        if ([aThread isFinished] == YES)
        {
            [NSException raise: NSInternalInconsistencyException
                        format: @"perform [%@-%@] attempted on finished thread (%@)",
             NSStringFromClass([self class]),
             NSStringFromSelector(aSelector),
             aThread];
        }
        if (aFlag == YES)
        {
            l = [[NSConditionLock alloc] init];
        }
        
        h = [GSPerformHolder newForReceiver: self
                                   argument: anObject
                                   selector: aSelector
                                      modes: anArray
                                       lock: l];
        [info addPerformer: h];
        if (l != nil)
        {
            [l lockWhenCondition: 1];
            [l unlock];
            RELEASE(l);
            if ([h isInvalidated] == NO)
            {
                /* If we have an exception passed back from the remote thread,
                 * re-raise it.
                 */
                if (nil != h->exception)
                {
                    NSException       *e = AUTORELEASE(RETAIN(h->exception));
                    
                    RELEASE(h);
                    [e raise];
                }
            }
        }
        RELEASE(h);
    }
}
複製程式碼
  • 宣告一個GSRunLoopThreadInfo 物件和一條 NSThread 執行緒
  • 判斷執行模式陣列引數是否為空
  • 獲取當前執行緒,將結果賦值於第一步宣告的區域性執行緒變數
  • 判斷如果傳入的要執行任務的執行緒 aThread 如果為空,那麼就把當前執行緒賦值於到 aThread 上
  • 確保 aThread 不為空之後獲取該執行緒對應的 GSRunLoopThreadInfo 物件並賦值於第一步宣告的區域性 info 變數
  • 確保 info 有值後,判斷是否是在當前執行緒上執行任務
  • 如果是在當前執行緒上執行任務,接著判斷是否要阻塞當前執行緒,或當前執行緒的 runloop 為空。
    • 如果是的話,則直接呼叫 performSelector:withObject 來執行任務
    • 如果不是的話,則通過執行緒對應的 runloop 物件呼叫 performSelector:target:argument:order:modes: 來執行任務
  • 如果不是在當前執行緒上執行任務,宣告一個 GSPerformHolder 區域性變數,宣告一把空的條件鎖 NSConditionLock
    • 判斷要執行任務的執行緒是否已經被回收,如果已被回收,則丟擲異常
    • 如果未被回收
      • 判斷是否要阻塞當前執行緒,如果傳入的引數需要阻塞,則初始化條件鎖
      • 根據傳入的引數及條件鎖初始化 GSPerformHolder 例項
      • 然後在 info 中加入 GSPerformHolder 例項
      • 然後判斷條件鎖如果不為空,賦予條件鎖何時加鎖的條件,然後解鎖條件鎖,然後釋放條件鎖
      • 判斷 GSPerformHolder 區域性變數是否已經被釋放,如果沒有被釋放,丟擲異常

  • performSelectorOnMainThread:withObject:waitUntilDone:
  • performSelectorOnMainThread:withObject:waitUntilDone:modes:

顧名思義,這兩個方法其實就是在主執行緒上執行任務,根據傳入的引數決定是否阻塞主執行緒,以及在哪些執行模式下執行任務。使用方法如下:

- (void)jh_performSelectorOnMainThreadwithObjectwaitUntilDone
{
    [self performSelectorOnMainThread:@selector(threadTask:) withObject:@{@"param": @"leejunhui"} waitUntilDone:NO];
//    [self performSelectorOnMainThread:@selector(threadTask:) withObject:@{@"param": @"leejunhui"} waitUntilDone:NO modes:@[NSRunLoopCommonModes]];
}

- (void)threadTask:(NSDictionary *)param
{
    NSLog(@"%s", __func__);
    NSLog(@"%@", [NSThread currentThread]);
}

// 輸出
2020-03-12 16:14:31.783962+0800 PerformSelectorIndepth[63614:1057033] -[ViewController threadTask:]
2020-03-12 16:14:31.784126+0800 PerformSelectorIndepth[63614:1057033] <NSThread: 0x600002e76dc0>{number = 1, name = main}
複製程式碼

因為是在主執行緒上執行,所以並不需要手動開啟 runloop。我們來看下這兩個方法在 GNUStep 中底層實現:

- (void) performSelectorOnMainThread: (SEL)aSelector
                          withObject: (id)anObject
                       waitUntilDone: (BOOL)aFlag
                               modes: (NSArray*)anArray
{
    /* It's possible that this method could be called before the NSThread
     * class is initialised, so we check and make sure it's initiailised
     * if necessary.
     */
    if (defaultThread == nil)
    {
        [NSThread currentThread];
    }
    [self performSelector: aSelector
                 onThread: defaultThread
               withObject: anObject
            waitUntilDone: aFlag
                    modes: anArray];
}

- (void) performSelectorOnMainThread: (SEL)aSelector
                          withObject: (id)anObject
                       waitUntilDone: (BOOL)aFlag
{
    [self performSelectorOnMainThread: aSelector
                           withObject: anObject
                        waitUntilDone: aFlag
                                modes: commonModes()];
}
複製程式碼

不難看出,這裡其實就是呼叫的 performSelector:onThread:withObject:waitUntilDone:modes 方法,但是有一個細節需要注意,就是有可能在 NSThread 類被初始化之前,就呼叫了 performSelectorOnMainThread 方法,所以需要手動呼叫一下 [NSThread currentThread]


  • performSelectorInBackground:withObject:

最後要探索的是 performSelectorInBackground:withObject: 方法,這個方法用法如下:

- (void)jh_performSelectorOnBackground
{
    [self performSelectorInBackground:@selector(threadTask:) withObject:@{@"param": @"leejunhui"}];
}

- (void)threadTask:(NSDictionary *)param
{
    NSLog(@"%s", __func__);
    NSLog(@"%@", [NSThread currentThread]);
}

// 輸出
2020-03-12 16:19:36.751675+0800 PerformSelectorIndepth[63660:1061569] -[ViewController threadTask:]
2020-03-12 16:19:36.751990+0800 PerformSelectorIndepth[63660:1061569] <NSThread: 0x6000027a0ac0>{number = 6, name = (null)}
複製程式碼

根據輸出我們可知,這裡顯然是開了一條子執行緒來執行任務,我們看一下 GNUStep 的底層實現:

- (void) performSelectorInBackground: (SEL)aSelector
                          withObject: (id)anObject
{
    [NSThread detachNewThreadSelector: aSelector
                             toTarget: self
                           withObject: anObject];
}
複製程式碼

可以看到在底層其實是呼叫的 NSThread 的類方法來執行傳入的任務。關於 NSThread 細節我們後面會進行探索。


3.2 小結

  • performSelector:onThread:withObject:waitUntilDone:performSelector:onThread:withObject:waitUntilDone:modes:
    • 在該方法所線上程的 runloop 處於給定的任一 mode 時,判斷是否阻塞當前執行緒,並且處於下一次 runloop 訊息迴圈的開頭的時候觸發給定的任務。
  • performSelectorOnMainThread:withObject:waitUntilDone:performSelectorOnMainThread:withObject:waitUntilDone:modes:
    • 當主執行緒的 runloop 處於給定的任一 mode 時,判斷是否阻塞主執行緒,並且處於下一次 runloop 訊息迴圈的開頭的時候觸發給定的任務。
  • performSelectorInBackground:withObject:
    • 在子執行緒上執行給定的任務。底層是通過 NSThreaddetachNewThread 實現。

四、總結

iOS 查漏補缺 - PerformSelector

相關文章