Bagaimana cara menghentikan () thread utama NSApp run () event loop? (disematkan di C ++)

Dec 21 2020

Berikut adalah contoh minimal yang dapat direproduksi, jika terlalu panjang untuk dibaca, lanjutkan ke bagian berikutnya dengan masalah tersebut, dan kemudian jelajahi kodenya jika diperlukan.

Contoh minimal:

Misalkan baris perintah C ++ sederhana:

main.cpp

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

Header pembungkus Objective-C: Wrapper.h

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

Dan implementasinya: 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];
}

Dan akhirnya bagian yang menarik, di 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" )

Proyek dapat dibangun dengan perintah berikut:

$ cmake -G Xcode.
$ open ocapp.xcodeproj

Masalah:

Saat menggunakan [NSApp run]dan [NSApp stop:self], saya tidak dapat menghentikan perulangan peristiwa, sehingga terus berjalan tanpa batas.

Aplikasi selesai meluncurkan
Berhenti
.....
Dibunuh: 9

Saat menggunakan CFRunLoopRun()dan CFRunLoopStop(CFRunLoopGetCurrent()), itu mulai / berhenti dengan benar, tetapi applicationDidFinishLaunchingtidak pernah dipicu.

Berhenti
Mengakhiri

Pertanyaan:

Kenapa ini? dan bagaimana cara agar kedua fitur tersebut berfungsi?

Jawaban

2 Kamil.S Jan 06 2021 at 03:56

Masalahnya bukan pada kode terlampir. Varian Anda yang sudah ada

[NSApplication sharedApplication];
[NSApp run];

dan

[NSApp stop:self];

benar.

Pelakunya adalah milik Anda CMakeLists.txt. Yang Anda sertakan membuat biner yang dapat dieksekusi. Itu bagus untuk aplikasi konsol tetapi ini bukan aplikasi MacOS yang valid yang terdiri dari folder AppName.app dan banyak file lainnya. Karena Anda menggunakan AppKit API tanpa perancah yang tepat dari aplikasi MacOS, itu tidak berfungsi.

Sebuah memperbaiki minimal di Anda CMakeLists.txtyaitu:

add_executable(
    ${PROJECT_NAME}
    MACOSX_BUNDLE
)

Sekarang Anda akan memiliki target Aplikasi yang benar di Xcode. Anda dapat mencari contoh lebih lanjut yang CMakeLists.txtsesuai untuk aplikasi MacOS di Internet.

Perbarui
Jadi saya menyelidikinya lebih lanjut dan memeriksa rutinitas keluar di
-[NSApplication run]( +[NSApp run]adalah sinonim kiri untuk kompatibilitas tetapi implementasi sebenarnya ada -[NSApplication run]). Kita dapat mengatur breakpoint simbolik melalui lldb seperti ini: b "-[NSApplication run]"potongan yang menarik (untuk X86-64) adalah:

->  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  

Kami dapat memverifikasi bahwa breakpoint di mana titik panah hanya dicapai dalam varian yang dipaketkan, tetapi tidak pada varian yang dapat dieksekusi "polos". Setelah penelitian lebih lanjut, saya menemukan jawaban inihttps://stackoverflow.com/a/48064763/5329717yang sangat membantu. Kutipan utama oleh @Remko adalah:

tampaknya permintaan penghentian loop UI hanya diproses setelah peristiwa UI (jadi tidak hanya setelah peristiwa loop utama).

Dan memang itulah masalahnya. Jika dalam varian eksekusi "telanjang" kami menambahkan

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

Kami mendapatkan perilaku yang diinginkan dan aplikasi berhenti secara normal. Jadi, varian aplikasi "polos" Anda bukanlah aplikasi MacOS yang benar, sehingga tidak menerima kejadian UI (runloop-nya bekerja dengan benar).

Di sisi lain, memiliki bundel aplikasi MacOS yang tepat dengan Info.plistdll diperlukan untuk MacOS untuk mengatur jendela aplikasi, ikon Dock, dll.

Dalam jangka panjang saya merekomendasikan baik menggunakan aplikasi konsol murni jika Anda tidak membutuhkan AppKit sama sekali atau melakukan sesuatu berdasarkan buku. Jika tidak, Anda akan mengalami anomali seperti itu.