34

我知道,我不是第一个尝试在 OSX 上将 Cocoa 与现有的 c/c++ 主循环一起使用的人,但我不太喜欢目前遇到的解决方案,所以我想出了一个不同的想法想讨论。我发现(在 glut、glfw、SDL 以及我认为的 QT 中)最常见的方法是使用轮询来替换 NSApplications 运行方法并自己处理事件:

nextEventMatchingMask:untilDate:inMode:dequeue:

这有一个很大的缺点,即 cpu 永远不会真正空闲,因为您必须一直轮询以检查是否有任何新事件,而且它不是 NSApplications 运行函数中唯一发生的事情,因此如果您可能会破坏一些细节使用这个替代品。

所以我想做的是保持可可 runLoop 完好无损。想象一下,您将在 c++ 中实现自己的计时器方法,这些方法通常会在您的主循环中进行管理和触发(这只是一个示例)。我的想法是将所有循环部分移动到辅助线程(因为据我所知,需要从主线程调用 NSApplication 运行),然后将自定义事件发布到我的 NSApplication 派生版本,该版本在其内部适当地处理它们发送事件:方法。例如,如果我的 c++ 循环中测量的计时器触发,我会向 NSApplication 发布一个自定义事件,该事件反过来运行我的应用程序的 loopFunc() 函数(也位于主线程中),该函数会适当地将事件发送到我的 c++ 事件链中. 首先,您认为这是一个好的解决方案吗?如果是,

otherEventWithType:location:modifierFlags:timestamp:windowNumber:context:subtype:data1:data2:

然后使用类似的东西:

[NSApp postEvent:atStart:]

通知 NSApplication。

我宁愿发布一个没有关于窗口的任何信息的事件(在 otherEventWithType 中),我可以简单地忽略那部分吗?

然后我想像这样覆盖 NSApplications sendEvent 函数:

    - (void)sendEvent:(NSEvent *)event
{
    //this is my custom event that simply tells NSApplication 
    //that my app needs an update
    if( [event type] == NSApplicationDefined)
    {
        myCppAppPtr->loopFunc(); //only iterates once
    }
        //transform cocoa events into my own input events
    else if( [event type] == NSLeftMouseDown)
    {
             ...
             myCppAppPtr->loopFunc(); //also run the loopFunc to propagate input events
    }
        ...

    //dont break the cocoa event chain
    [super sendEvent:event];

}

很抱歉这篇长文,但这一直困扰着我,因为到目前为止我对这个主题的发现并不满意。这就是我在 NSApplication 中发布和检查自定义事件的方式吗?您认为这是将可可集成到现有运行循环中而不进行轮询的有效方法吗?

4

1 回答 1

30

好吧,毕竟这花了我比我预期更多的时间,我想概述一下我尝试过的事情,并告诉你我有什么经验。这有望为人们在未来将 Cocoa 集成到现有主循环中节省大量时间。我在搜索讨论的问题时发现的第一个函数是函数

nextEventMatchingMask:untilDate:inMode:dequeue:

