在主线程上更新NSMenu时崩溃

时间:2013-03-06 20:16:12

标签: objective-c nsthread nsmenu

我正在尝试以编程方式更新NSMenu。问题是更新NSMenu的代码不在主线程中运行。我最初忘记了这个小细节 - 导致我的代码在不同的线程尝试更新菜单时崩溃了。我试图通过显式获取更新代码在主线程中运行来解决此问题。

更新菜单的代码全部在AppDelegate中,它看起来像这样:

-(void)buildMenu{
    dispatch_block_t codeForExecutionOnMainThread = ^{ 
        //Need to empty menu
        int i=[devicemenu numberOfItems]-7; //7 is the number of permanent menu items.
        while (i-->0)
            [devicemenu removeItemAtIndex:i];

        //Need to iterate through connectedDevices Array
        NSEnumerator *e = [connectedDevices objectEnumerator];
        id device;
        while (device = [e nextObject]){
            [self newMenuItem:[device objectForKey:@"DeviceName"]
                   parentMenu:devicemenu
                   deviceID:[[device objectForKey:@"DeviceID"] unsignedIntValue]];
        }

    };
    if ([NSThread isMainThread]){
        codeForExecutionOnMainThread();
    }
    else{
        dispatch_sync(dispatch_get_main_queue(), codeForExecutionOnMainThread);
    }
}

令人讨厌的是,它只是移动了崩溃。原始崩溃发生在请求构建菜单的线程上,现在崩溃现在发生在主线程上(正如人们所预料的那样)。此外,只要单击NSMenu,就会发生崩溃(之前NSMenu显示正常 - 它只是无法从中删除条目)。

devicemenu在AppDelegate中声明如下:

@interface AppDelegate : NSObject <NSApplicationDelegate,NSUserNotificationCenterDelegate>
{
    IBOutlet NSMenu *devicemenu;
}

其余的devicemenu是在界面构建器中构建的。

为了绝对清楚,崩溃不在此代码中。只是如果我从这段代码中删除线程内容,那么当我点击菜单时就不会发生崩溃(而是当从菜单中删除一个条目时就会发生崩溃)。有时代码也不会崩溃 - 但在这种情况下,NSMenu也不起作用。

我得到的崩溃,当它崩溃而不是什么都不做时,可以在线程1中重现:EXC_BAD_ACCESSobjc_msgSend来自NSApplicationMain来自main

我知道 - 我只是走了,让问题变得更糟。我觉得我正朝着正确的方向迈出一步 - 至少菜单上的所有东西都在同一个线程中......

我希望这只是一个明显的错误。是什么阻止了NSMenu被包含在一堆线程中的工作?最后,如果它是相关的,我的应用程序没有窗口 - 它只是菜单,菜单位于菜单栏的右侧。

堆栈跟踪如下:

   * thread #1: tid = 0x1d07, 0x96edba87 libobjc.A.dylib`objc_msgSend + 23, stop reason = EXC_BAD_ACCESS (code=2, address=0x11e)
    frame #0: 0x96edba87 libobjc.A.dylib`objc_msgSend + 23
    frame #1: 0x95aa75d4 CoreFoundation`CFStringCreateCopy + 84
    frame #2: 0x965485f4 HIToolbox`_InsertMenuItemTextWithCFString(MenuData*, __CFString const*, unsigned short, unsigned long, unsigned long) + 34
    frame #3: 0x96378924 HIToolbox`InsertMenuItemTextWithCFString + 63
    frame #4: 0x93e6044a AppKit`-[NSCarbonMenuImpl _carbonMenuInsertItem:atCarbonIndex:] + 550
    frame #5: 0x94088e76 AppKit`-[NSCarbonMenuImpl _privatePopulateCarbonMenu] + 237
    frame #6: 0x9408fac8 AppKit`-[NSCarbonMenuImpl _populatePrivatelyIfNecessary] + 67
    frame #7: 0x9408fa73 AppKit`-[NSCarbonMenuImpl _checkoutMenuRefWithToken:creating:populating:] + 330
    frame #8: 0x9411b909 AppKit`-[NSCarbonMenuImpl _maximumSizeForScreen:] + 73
    frame #9: 0x942954a4 AppKit`-[NSMenu size] + 60
    frame #10: 0x94397191 AppKit`+[NSStatusBarButtonCell popupStatusBarMenu:inRect:ofView:withEvent:] + 427
    frame #11: 0x94396d0e AppKit`-[NSStatusBarButtonCell trackMouse:inRect:ofView:untilMouseUp:] + 142
    frame #12: 0x93f50db9 AppKit`-[NSControl mouseDown:] + 867
    frame #13: 0x93f48a21 AppKit`-[NSWindow sendEvent:] + 6968
    frame #14: 0x94397b19 AppKit`-[NSStatusBarWindow sendEvent:] + 75
    frame #15: 0x93f43a0f AppKit`-[NSApplication sendEvent:] + 4278
    frame #16: 0x93e5d72c AppKit`-[NSApplication run] + 951
    frame #17: 0x93e006f6 AppKit`NSApplicationMain + 1053
    frame #18: 0x000042cb DeviceMenu`main(argc=3, argv=0xbffffac0) + 43 at main.m:13

