如何在代码中退出自定义的 [NSApp run] 方法?

编程语言 2026-07-09

正如标题所示,我一直在开发一个跨平台的项目,计划通过将C 与Objective-C结合,在不使用Xcode的情况下,面向macOS后端进行编程来创建应用程序,类似GLFW、Qt、wxWidgets等等。

目前,我在应用程序的运行循环上遇到一个问题,传统上你会调用 [NSApp run](或隐式调用 NSApplicationMain(argc, argv)),于是我决定实现自己的运行事件循环(见下文)。

@autoreleasepool {
        // We call `[NSApp run]` here as a workaround for menubar issues caused
        // by unbundled apps
        if (![[NSRunningApplication currentApplication] isFinishedLaunching]) {
            [NSApp run];
        }

        [NSApp finishLaunching];
        do {
            NSEvent *event = [NSApp nextEventMatchingMask:NSEventMaskAny
                                        untilDate:[NSDate distantFuture]
                                        inMode:NSDefaultRunLoopMode
                                        dequeue:YES];

            if (event == nil) {
                break;
            }

            [NSApp sendEvent:event];
            [NSApp updateWindows];
        } while (!applicationShouldTerminate);
   }

虽然这确实能让应用保持运行,且 NSEvent 能正确触发,但当我关闭最后一个窗口,应用还是保持打开。进一步检查后,似乎窗口计数没有从1 降下去,因此永远不会降到零。

这对我来说有些奇怪,因为我有一个子类化的应用程序代理,我将其附加到全局的 NSApp,我的代理的 applicationShouldTerminateAfterLastWindowClosed: 方法返回 YES,在我的 applicationDidFinishLaunching: 方法中我调用 [NSApp stop:nil],它会立即终止默认的运行循环并将执行交给我的自定义运行循环。

我对Cocoa开发(尤其是程序化的Cocoa开发)相当新,因此欢迎任何可能对我的情况有帮助的反馈!

以下是我的代码的最小可工作版本:

// Compile using `clang -o app main.m -framework Cocoa`
#import <Cocoa/Cocoa.h>

@interface AppDelegate : NSObject <NSApplicationDelegate>

@end

@implementation AppDelegate

- (void)applicationDidFinishLaunching:(NSNotification *)notification {
    [NSApp stop:nil];
}

- (NSApplicationTerminateReply) applicationShouldTerminate:(NSApplication *)sender {
    return NSTerminateNow;
}

- (void)applicationWillTerminate:(NSNotification *)notification {
}

- (BOOL)applicationShouldTerminateAfterLastWindowClosed:(NSNotification *)notification {
    return YES;
}

@end

int main(int argc, const char *argv[]) {
    @autoreleasepool {
        [NSApplication sharedApplication];
        [NSApp setActivationPolicy:NSApplicationActivationPolicyRegular];
        AppDelegate *delegate = [[AppDelegate alloc] init];
        [NSApp setDelegate:delegate];
        [NSApp activateIgnoringOtherApps:YES];

        NSUInteger windowStyle = NSWindowStyleMaskTitled
        | NSWindowStyleMaskClosable
        | NSWindowStyleMaskMiniaturizable
        | NSWindowStyleMaskResizable;
        NSRect frame = NSMakeRect(0, 0, 600, 400);
        NSWindow *window = [[NSWindow alloc] initWithContentRect:frame
                                                       styleMask:windowStyle
                                                         backing:NSBackingStoreBuffered
                                                           defer:NO];

        [window setTitle:@"Simple Window"];
        [window makeKeyAndOrderFront:NSApp];

        NSWindowController *controller = [[NSWindowController alloc] initWithWindow:window];
        [window orderFrontRegardless];

        // We call `[NSApp run]` here as a workaround for menubar issues caused by
        // unbundled apps
        if (![[NSRunningApplication currentApplication] isFinishedLaunching]) {
            [NSApp run];
        }

        [NSApp finishLaunching];
        for (;;) {
            NSEvent *event = [NSApp nextEventMatchingMask:NSEventMaskAny
                                                untilDate:[NSDate distantFuture]
                                                   inMode:NSDefaultRunLoopMode
                                                  dequeue:YES];

            if (event == nil) {
                break;
            }

            [NSApp sendEvent:event];
            [NSApp updateWindows];
        }
    }

    return 0;
}

