如何在代码中退出自定义的 [NSApp run] 方法?
正如标题所示,我一直在开发一个跨平台的项目,计划通过将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);
}
当然如果有更“官方”的方式,请随时在本回答下回复;就目前而言,这将是我暂时采用的方法。