这是填充菜单的地方。正如我所说,所有这些东西都可以正常工作 - 只要线程不在那里。但是要删除设备需要进行线程处理。最令人沮丧的。谢谢你花时间看看这个。

- (void)newMenuItem:(NSString*)menuName 
     parentMenu:(NSMenu*)parentMenu
       deviceid:(unsigned)deviceid
       parentid:(unsigned)parentid
         fullid:(unsigned)fullid
{
    if (((deviceid&0x00ffffff)==0) && (parentid==0))
    {
        NSMenuItem *newMenu = [[NSMenuItem alloc] initWithTitle:menuName
                                                         action:@selector(deviceClicked:)
                                                  keyEquivalent:@""];
        [newMenu setTarget:self];

        [newMenu setTag:deviceid>>24&0xff];
        [newMenu setRepresentedObject:[NSNumber numberWithUnsignedInt:fullid]];

        [parentMenu insertItem:newMenu atIndex:0];
    }
    else
    {
        //child
        unsigned parentTag=(parentid==0)?(deviceid>>24&0xff):deviceid>>28&0xf;

        unsigned shift=(parentid==0)?8:4;
        unsigned mytag=(deviceid<<shift)>>28;
        bool lastItem = !((deviceid<<shift)<<4);

        if (![[parentMenu itemWithTag:parentTag] hasSubmenu])
        {
            //need to add the submenu here
            NSMenu *submenu = [[NSMenu alloc] init];
            [submenu addItemWithTitle:[[parentMenu itemWithTag:parentTag] title]
                               action:@selector(deviceClicked:)
                        keyEquivalent:@""];

            [[parentMenu itemWithTag:parentTag] setSubmenu:submenu];
        }

        if(lastItem)
        {
            NSMenuItem *newMenu = [[NSMenuItem alloc] initWithTitle:menuName
                                                             action:@selector(deviceClicked:)
                                                      keyEquivalent:@""];
            [newMenu setTarget:self];
            [newMenu setTag:mytag];
            [newMenu setRepresentedObject:[NSNumber numberWithUnsignedInt:fullid]];

            [[[parentMenu itemWithTag:parentTag] submenu] insertItem:newMenu atIndex:1];
        }
        else
        {
            [self newMenuItem:menuName parentMenu:[[parentMenu itemWithTag:parentTag]submenu] deviceid:deviceid<<shift parentid:mytag fullid:fullid];
        }
    }
}

1 个答案:

答案 0 :(得分:0)

感谢@trojanfoe的帮助以及他给我的提示,我能够自己找到(并修复)这个 - 尽管我仍然不确定发生了什么。

这个问题归结于connectedDevices数组的构造。此数组的每个字典中的一个项称为DeviceName,它用于提供菜单的名称。 DeviceName是根据一组规则使用NSMutableString构造的 - 我将它分配,使用并放入connectedDevices数组中的字典中。

我的理解显然是有缺陷的,NSArray会包含放入其中的对象的副本。在这种情况下,似乎没有 - 因为当我释放NSMutableString时,它会导致菜单崩溃。如果我不释放它,那么(触摸木头)一切似乎都没问题。

我无法解释为什么线程代码应该导致此问题表现出来。而且它只是表明撞击的位置可能距离爆破轮胎的钉子几英里。

感谢大家帮忙解决此问题。