但正如我在问题中所说,我的主要问题是我必须不断轮询新事件,这会浪费相当多的 CPU 时间。所以我尝试了以下两种方法来简单地让我的 mainloops 更新函数从 NSApplications 主循环中调用:

  1. 将自定义事件发布到 NSApplication,覆盖 NSApplications sendEvent:函数并从那里简单地调用我的 mainloops 更新函数。与此类似:

    NSEvent* event = [NSEvent otherEventWithType: NSApplicationDefined
                                                         location: NSMakePoint(0,0)
                                                   modifierFlags: 0
                                                       timestamp: 0.0
                                                    windowNumber: 0
                                                         context: nil
                                                         subtype: 0
                                                           data1: 0
                                                           data2: 0];
                    [NSApp postEvent: event atStart: YES];
    
    
    //the send event function of my overwritten NSApplication
       - (void)sendEvent:(NSEvent *)event
    {
        //this is my custom event that simply tells NSApplication 
        //that my app needs an update
        if( [event type] == NSApplicationDefined)
        {
            myCppAppPtr->loopFunc(); //only iterates once
        }
    }
    

    这在理论上只是一个好主意,因为如果我的应用程序更新得很快(例如由于计时器快速触发),整个可可事件队列变得完全没有响应,因为我添加了太多自定义事件。所以不要用这个...

  2. 将 performSelectorOnMainThread与 cocoaFunction 一起使用,然后调用我的更新函数

    [theAppNotifier
    performSelectorOnMainThread:@selector(runMyMainLoop) withObject:nil
    waitUntilDone:NO ];
    

    这好多了,应用程序和可可 EventLoop 响应速度非常快。如果您只是想实现一些简单的事情,我建议您沿着这条路线走,因为它是这里提出的最简单的路线。无论如何,我几乎无法控制使用这种方法发生的事情的顺序(如果你有一个多线程应用程序,这一点至关重要),即当我的计时器被触发并且会做相当长的工作时,他们通常会在任何新鼠标之前重新安排时间/可以将键盘输入添加到我的 eventQueue 中,从而使整个输入变得迟缓。在由重复计时器绘制的窗口上打开垂直同步足以让这种情况发生。

  3. 毕竟我不得不回来nextEventMatchingMask:untilDate:inMode:dequeue:,经过一些试验和错误,我实际上找到了一种方法来让它在没有持续轮询的情况下工作。我的循环结构类似于:

    void MyApp::loopFunc()
    {
        pollEvents();
        processEventQueue();
        updateWindows();
        idle();
    }
    

    其中 pollEvents 和 idle 是重要的功能,基本上我使用类似的东西。

    void MyApp::pollEvents()
    {
        NSEvent * event;
    
        do
        {
            event = [NSApp nextEventMatchingMask:NSAnyEventMask untilDate:[NSDate distantPast] inMode:NSDefaultRunLoopMode dequeue:YES];
    
    
                //Convert the cocoa events to something useful here and add them to your own event queue
    
            [NSApp sendEvent: event];
        }
        while(event != nil);
    }
    

    为了在 idle() 函数中实现阻塞,我这样做了(不确定这是否好,但它似乎工作得很好!):

    void MyApp::idle()
    {
        m_bIsIdle = true;
        NSEvent * event = [NSApp nextEventMatchingMask:NSAnyEventMask untilDate:[NSDate distantFuture] inMode:NSDefaultRunLoopMode dequeue:NO];
        m_bIsIdle = false;
    }
    

    这会导致可可等到有事件发生,如果发生这种情况,则空闲简单地退出并且 loopfunc 再次启动。如果我的一个计时器(我不使用可可计时器)触发,要唤醒空闲功能,我再次使用自定义事件:

    void MyApp::wakeUp()
    {
        m_bIsIdle = false;
    
        //this makes sure we wake up cocoas run loop
        NSAutoreleasePool * pool = [[NSAutoreleasePool alloc] init];
        NSEvent* event = [NSEvent otherEventWithType: NSApplicationDefined
                                            location: NSMakePoint(0,0)
                                       modifierFlags: 0
                                           timestamp: 0.0
                                        windowNumber: 0
                                             context: nil
                                             subtype: 0
                                               data1: 0
                                               data2: 0];
        [NSApp postEvent: event atStart: YES];
        [pool release];
    }
    

    由于我之后立即清除了整个可可事件队列,因此我没有第 1 节中描述的相同问题。但是,这种方法也有一些缺点,因为我认为它并不能完成[NSApplication run]内部所做的所有事情,即应用程序委托如下:

    - (BOOL)applicationShouldTerminateAfterLastWindowClosed:(NSApplication*)theApplication
    {
          return YES;
    }
    

    似乎不起作用,无论如何我可以忍受,因为如果最后一个窗口刚刚关闭,您可以轻松检查自己。

我知道这个答案很长,但我到达那里的旅程也是如此。我希望这对某人有所帮助,并防止人们犯我所犯的错误。

于 2011-08-12T12:01:52.067 回答