Wie stoppe ich die NSApp run () -Ereignisschleife des Hauptthreads ()? (eingebettet in C ++)

Dec 21 2020

Hier ist ein minimal reproduzierbares Beispiel. Wenn das Lesen zu lang ist, fahren Sie mit dem nächsten Abschnitt mit dem Problem fort und untersuchen Sie den Code bei Bedarf.

Minimales Beispiel:

Nehmen wir eine einfache C ++ - Befehlszeile an:

main.cpp

#include <iostream>
#include "Wrapper.h"
int main()
{
    Wrapper wrapper;
    wrapper.run();
    std::cout << "Exiting" << std::endl;
}

Der Objective-C-Wrapper-Header: Wrapper.h

struct OCWrapper;
class Wrapper
{
public:
    Wrapper() noexcept;
    virtual ~Wrapper() noexcept;
    void run();
private:
    OCWrapper* impl=nullptr;
};

Und es Implementierung: Wrapper.mm

#import "Wrapper.h"
#import "MyOCApp.h"

struct OCWrapper
{
    MyOCApp* wrapped=nullptr;
};

Wrapper::Wrapper() noexcept: impl(new OCWrapper)
{
    impl->wrapped = [[ MyOCApp alloc] init];
}

Wrapper::~Wrapper() noexcept
{
    [impl->wrapped release];
    delete impl;
}

void Wrapper::run()
{
    [impl->wrapped run];
}

Und schließlich der interessante Teil in Objective-C, MyOCApp.h:

#import <AppKit/AppKit.h>
#import <Foundation/Foundation.h>

@interface MyOCApp: NSObject
@end

@implementation MyOCApp
- (id)init 
{
    [[NSNotificationCenter defaultCenter] addObserver:self
                selector:@selector(applicationDidFinishLaunching:)
                name:NSApplicationDidFinishLaunchingNotification object:nil];
    
    return self;
}

- (void)run
{
    [self performSelector:@selector(shutdown:) withObject:nil afterDelay: 2];
    //CFRunLoopRun();
    
    [NSApplication sharedApplication];
    [NSApp run];
}

- (void) shutdown:(NSNotification *) notif
{
    NSLog(@"Stopping");
    //CFRunLoopStop(CFRunLoopGetCurrent());
    [NSApp stop:self];
}

- (void) applicationDidFinishLaunching:(NSNotification *) notif
{
    NSLog(@"Application ready");
}
@end

CMakeLists.txt

cmake_minimum_required (VERSION 3.10.0)
cmake_policy( SET CMP0076 NEW)

set(CMAKE_CXX_STANDARD 17)

project(ocapp)

add_executable(${PROJECT_NAME}) find_library(APP_KIT AppKit) find_library(CORE_FOUNDATION CoreFoundation) target_link_libraries( ${PROJECT_NAME} ${APP_KIT} ${CORE_FOUNDATION} )

target_sources( ${PROJECT_NAME} PRIVATE "main.cpp" "Wrapper.mm" PUBLIC "Wrapper.h" "MyOCApp.h" )

Das Projekt kann mit folgenden Befehlen erstellt werden:

$ cmake -G Xcode.
$ open ocapp.xcodeproj

Das Problem:

Bei Verwendung von [NSApp run]und [NSApp stop:self]kann ich die Ereignisschleife nicht stoppen, sodass sie unbegrenzt weiter ausgeführt wird.

Anwendung beendet Starten
Stoppen
.....
Getötet: 9

Bei Verwendung von CFRunLoopRun()und CFRunLoopStop(CFRunLoopGetCurrent())wird es korrekt gestartet / gestoppt, jedoch applicationDidFinishLaunchingnie ausgelöst.

Stoppen
Terminating

Die Frage:

Warum ist das? und wie funktionieren beide Funktionen?

Antworten

2 Kamil.S Jan 06 2021 at 03:56

Das Problem liegt nicht im angehängten Code. Ihre bestehende Variante

[NSApplication sharedApplication];
[NSApp run];

und

[NSApp stop:self];

ist richtig.

Der Schuldige ist dein CMakeLists.txt. Die von Ihnen eingeschlossene Datei erstellt eine ausführbare Binärdatei. Das ist in Ordnung für eine Konsolen-App, aber keine gültige MacOS-App, die aus dem Ordner AppName.app und einer Reihe anderer Dateien besteht. Da Sie die AppKit-API ohne das richtige Gerüst einer MacOS-App verwenden, funktioniert dies nicht.

Eine minimale Lösung in Ihrem CMakeLists.txtist:

add_executable(
    ${PROJECT_NAME}
    MACOSX_BUNDLE
)

Jetzt haben Sie ein korrektes App-Ziel in Xcode. Weitere Beispiele für CMakeLists.txtfür MacOS geeignete Apps finden Sie im Internet.

Update
Also habe ich es weiter untersucht und die Exit-Routine in überprüft
-[NSApplication run]( +[NSApp run]ist ein Synonym für Kompatibilität, aber die eigentliche Implementierung ist in -[NSApplication run]). Wir können einen symbolischen Haltepunkt durch lldb wie folgt setzen: b "-[NSApplication run]"Das Snippet von Interesse (für X86-64) ist:

->  0x7fff4f5f96ff <+1074>: add    rsp, 0x98
    0x7fff4f5f9706 <+1081>: pop    rbx
    0x7fff4f5f9707 <+1082>: pop    r12
    0x7fff4f5f9709 <+1084>: pop    r13
    0x7fff4f5f970b <+1086>: pop    r14
    0x7fff4f5f970d <+1088>: pop    r15
    0x7fff4f5f970f <+1090>: pop    rbp
    0x7fff4f5f9710 <+1091>: ret  

Wir können überprüfen, ob ein Haltepunkt, an dem Pfeilspitzen getroffen werden, nur in der gebündelten Variante, nicht jedoch in der "nackten" ausführbaren Variante getroffen wird. Nach weiteren Recherchen fand ich diese Antworthttps://stackoverflow.com/a/48064763/5329717das ist sehr hilfreich. Das Schlüsselzitat von @Remko lautet:

Es scheint, dass die Anforderung zum Stoppen der UI-Schleife nur nach einem UI-Ereignis verarbeitet wird (also nicht nur nach einem Hauptschleifenereignis).

Und das ist tatsächlich der Fall. Wenn in der "nackten" ausführbaren Variante wir hinzufügen

- (void) shutdown:(NSNotification *) notif
{
    NSLog(@"Stopping");
    //CFRunLoopStop(CFRunLoopGetCurrent());
    [NSApp stop:self];
    [NSApp abortModal]; //this is used for generating a UI NSEvent
}

Wir bekommen das gewünschte Verhalten und die App wird normal beendet. Ihre "nackte" App-Variante ist also keine korrekte MacOS-App und empfängt daher keine UI-Ereignisse (der Runloop funktioniert trotzdem korrekt).

Auf der anderen Seite ist ein korrektes MacOS-App-Bundle mit Info.plistusw. erforderlich, damit MacOS ein App-Fenster, ein Dock-Symbol usw. einrichten kann.

Auf lange Sicht empfehle ich, entweder eine reine Konsolen-App zu wählen, wenn Sie AppKit überhaupt nicht benötigen, oder Dinge nach dem Buch zu tun. Andernfalls werden Sie auf solche Anomalien stoßen.