简体   繁体   中英

How to have an event-loop on non-main thread in macOS?

Related to this other question : I have the need to gather information about what is the current active application, on macOS.

The linked QA answer provide a mechanism to get alerted (event) when the active application changes, but it crashes when run on a separated thread:

FocusDetector::AppFocus focus;
focus.run();

//std::thread threadListener(&FocusDetector::AppFocus::run, &focus); //Does not works
//if (threadListener.joinable())
//{
//  threadListener.join();
//}

.

    *** Assertion failure in +[NSUndoManager _endTopLevelGroupings], /xxxxxxx/NSUndoManager.m:363
2020-11-24 08:54:41.758 focus_detection[81935:18248374] *** Terminating app due to uncaught exception 'NSInternalInconsistencyException', reason: '+[NSUndoManager(NSInternal) _endTopLevelGroupings] is only safe to invoke on the main thread.'
*** First throw call stack:
(
    0   CoreFoundation            0x00007fff3006cb57 __exceptionPreprocess + 250
    1   libobjc.A.dylib           0x00007fff68eb35bf objc_exception_throw + 48
    2   CoreFoundation            0x00007fff30095d08 +[NSException raise:format:arguments:] + 88
    3   Foundation                0x00007fff32787e9d -[NSAssertionHandler handleFailureInMethod:object:file:lineNumber:description:] + 191
    4   Foundation                0x00007fff326c45ee +[NSUndoManager(NSPrivate) _endTopLevelGroupings] + 440
    5   AppKit                    0x00007fff2d25165c -[NSApplication run] + 864
    6   focus_detection           0x0000000104b1a010 _ZN13FocusDetector8AppFocus3runEv + 128
    7   focus_detection           0x0000000104b19547 _ZNSt3__1L8__invokeIMN13FocusDetector8AppFocusEFvvEPS2_JEvEEDTcldsdeclsr3std3__1E7forwardIT0_Efp0_Efp_spclsr3std3__1E7forwardIT1_Efp1_EEEOT_OS6_DpOS7_ + 119
    8   focus_detection           0x0000000104b1944e _ZNSt3__1L16__thread_executeINS_10unique_ptrINS_15__thread_structENS_14default_deleteIS2_EEEEMN13FocusDetector8AppFocusEFvvEJPS7_EJLm2EEEEvRNS_5tupleIJT_T0_DpT1_EEENS_15__tuple_indicesIJXspT2_EEEE + 62
    9   focus_detection           0x0000000104b18c66 _ZNSt3__114__thread_proxyINS_5tupleIJNS_10unique_ptrINS_15__thread_structENS_14default_deleteIS3_EEEEMN13FocusDetector8AppFocusEFvvEPS8_EEEEEPvSD_ + 118
    10  libsystem_pthread.dylib   0x00007fff6a260109 _pthread_start + 148
    11  libsystem_pthread.dylib   0x00007fff6a25bb8b thread_start + 15
)
libc++abi.dylib: terminating with uncaught exception of type NSException
Abort trap: 6

This is obviously related with NSApplication , for which the documentation state:

Every app uses a single instance of NSApplication to control the main event loop

