Objective-C: [NSObject performSelector: onThread…]

2019-09-12 10:44发布

问题:

Very similar issue is already discussed here. The problem at hand and what I am trying to achieve is to call a function on a given object in the thread it is created at. Here is the complete case:

  1. an instance of class A is created in a given NSThread (Thread A) (not the main one). The instance keeps its creating NSThread as a member variable.
  2. an instance of class B has one of its member functions executing in another NSThread - Thread B, and wants to call a function of A in A's creation thread. Thus B's currently executing function issues the following call:

    [_a performSelector: @(fun) 
               onThread: _a.creationThread
             withObject: nil
          waitUntilDone: NO];
    

If the creation thread of A's instance is not the main one, fun never gets called. If the creation thread is the main one it is always called. First I was thinking whether the thread that created A's instance has been destroyed and the pointer points to an invalid thread but actually calling any functions on the thread object (Thread A) produces valid results and no crashes. Also checking the object is valid according to this check. Any suggestions?


Update:

What I'm doing is creating a timer on a background thread:

_timer = [NSTimer timerWithTimeInterval:60.0 target:self selector:@selector(fun:) userInfo:nil repeats:YES];
[[NSRunLoop currentRunLoop] addTimer:_timer forMode:NSDefaultRunLoopMode];

This code is the one starting the timer. The timer is not started in any specific background thread. It just happens that the function that creates the timer could be called in any thread. Thus the timer should be invalidated in the exactly same one as the NSTimer documentation states: "you should always call the invalidate method from the same thread on which the timer was installed."

回答1:

To run timer on background thread, you have two options.

  1. Use dispatch timer source:

    @property (nonatomic, strong) dispatch_source_t timer;
    

    and you can then configure this timer to fire every two seconds:

    - (void)startTimer {
        dispatch_queue_t queue = dispatch_queue_create("com.domain.app.timer", 0);
        self.timer = dispatch_source_create(DISPATCH_SOURCE_TYPE_TIMER, 0, 0, queue);
        dispatch_source_set_timer(self.timer, dispatch_walltime(NULL, 0), 2.0 * NSEC_PER_SEC, 0.1 * NSEC_PER_SEC);
        dispatch_source_set_event_handler(self.timer, ^{
            // call whatever you want here
        });
        dispatch_resume(self.timer);
    }
    
    - (void)stopTimer {
        dispatch_cancel(self.timer);
        self.timer = nil;
    }
    
  2. Run NSTimer on background thread. To do this, you can do something like:

    @property (atomic) BOOL shouldKeepRunning;
    @property (nonatomic, strong) NSThread *timerThread;
    @property (nonatomic, strong) NSTimer *timer;
    

    And

    - (void)startTimerThread {
        self.timerThread = [[NSThread alloc] initWithTarget:self selector:@selector(startTimer:) object:nil];
        [self.timerThread start];
    }
    
    - (void)stopTimerThread {
        [self performSelector:@selector(stopTimer:) onThread:self.timerThread withObject:nil waitUntilDone:false];
    }
    
    - (void)startTimer:(id)__unused object {
        @autoreleasepool {
            NSRunLoop *runLoop = [NSRunLoop currentRunLoop];
            self.timer = [NSTimer timerWithTimeInterval:1 target:self selector:@selector(handleTimer:) userInfo:nil repeats:YES];
            [runLoop addTimer:self.timer forMode:NSDefaultRunLoopMode];
    
            self.shouldKeepRunning = YES;
            while (self.shouldKeepRunning && [runLoop runMode:NSDefaultRunLoopMode beforeDate:[NSDate distantFuture]])
                ;
    
            self.timerThread = nil;
        }
    }
    
    - (void)handleTimer:(NSTimer *)timer {
        NSLog(@"tick");
    }
    
    - (void)stopTimer:(id)__unused object {
        [self.timer invalidate];
        self.timer = nil;
        self.shouldKeepRunning = FALSE;
    }
    

    I'm not crazy about the shouldKeepRunning state variable, but if you look at the Apple documentation for the run method, they discourage the reliance upon adding sources/timers to run loops:

    If you want the run loop to terminate, you shouldn't use this method. Instead, use one of the other run methods and also check other arbitrary conditions of your own, in a loop. A simple example would be:

    BOOL shouldKeepRunning = YES;        // global
    NSRunLoop *theRL = [NSRunLoop currentRunLoop];
    while (shouldKeepRunning && [theRL runMode:NSDefaultRunLoopMode beforeDate:[NSDate distantFuture]]);
    

Personally, I'd recommend the dispatch timer approach.