为什么 NSTimer 在 UIViewController 之外崩溃?

Why does NSTimer crash outside UIViewController?

我试图通过重新排列相关方法(包括我之前放在 UIViewController 中的 NSTimer 来提高代码的可读性。我现在需要将它们重新定位到自定义 class,以便它们可以独立于 ViewController 工作。

但是在我尝试这样做的过程中,我引入了一个 NSTimer 的问题,即使代码看起来是正确的,以前也没有这个问题。发生崩溃并出现以下错误日志:

Thread 1: EXC_BAD_ACCESS (code=1, address=0x1)

当我尝试使用断点并逐步执行代码时 Xcode 似乎陷入了此语句的循环中

    timer = [NSTimer scheduledTimerWithTimeInterval:1
                                             target:self
                                           selector:@selector(nextClock)
                                           userInfo:nil
                                            repeats:YES];

这是我的代码的精简版。 NSTimer 在一个名为 ConcertController 的 class 中,它在 PlayViewController.h

中有一个前向声明

PlayViewController.h

    #import <UIKit/UIKit.h>
    #import "ConcertController.h"

    @interface PlayViewController : UIViewController

    {    
        ConcertController *concertStateMachine;    
    }

    @end

并在 PlayViewController.m

中从 viewDidLoad 调用

PlayViewController.m

    #import "PlayViewController.h"

    @implementation PlayViewController { 

    }

    @synthesize lastEventChangeTime;

    ...
    ...

    - (void)viewDidLoad                                             
    {
      [super viewDidLoad];

      selectedFamily         = [parent getSelectedFamily];
      selectedPlayerID       = [parent getSelectedPlayerID];

      concertStateMachine    = [[ConcertController alloc] initConcertStateMachine:(int)selectedFamily
                                                                        forPlayer:(int)selectedPlayerID];

      CGRect rect            = [UIScreen mainScreen].bounds;
      float  statusBarHeight = [[UIApplication sharedApplication] statusBarFrame].size.height;
      screenFrame            = CGRectMake(0,statusBarHeight,rect.size.width,rect.size.height - statusBarHeight);
      self.view              = [[UIView alloc] initWithFrame: screenFrame];

    }

ConcertControllerPlayViewController.h

中有前向声明

ConcertController.h

    #import <UIKit/UIKit.h>

    @class PlayViewController;

    @interface ConcertController : NSObject
    {
        int        currentState;
        NSUInteger clockCount;
        int        totalMinutes;
        int        totalSeconds;
        …
        …

        NSDate*    lastEventChangeTime;
        NSTimer*   timer;
    }

    - (id)initConcertStateMachine:(int)selectedFamily 
                        forPlayer:(int)selectedPlayerID;

    @property (nonatomic, retain) NSDate *lastEventChangeTime;

    @end

ConcertController.m

    #import "PlayViewController.h"
    @implementation ConcertController

    @synthesize lastEventChangeTime;

    - (id)initConcertStateMachine:(int)selectedFamily
                        forPlayer:(int)selectedPlayerID
    {    
        [self  findEntryPointsFor:(int)selectedFamily
                        andPlayer:(int)selectedPlayerID];
        [self startClock];

        return self;
    }


    - (void)startClock
    {     
    lastEventChangeTime = [[NSDate alloc] init];

        currentState    =  0; // CLOCK_Init_CurrentState;
        clockCount      = 24; // number of seconds per state
        totalMinutes    =  0  // CLOCK_Init_totalMinutes;
        totalSeconds    =  0; // CLOCK_Init_totalSeconds;

        timer           = [NSTimer scheduledTimerWithTimeInterval:1
                                                           target:self
                                                         selector:@selector(nextClock)
                                                         userInfo:nil
                                                          repeats:YES];
    }


    - (void)nextClock                                       
    {    
    lastEventChangeTime = [NSDate date];
    clockCount++;
    [self masterClockReadout];

    if ((clockCount % (int)24) == 0) 
        {
         // [self nextState]; // other (i.e. non-UIView) code goes here
        }
    }

编辑#2

ConcertController.m 中的初始化已按照建议进行了标准化。

    - (id)initConcertStateMachine:(int)selectedFamily
                forPlayer:(int)selectedPlayerID
    {
        self = [super init];

        if (self) {

            [self findEntryPointsFor:(int)selectedFamily
                   andPlayer:(int)selectedPlayerID];        
            [self startClock];
    }
    return self;
}

编辑#1。这是要求的日志。注意:日志还会显示一些在发布的代码示例中未找到的项目(为清楚起见已删除)。格雷格

2017-07-25 17:06:30.809 SatGam2[5476:1809452] FamilySelectViewController loaded
2017-07-25 17:06:34.361 SatGam2[5476:1809452] PlayerIDSelectViewController loaded
2017-07-25 17:06:36.250 SatGam2[5476:1809452] SyncViewController loaded (Family 1 PlayerID 1)
2017-07-25 17:06:38.376 SatGam2[5476:1809452] Initialising MotionListener
2017-07-25 17:06:38.674444+1000 SatGam2[5476:1811506] [aqme] 254: AQDefaultDevice (1): skipping input stream 0 0 0x0
2017-07-25 17:06:38.692 SatGam2[5476:1809452] PlayViewController init called and AudioSession active
2017-07-25 17:06:38.693 SatGam2[5476:1809452] MIDI Event [tuningTransposition: 1 assignedPitches: 1]
2017-07-25 17:06:38.694 SatGam2[5476:1809452] Selected octave is 2
2017-07-25 17:06:38.694 SatGam2[5476:1809452] Dekany : index  MIDI Note Number     Frequency
2017-07-25 17:06:38.694 SatGam2[5476:1809452]          52     63                   662.2421
2017-07-25 17:06:38.695 SatGam2[5476:1809452]          53     64                   708.2311
2017-07-25 17:06:38.695 SatGam2[5476:1809452]          54     65                   772.6157
2017-07-25 17:06:38.695 SatGam2[5476:1809452]          55     66                   809.407
2017-07-25 17:06:38.695 SatGam2[5476:1809452]          56     67                   882.9894
2017-07-25 17:06:38.695 SatGam2[5476:1809452]          57     68                   910.5828
2017-07-25 17:06:38.696 SatGam2[5476:1809452]          58     69                   993.3631
2017-07-25 17:06:38.696 SatGam2[5476:1809452]          59     70                   1030.154
2017-07-25 17:06:38.696 SatGam2[5476:1809452]          60     73                   1158.924
2017-07-25 17:06:38.696 SatGam2[5476:1809452]          61     74                   1214.11
2017-07-25 17:06:38.697 SatGam2[5476:1809452] 
(
    "662.2421",
    "708.2311",
    "772.6157",
    "809.407",
    "882.9894",
    "910.5828",
    "993.3631",
    "1030.154",
    "1158.924",
    "1214.11"
)
2017-07-25 17:06:38.697 SatGam2[5476:1809452] concert sequence for selectedFamily 1 and selectedPlayerID 1
2017-07-25 17:06:38.697 SatGam2[5476:1809452] entryPoints
2017-07-25 17:06:38.697 SatGam2[5476:1809452]  1  1  0   0
2017-07-25 17:06:38.698 SatGam2[5476:1809452]  2  1  0   0
2017-07-25 17:06:38.698 SatGam2[5476:1809452]  3  1  0   0
2017-07-25 17:06:38.698 SatGam2[5476:1809452]  4  1  0   0
2017-07-25 17:06:38.698 SatGam2[5476:1809452]  5  0 -1  48
2017-07-25 17:06:38.698 SatGam2[5476:1809452]  6  0  0  24
2017-07-25 17:06:38.699 SatGam2[5476:1809452]  7  1  1   0
2017-07-25 17:06:38.699 SatGam2[5476:1809452]  8  1  0   0
2017-07-25 17:06:38.699 SatGam2[5476:1809452]  9  0 -1  48
2017-07-25 17:06:38.699 SatGam2[5476:1809452] 10  0  0  24
2017-07-25 17:06:38.700 SatGam2[5476:1809452] 11  1  1   0
2017-07-25 17:06:38.700 SatGam2[5476:1809452] 12  1  0   0
2017-07-25 17:06:38.700 SatGam2[5476:1809452] 13  1  0   0
2017-07-25 17:06:38.700 SatGam2[5476:1809452] 14  0 -1  96
2017-07-25 17:06:38.700 SatGam2[5476:1809452] 15  0  0  72
2017-07-25 17:06:38.700 SatGam2[5476:1809452] 16  0  0  48
2017-07-25 17:06:38.701 SatGam2[5476:1809452] 17  0  0  24
2017-07-25 17:06:38.701 SatGam2[5476:1809452] 18  1  1   0
2017-07-25 17:06:38.701 SatGam2[5476:1809452] 19  1  0   0
2017-07-25 17:06:38.701 SatGam2[5476:1809452] 20  1  0   0
2017-07-25 17:06:38.701 SatGam2[5476:1809452] 21  1  0   0
2017-07-25 17:06:38.702 SatGam2[5476:1809452] 22  0 -1 144
2017-07-25 17:06:38.702 SatGam2[5476:1809452] 23  0  0 120
2017-07-25 17:06:38.702 SatGam2[5476:1809452] 24  0  0  96
2017-07-25 17:06:38.702 SatGam2[5476:1809452] 25  0  0  72
2017-07-25 17:06:38.702 SatGam2[5476:1809452] 26  0  0  48
2017-07-25 17:06:38.702 SatGam2[5476:1809452] 27  0  0  24
2017-07-25 17:06:38.703 SatGam2[5476:1809452] 28  1  1   0
2017-07-25 17:06:38.703 SatGam2[5476:1809452] 29  1  0   0
2017-07-25 17:06:38.703 SatGam2[5476:1809452] 30  0 -1  48
2017-07-25 17:06:38.703 SatGam2[5476:1809452] 31  0  0  24
(lldb) bt
  * thread #1, queue = 'com.apple.main-thread', stop reason = EXC_BAD_ACCESS (code=1, address=0x1)
    frame #0: 0x03144383 libobjc.A.dylib`objc_release + 19
  * frame #1: 0x00081c1c SatGam2`-[ConcertController startClock](self=0x786bb480, _cmd="startClock") at ConcertController.m:46 [opt]
    frame #2: 0x00081b87 SatGam2`-[ConcertController initConcertStateMachine:forPlayer:](self=0x786bb480, _cmd="initConcertStateMachine:forPlayer:", selectedFamily=1, selectedPlayerID=1) at ConcertController.m:25 [opt]
    frame #3: 0x00093a16 SatGam2`-[PlayViewController viewDidLoad](self=0x7aa49c00, _cmd="viewDidLoad") at PlayViewController.m:154 [opt]
    frame #4: 0x014e2878 UIKit`-[UIViewController _sendViewDidLoadWithAppearanceProxyObjectTaggingEnabled] + 38
    frame #5: 0x014e7201 UIKit`-[UIViewController loadViewIfRequired] + 1434
    frame #6: 0x014e776c UIKit`-[UIViewController view] + 29
    frame #7: 0x00085c29 SatGam2`-[MultiviewViewController displayView:](self=<unavailable>, _cmd="displayView:", intNewView=<unavailable>) at MultiviewViewController.m:45 [opt]
    frame #8: 0x000825ed SatGam2`-[MultiviewAppDelegate displayView:](self=0x793a1360, _cmd="displayView:", intNewView=4) at    MultiviewAppDelegate.m:17 [opt]
    frame #9: 0x0008828e SatGam2`-[SyncViewController fromSyncButton:](self=<unavailable>, _cmd="fromSyncButton:", button=0x7b67deb0) at SyncViewController.m:65 [opt]
    frame #10: 0x03146220 libobjc.A.dylib`-[NSObject performSelector:withObject:withObject:] + 63
    frame #11: 0x0131fca0 UIKit`-[UIApplication sendAction:to:from:forEvent:] + 91
    frame #12: 0x0131fc3a UIKit`-[UIApplication sendAction:toTarget:fromSender:forEvent:] + 41
    frame #13: 0x014c7f67 UIKit`-[UIControl sendAction:to:forEvent:] + 64
    frame #14: 0x014c82d1 UIKit`-[UIControl _sendActionsForEvents:withEvent:] + 469
    frame #15: 0x014c7207 UIKit`-[UIControl touchesEnded:withEvent:] + 666
    frame #16: 0x01396526 UIKit`-[UIWindow _sendTouchesForEvent:] + 3066
    frame #17: 0x01397dea UIKit`-[UIWindow sendEvent:] + 4445
    frame #18: 0x0133e1b0 UIKit`-[UIApplication sendEvent:] + 363
    frame #19: 0x01bbac2f UIKit`__dispatchPreprocessedEventFromEventQueue + 2973
    frame #20: 0x01bb20ff UIKit`__handleEventQueue + 1255
    frame #21: 0x01bb3663 UIKit`__handleHIDEventFetcherDrain + 66
    frame #22: 0x0360aa5f    CoreFoundation`__CFRUNLOOP_IS_CALLING_OUT_TO_A_SOURCE0_PERFORM_FUNCTION__ + 15
    frame #23: 0x035f01c4 CoreFoundation`__CFRunLoopDoSources0 + 500
    frame #24: 0x035ef69c CoreFoundation`__CFRunLoopRun + 1084
    frame #25: 0x035eefd4 CoreFoundation`CFRunLoopRunSpecific + 372
    frame #26: 0x035eee4b CoreFoundation`CFRunLoopRunInMode + 123
    frame #27: 0x0516aa7a GraphicsServices`GSEventRunModal + 71
    frame #28: 0x0516a95f GraphicsServices`GSEventRun + 80
    frame #29: 0x0131dbc9 UIKit`UIApplicationMain + 148
    frame #30: 0x00080f74 SatGam2`main(argc=1, argv=0xbff82818) at main.m:12 [opt]
    frame #31: 0x05f0e779 libdyld.dylib`start + 1
    (lldb) 

如果您不使用 ARC,

然后将其更改为:

lastEventChangeTime = [NSDate date];

尝试为其添加保留

lastEventChangeTime = [[NSDate date] retain];

并且当您完成 lastEventChangeTime 后,设置

lastEventChangeTime = nil;

我不完全确定是不是这样,但我认为您不应该在初始化程序中调用 startClock。该 init 方法通常不遵循合适的初始化程序的正确结构,即

- (instancetype)init... {
    self = [super init];
    if (self) {
        // initialize properties (and ivars in your case)
    }
    return self;
}

我认为问题在于您正在安排定时器,它包含对 self 的引用,即初始化器完成之前的 ConcertController 实例,即实际上没有self还没有。特别是因为您也从未调用过 super 的初始化程序(除非您在那个 findEntryPointsFor:andPlayer: 方法中这样做,这完全违反了任何约定)。 如果您稍后调用 startClock(例如从视图控制器),它可能已经工作了,但我 真的 建议修复您的 init 以满足约定。不要忘记,尤其是在不仅仅是编码美学的 ARC 下,ARC 还依赖于某些东西来正确推断要保留和释放的内容等。

除此之外,您直接定义 ivars 的事实有点可疑。我猜这是来自 MRC 的迁移?我建议在这里也使用属性(这并不是真正的性能损失,正如许多人似乎错误地相信的那样)。唯一要记住的是,在 initializer 中,您可以使用 _ivarName(即下划线符号)访问它们并依赖其他地方的 getters 和 setters(即通常点语法,除了一些边缘情况,在这些情况下你需要避免一些键值观察的东西,但正如我从这里看到的那样,你甚至没有那个)。这确实是更清洁、更安全的方法,尤其是当您想要更换计时器等时。如果您担心保持 class 的 public 界面干净,请使用 class 扩展,这仍然比 ivars 好。

我猜你的 ConcertController 声明不好。

为您的 PlayVC 试试这个 header:

#import <UIKit/UIKit.h>
#import "ConcertController.h"

@interface PlayViewController : UIViewController

{    
     // remove this line.
}

@property (nonatomic, strong)  ConcertController *concertStateMachine;  

@end

如果不是属性,object通常会在需要内存时由iOS自由释放。在您的 ConcertController 中,您还应该更改 NSTimer* 计时器;也是属性。试一试。