Спорадический NSTimer зомби

#ios #objective-c

#iOS #objective-c

Вопрос:

У меня есть таймер в UIViewController:

 @interface MyViewController : UIViewController <UITableViewDataSource,UITableViewDelegate, MFMailComposeViewControllerDelegate>
...
@property (nonatomic, assign) int m_remaining_time;
...
@end
 

В файле .m:

 @interface MyViewController ()
{
    NSTimer *m_timer;
}
 

Таймер запускается при viewDidAppear()

 m_timer = [NSTimer scheduledTimerWithTimeInterval:1.0f target:self selector:@selector(decrementSpin) userInfo:nil repeats:YES];
 

Метод decrementSpin просто уменьшает счетчик и печатает журнал:

 - (void)decrementSpin
{
    self.m_remaining_time--;

    if(self.m_remaining_time > 0) {
        NSLog(@"[TIMER] >> %d", self.m_remaining_time);
    }
    else {
        NSLog(@"[TIMER] >> Time finished!");
        [m_timer invalidate];
        m_timer = nil;
    }
}
 

Я уничтожаю таймер в каждом случае, когда ViewController закроется:
В appDidEnterInBackground viewDidDisappear исчезает при нажатии кнопки, закрывающей представление (например, перейти к следующему представлению)

Проблема в том, что в некоторых случаях я видел в симуляторе журнал [TIMER] >> %d, который продолжает печатать даже после того, как я закрыл представление.

Это не всегда так, но иногда он продолжает подсчитывать.

Какие-нибудь подсказки?

Редактировать:

@skaak, я делаю следующий код в appDidEnterInBackground, viewDidDisappear:

 if(m_timer) {
    [m_timer invalidate];
    m_timer = nil;
}
 

Затем, когда игрок нажимает кнопки «Далее» и «Домой».
Например, случай с кнопкой «Домой»:

 -(IBAction)homeButtonPressed:(id)sender{
if(m_timer) {
    [m_timer invalidate];
    m_timer = nil;
}
[[NSNotificationCenter defaultCenter] removeObserver:self];


InitScreenViewController* initVc = [self.storyboard instantiateViewControllerWithIdentifier:@"initScreenViewController"];
UINavigationController *navVC = [[UINavigationController alloc] initWithRootViewController:initVc];
[navVC setNavigationBarHidden:true];
[UIView transitionWithView:[AppDelegate getShareInstance].window duration:0.1f options:UIViewAnimationOptionTransitionCrossDissolve animations:^{
    [AppDelegate getShareInstance].window.rootViewController = navVC;
}completion:nil];
}
 

Правка # 2:

Я добавил следующий флаг:

 @property (assign, nonatomic) BOOL isCurrentlyViewShowed;
 

Который установлен на viewDidAppear (вместе с вашим предложением):

 self.isCurrentlyViewShowed = TRUE;
if(m_timer) {
    [m_timer invalidate];
    m_timer = nil;
}
m_timer = [NSTimer scheduledTimerWithTimeInterval:1.0f target:self selector:@selector(decrementSpin) userInfo:nil repeats:YES];
 

Затем для него устанавливается значение off каждый раз, когда представление закрывается:

 if(m_timer) {
    [m_timer invalidate];
    m_timer = nil;
}
self.isCurrentlyViewShowed = FALSE;
 

И, наконец, при уменьшении скорости:

 if(self.isCurrentlyViewShowed == FALSE){
    if(m_timer) {
        [m_timer invalidate];
        m_timer = nil;
    }
}

self.m_remaining_time--;  

if(self.m_remaining_time > 0) {
    NSLog(@"[TIMER] (%p)>> %d (FLAG: %d)", m_timer, self.m_remaining_time, self.isCurrentlyViewShowed);
}
else {
    NSLog(@"[TIMER] (%p)>> Time finished (FLAG: %d)", m_timer, self.isCurrentlyViewShowes);
    [m_timer invalidate];
    m_timer = nil;
    AudioServicesPlaySystemSound(kSystemSoundID_Vibrate);
}
 

Я повторил тест примерно 100 раз, и мне удалось воспроизвести его один раз!
Я оставил это представление и увидел в журнале строку:

[ТАЙМЕР] (0x2818ecfc0)>> 58 (ФЛАГ: 1) [ТАЙМЕР] (0x2818ecfc0)>> 57 (ФЛАГ: 1) …

Это означает, что каким-то образом объект просмотра повторно инициализируется? Я этого не понимаю..

Комментарии:

1. Я отлаживал подобные вещи, регистрируя m_timer адрес после каждого создания и делая недействительным, чтобы увидеть, какой путь кода оставляет более одного запущенного.

2. Покажите, как вы останавливаете таймер при переходе на следующую страницу. Вы уверены, что рассмотрели все аспекты? Вы также можете использовать блок вместо сообщения при срабатывании таймера и проверять наличие нулевого значения, например, для покрытия многих (не всех) случаев.

3. @skaak Я отредактировал первоначальный вопрос. Спасибо!

4. Не уверен — нужно посмотреть на все это. Но вот идея. Я опубликую это как код..

Ответ №1:

Это сложно, и для его изоляции требуется весь код. Но вот идея, которая может помочь. На самом деле, я думаю, вам все равно нужно сделать это так. Измените свой viewDidAppear на что-то вроде приведенного ниже.

 - viewDidAppear
{
  if ( ! m_timer )
  {
    m_timer = ...
  }
}
 

или, в зависимости от логики, для

 - viewDidAppear
{
  if ( m_timer )
  {
    [m_timer invalidate];
    m_timer = nil;
  }
  m_timer = ...
}
 

В любом случае вы будете уверены, что у вас одновременно работает только один таймер, что может быть вашей проблемой.

Комментарии:

1. интересная идея о том, что проблема заключается не в том, что таймер не остановлен, а в том, что запускается второй. Я протестирую его и дам вам знать. Другая идея, возможно, заключается в использовании флага, который будет установлен на viewDidAppear и выключен на viewDidDisappear, указывая, что представление отображается в данный момент. Таким образом, внутри decrementSpin, если флаг выключен -? аннулируйте таймер. Если включено -> делайте, как раньше. Может быть, использовать обе идеи?

2. Да — зависит от логики игры, но звучит так, как будто вам понадобятся оба. Это должно работать на iOS, но, например, в watchOS нет симметрии между viewDidAppear и viewDidDisappear. Даже в iOS у вас может, например, появиться всплывающее предупреждение, которое разрушает ожидаемую последовательность.

3. К сожалению, проблема была воспроизведена. Пожалуйста, проверьте часть редактирования # 2 из первоначального вопроса

4. Хорошо — вы вообще используете всплывающие окна? Тогда viewWillAppear / Disappear не вызывается. Но чтобы исправить это, мне нужно будет просмотреть весь код, поскольку я подозреваю, что вы пропустили один или есть некоторый дисбаланс между добавлением и удалением таймера.

5. Как я могу поделиться кодом этого ViewController? он слишком большой. Вы знаете хороший способ?