解决方案

正如 @Willeke提示的那样,使用自定义事件循环时,当最后一个窗口被关闭,我们需要向应用程序发出终止信号。

出于某种原因,当我关闭一个窗口时,[NSApp windows] 数组并不会删除关联的窗口对象,因此窗口计数([[NSApp windows] count] )不会递减。

我怀疑一个可行的解决办法是让一个子类化的 NSWindowDelegate 拥有一个 NSWindow,其中为你创建的每个窗口都分配一个窗口代理……

不过现在,我的一个变通解决办法是自己记录已经创建了多少个窗口,并设置一个观察者来跟踪任意窗口何时关闭,从而允许我在窗口计数降至零之前逐步减少自己的窗口计数器,最后再设定一个布尔值,表示应用应当终止。

下面是我最终的解决方案,供感兴趣或遇到同样问题的人参考!

//
//  main.m
//  custom-run-loop
//
//  Created by Michael Warrick on 22/04/2026.
//

#import <Cocoa/Cocoa.h>

int windowCount;
BOOL applicationShouldTerminate;

void create_application(void);
void create_window(void);
void run_application(void);

@interface AppDelegate : NSObject <NSApplicationDelegate>

@end

@implementation AppDelegate

- (void)applicationDidFinishLaunching:(NSNotification *)notification {
    [NSApp stop:nil];

    [[NSNotificationCenter defaultCenter] addObserver:self
                                             selector:@selector(windowWillClose:)
                                                 name:NSWindowWillCloseNotification
                                               object:nil];
}

- (void) windowWillClose:(NSNotification *)notification {
    --windowCount;
    if (windowCount == 0) {
        applicationShouldTerminate = YES;
    }
}

- (NSApplicationTerminateReply) applicationShouldTerminate:(NSApplication *)sender {
    return NSTerminateNow;
}

- (void)applicationWillTerminate:(NSNotification *)notification {
}

- (BOOL)applicationShouldTerminateAfterLastWindowClosed:(NSNotification *)notification {
    return YES;
}

@end

int main(int argc, const char * argv[]) {
    @autoreleasepool {
        create_application();
        create_window();
        run_application();
    }

    return 0;
}

void create_application(void) {
    [NSApplication sharedApplication];
    [NSApp setActivationPolicy:NSApplicationActivationPolicyRegular];
    AppDelegate *delegate = [[AppDelegate alloc] init];
    [NSApp setDelegate:delegate];
    [NSApp activateIgnoringOtherApps:YES];

    windowCount = 0;
    applicationShouldTerminate = NO;
}

void create_window(void) {
    NSUInteger windowStyle = NSWindowStyleMaskTitled
    | NSWindowStyleMaskClosable
    | NSWindowStyleMaskMiniaturizable
    | NSWindowStyleMaskResizable;
    NSRect frame = NSMakeRect(0, 0, 600, 400);
    NSWindow *window = [[NSWindow alloc] initWithContentRect:frame
                                                   styleMask:windowStyle
                                                     backing:NSBackingStoreBuffered
                                                       defer:NO];

    [window setTitle:@"Simple Window"];
    [window makeKeyAndOrderFront:NSApp];

    NSWindowController *controller = [[NSWindowController alloc] initWithWindow:window];
    [window orderFrontRegardless];

    ++windowCount;
}

void run_application(void) {
    if (![[NSRunningApplication currentApplication] isFinishedLaunching]) {
        [NSApp run];
    }

    [NSApp finishLaunching];
    do {
        NSEvent *event = [NSApp nextEventMatchingMask:NSEventMaskAny
                                            untilDate:[NSDate distantFuture]
                                               inMode:NSDefaultRunLoopMode
                                              dequeue:YES];

        if (event == nil) {
            break;
        }

        [NSApp sendEvent:event];
        [NSApp updateWindows];
    } while (!applicationShouldTerminate);
}

当然如果有更“官方”的方式,请随时在本回答下回复;就目前而言,这将是我暂时采用的方法。

站内所有文章版权归属LeftHeroAI导航站,无授权禁止任何主体转载、抄袭、复制内容,亦不得私自架设镜像站点。一经侵权,本站将通过法律途径追责。

相关文章