【问题标题】:How to have an event-loop on non-main thread in macOS?如何在 macOS 的非主线程上有一个事件循环?
【发布时间】:2021-03-06 02:34:26
【问题描述】:

this other question 相关:我需要收集有关macOS 上当前活动应用程序的信息。

链接的 QA 答案提供了一种机制,可在活动应用程序更改时收到警报(事件),但在单独的线程上运行时会崩溃:

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

这显然与NSApplication有关,文档说明:

每个应用都使用一个 NSApplication 实例来控制主事件循环

因此,我正在寻找另一种监听事件的方法,它不限于主事件循环(或主线程。

直观地说,应该可以在单独的线程中获取有关当前应用程序焦点的信息。

我不知道如何解决这个问题,抱歉没有提供太多研究。我确实在互联网上研究了“NSNotification不在主线程中”和其他类似的句子,但没有成功。

问题:

如何在主线程外监听activeAppDidChangeNSNotification?

【问题讨论】:

  • 好吧,在 c++ 类中包装通知观察器的使用不会改变它们发布的线程。
  • 正确,但是一旦我取消注释创建线程并在内部调用 AppFocus::run 的行,它就会崩溃,“只能在主线程上安全调用”,所以我猜它是与在线程内运行 NSApplication::run 有关。
  • +[NSUndoManager(NSInternal) _endTopLevelGroupings] is only safe to invoke on the main thread. 这是一个远离 NSWorkspaceDidActivateApplicationNotification 的问题。你想做什么,你的目标是什么?在不同的线程中请求另一个应用程序状态需要注意其他应用程序的主线程消失。
  • 我的目标是什么?:一个后台应用程序,需要检测哪个应用程序(PID)有焦点。主线程不可用(这意味着重构太多,可能有数十万行)。我可以创建一个侦听器线程,但它不会是主线程。链接的答案是一个完整的示例,到目前为止创建线程的行已被注释。
  • 你知道我的例子就是这样做的..pid_t pid = ((NSRunningApplication *)note.userInfo[NSWorkspaceApplicationKey]).processIdentifier;,这个例子有一个单独的线程和一个线程管理器。

标签: objective-c multithreading macos events nsworkspace


【解决方案1】:

将观察者置于下面的以下通知中,并让他们调用您的方法。 NotificationCenter 是singleton 有充分的理由,而您当然可以创建自己的。我猜[NSWorkspace sharedWorkspace] 是一个单身人士,[[NSWorkspace sharedWorkspace] notificationCenter] 被命名为 - 要​​观察的通知中心。在您的情况下,期望的发送对象是 nil,因为您不知道要观察哪个对象更具体。

#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);
}

【讨论】:

  • 感谢您的回答。我试图理解它的含义:1)“将观察者放在......”这似乎添加了新的事件监听器。 2)我也不确定为什么单例和主线程是相关的。 3) 最后,我完全没有得到关于“nil”的评论。
  • 在我的 IDE 上,cmd+click 将跟随一个令牌,对其进行声明。
  • 为了更清楚,如果这个答案只是建议在主线程中创建一个事件循环,我就是不能。
  • NotificationCenter 是线程安全的,您可以在所需的线程中添加观察者。只是再次测试以非常确定。也适用于[NSNotificationCenter defaultCenter][NSWorkspace sharedWorkspace] notificationCenter]developer.apple.com/library/archive/documentation/Cocoa/…
  • 我再做一个回答,向您展示概念证明。你会告诉我这是否是 mainThread 上的额外事件.. 交易?
【解决方案2】:

只是证明您可以在非 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

实例和启动就像..

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

停止

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

因此,当循环时间设置为 1 秒时,它将每秒检查最后收到的 bundleIdentifier 是否已更改。您可以通过检查 bundleIdentifier 是否与 [[NSBundle mainBundle] bundleIdentifier] 不同来扩展 -(void)mimi: 方法,以了解您是否自己变得活跃并在需要时忽略它。

编辑来自Apple Docs

NSRunningApplication 是线程安全的,因为它的属性是原子返回的。但是,它仍然受制于上述主运行循环策略。如果您从后台线程访问 NSRunningApplication 的实例,请注意,当主运行循环运行(或不运行)时,它的随时间变化的属性可能会在您的下方发生变化。

【讨论】:

  • 为了测试这段代码,我在objective-c中创建了一个空的控制台项目。我在项目中复制了这两个文件,并添加了一个带有 main() 函数的 main.m。主函数alloc/init/启动一个ThreadManagerExample,等待10s,释放一切。我可以看到 ::start() 被调用,但除此之外,程序什么也不做。
  • 控制台项目不会永久运行,除非您让它们这样做。最好使用模板中的 NSApplication 进行尝试,并将 #import "ThreadManagerExample.h" 放在 ViewController.h 的标题中,并将 alloc/init & start 放在 -(void)ViewDidLoad 中。然后在 xcode 中查看你的 console.log ..
  • 这就是我睡 10 秒的原因。
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2020-02-17
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2021-03-31
相关资源
最近更新 更多