In consequence, I am looking for another way to listen on events, which is not restricted to the main event-loop ( or main thread.

Intuitively, it should be possible to get information about the current application with focus, in a separated thread.

I have no idea how to approach this problem, sorry for not providing much research. I did researched on internet for "NSNotification not in main thread" and other similar sentences, but without success.

Question:

How to listen on activeAppDidChange NSNotification outside the main thread?

Place Observers to the following Notifications below and let them invoke your methods. NotificationCenter are singleton for good reason, while you can of course, create your own. [NSWorkspace sharedWorkspace] is a singleton i guess, and [[NSWorkspace sharedWorkspace] notificationCenter] as named - the notification center to observe. The sending object to expect is nil in your case, because you don't know which object to observe more specific.

#import <AppKit/AppKit.h>
- (void)applicationDidFinishLaunching:(NSNotification *)aNotification {
    // Insert code here to initialize your application
    [[[NSWorkspace sharedWorkspace] notificationCenter]
        addObserver:self
        selector:@selector(activeAppDidChange:)
        name:NSWorkspaceDidActivateApplicationNotification
        object:nil
    ];
    [[[NSWorkspace sharedWorkspace] notificationCenter]
        addObserver:self
        selector:@selector(activeAppDidTerminate:)
        name:NSWorkspaceDidTerminateApplicationNotification
        object:nil
    ];
    // id<NSObject> myObserver;
    _myObserver = [[[NSWorkspace sharedWorkspace] notificationCenter] 
        addObserverForName:NSWorkspaceDidHideApplicationNotification
        object:nil 
        queue:[NSOperationQueue mainQueue] 
        usingBlock:^(NSNotification * _Nonnull note) {
            // do stuff in block
            NSRunningApplication *app = note.userInfo[NSWorkspaceApplicationKey];
            NSLog(@"%u %@ %ld", 
               app.processIdentifier, 
               app.bundleIdentifier, 
               (long)app.activationPolicy
            );
        }
    ];
}
-(void)activeAppDidChange:(NSNotification *)note {
    NSLog(@"%@",note.userInfo.debugDescription);
}
-(void)activeAppDidTerminate:(NSNotification *)note {
    NSLog(@"%@",note.userInfo.debugDescription);
}

Just proof you can receive Notifications on Threads that are not mainThread.

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

NS_ASSUME_NONNULL_BEGIN

@interface ThreadManagerExample : NSObject
@property (nonatomic, readonly) BOOL started;
@property (nonatomic, readonly) uint64_t looptime;
-(void)start;
-(void)stop;
@end

NS_ASSUME_NONNULL_END
// ThreadManagerExample.m
#import "ThreadManagerExample.h"

static const double kThreadPriority = 1.0;

@interface ReceivingThread : NSThread
@property (nonatomic, weak) ThreadManagerExample * threadManager;
@end

@interface ThreadManagerExample ()
@property (nonatomic, strong) ReceivingThread *thread;
@property (nonatomic, readwrite) BOOL started;
@property (nonatomic, readwrite) uint64_t looptime;
@end

@implementation ThreadManagerExample
-(void)dealloc {
    [NSObject cancelPreviousPerformRequestsWithTarget:self selector:@selector(startThread) object:nil];
    if ( _thread ) {
        [_thread cancel];
        while ( !_thread.isFinished ) {
            [NSThread sleepForTimeInterval:0.01];
        }
    }
}
-(instancetype)init {
    if ( !(self = [super init]) ) return nil;
    _looptime = 1000000000; // 1 sec
    return self;
}

-(void)startThread {
    if ( !_thread) {
        self.thread = [ReceivingThread new];
        _thread.threadManager = self;
        _thread.name=@"ReceivingThread";
        [_thread setThreadPriority:kThreadPriority];
        [_thread start];
    }
}

-(void)start {
    if ( !_thread ) {
        @synchronized ( self ) {
            self.started = YES;
        }
        [self performSelector:@selector(startThread) withObject:nil afterDelay:0.0];
    }
}

-(void)stop {
    @synchronized ( self ) {
        self.started = NO;
    }
    if ( _thread ) {
        [_thread cancel];
        self.thread = nil;
    }
}
@end

@implementation ReceivingThread {
    BOOL somethinghappend;
    NSString *oldBundleIdentifier;
    NSString *lastbundleIdentifier;
    pid_t oldPID;
    pid_t focusedPID;
}
-(instancetype)init {
    if (!(self=[super init])) return nil;
    [[[NSWorkspace sharedWorkspace] notificationCenter] addObserver:self selector:@selector(mimi:) name:NSWorkspaceDidActivateApplicationNotification object:nil];
    return self;
}
-(void)mimi:(NSNotification*)note {
    NSRunningApplication *app = note.userInfo[NSWorkspaceApplicationKey];
    lastbundleIdentifier = app.bundleIdentifier;
    focusedPID = app.processIdentifier;
    if (![oldBundleIdentifier isEqualToString:lastbundleIdentifier]) {
        somethinghappend = YES;
    }
    oldBundleIdentifier = lastbundleIdentifier;
    oldPID = focusedPID;
}
-(void)main {
    [NSThread setThreadPriority:kThreadPriority];
    while ( !self.isCancelled ) {
        uint64_t nextLoop = 0;
        @synchronized ( _threadManager ) {
            if (somethinghappend) {
                NSLog(@"%@ %u",lastbundleIdentifier, focusedPID);
                somethinghappend = NO;
            }
            uint64_t now = mach_absolute_time();
            nextLoop = now + _threadManager.looptime;
        }
        mach_wait_until(nextLoop);
    }
}
@end

and instance and starting is done like..

if (!_manager) _manager = [[ThreadManagerExample alloc] init];
[_manager start];

stopping

[_manager stop];
_manager = nil; // if needed

So as looptime is set to 1 sec it will check each second if the last received bundleIdentifier was changed. You could extend the -(void)mimi: method by checking if the bundleIdentifier differs from [[NSBundle mainBundle] bundleIdentifier] to know if you became active yourself and ignore that when needed.

Edit from Apple Docs

NSRunningApplication is thread safe, in that its properties are returned atomically. However, it is still subject to the main run loop policy described above. If you access an instance of NSRunningApplication from a background thread, be aware that its time-varying properties may change from under you as the main run loop runs (or not).

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM