将日志文件的内容连续写入NSTextview

时间:2014-05-11 16:09:45

标签: cocoa cocoa-bindings fsevents

我遇到了NSTextview的问题,它应该不断更新日志文件的内容。该应用程序是一个主要细节UI,主视图包含一组“备份”对象,而详细视图包含一个NSTabView,其中一个选项卡包含NSTextview。
基本上我想要像tail -f logfile这样的东西将它的输出放到NSTextview中。我没有使用NSTask等,而是将NSTextview的“归因字符串”绑定到我的“备份”对象的属性(所以我可以设置字体):

backup.m

- (NSAttributedString *)logContent
{
NSDictionary *attributes = @{NSFontAttributeName:[NSFont fontWithName:@"Monaco" size:12]};
NSString *str = [NSString stringWithContentsOfURL:theLogfile encoding:NSUTF8StringEncoding error:nil];
if (str) {
    NSAttributedString *attrstr = [[NSAttributedString alloc] initWithString:str attributes:attributes];
    return attrstr;
} else
    return nil;
}

然后我将FSEventStream挂钩到日志文件,每次日志文件更改时都会通知回调。在回调中,我手动通知侦听器属性已更改并向下滚动NSTextview:

backup.m

- (void)_fsEventsCallback:(NSArray *)eventPaths{
if ([eventPaths containsObject:theLogfile.path]){
    [self willChangeValueForKey:@"logContent"];
    [self didChangeValueForKey:@"logContent"];
    [_myAppDel.logTextView scrollRangeToVisible:NSMakeRange([[_myAppDel.logTextView string] length], 0)];
}}

实际删除是通过NSNotification

完成的

App Delegate.m

- (void)removeBackupObject:(NSNotification *)notification
{
if (notification.object) {
    [self.backupsArrayController removeObject:notification.object];
}
}

这很有用,我比使用NSTask更喜欢代码,但当我告诉NSArrayController删除“备份”对象时,应用程序偶尔会出现一个奇怪的错误:

Crashed Thread:  5  Dispatch queue: com.apple.root.low-priority

Exception Type:  EXC_CRASH (SIGABRT)
Exception Codes: 0x0000000000000000, 0x0000000000000000

Application Specific Information:
*** Terminating app due to uncaught exception 'NSGenericException', reason: '*** Collection <__NSSetM: 0x60000045f1a0> was mutated while being enumerated.'
terminating with uncaught exception of type NSException
abort() called

Application Specific Backtrace 1:
0   CoreFoundation                      0x00007fff8aec425c __exceptionPreprocess + 172
1   libobjc.A.dylib                     0x00007fff8a7a4e75 objc_exception_throw + 43
2   CoreFoundation                      0x00007fff8aec3b64 __NSFastEnumerationMutationHandler + 164
3   Foundation                          0x00007fff8d0e3f05 -[NSISEngine chooseOutgoingRowHeadForIncomingRowHead:] + 305
4   Foundation                          0x00007fff8d0e1aa8 -[NSISEngine minimizeConstantInObjectiveRowWithHead:] + 114
5   Foundation                          0x00007fff8d0e1623 -[NSISEngine optimize] + 147
6   Foundation                          0x00007fff8d0e851d -[NSISEngine constraintDidChangeSuchThatMarker:shouldBeReplacedByMarkerPlusDelta:] + 296
7   Foundation                          0x00007fff8d0e839e -[NSISEngine tryToChangeConstraintSuchThatMarker:isReplacedByMarkerPlusDelta:undoHandler:] + 420
8   Foundation                          0x00007fff8d0d3798 -[NSLayoutConstraint _tryToChangeContainerGeometryWithUndoHandler:] + 462
9   Foundation                          0x00007fff8d0d31b3 -[NSLayoutConstraint _setSymbolicConstant:constant:] + 402
10  AppKit                              0x00007fff8e2ac4ba -[NSView(NSConstraintBasedLayout) _autoresizingConstraints_frameDidChange] + 247
11  AppKit                              0x00007fff8e2ab25f -[NSView setFrameOrigin:] + 901
12  AppKit                              0x00007fff8e2b51b6 -[NSView setFrame:] + 259
13  AppKit                              0x00007fff8e682c2f -[NSClipView _updateOverhangSubviewsIfNeeded] + 739
14  AppKit                              0x00007fff8e2e80a1 -[NSClipView _scrollTo:animateScroll:flashScrollerKnobs:] + 1984
15  AppKit                              0x00007fff8e2e76ff -[NSClipView _reflectDocumentViewFrameChange] + 128
16  AppKit                              0x00007fff8e2ac0ac -[NSView _postFrameChangeNotification] + 203
17  AppKit                              0x00007fff8e2b5852 -[NSView setFrameSize:] + 1586
18  AppKit                              0x00007fff8e447bac -[NSTextView(NSPrivate) _setFrameSize:forceScroll:] + 764
19  AppKit                              0x00007fff8e3b222f -[NSTextView setConstrainedFrameSize:] + 633
20  AppKit                              0x00007fff8e443f70 -[NSLayoutManager(NSPrivate) _resizeTextViewForTextContainer:] + 1025
21  AppKit                              0x00007fff8e35133e -[NSLayoutManager(NSPrivate) _recalculateUsageForTextContainerAtIndex:] + 2636
22  AppKit                              0x00007fff8e343fb1 _enableTextViewResizing + 211
23  AppKit                              0x00007fff8e34a6ef -[NSLayoutManager textStorage:edited:range:changeInLength:invalidatedRange:] + 557
24  AppKit                              0x00007fff8e34a4aa -[NSTextStorage _notifyEdited:range:changeInLength:invalidatedRange:] + 149
25  AppKit                              0x00007fff8e451a2c -[NSTextStorage processEditing] + 200
26  AppKit                              0x00007fff8e44d832 -[NSTextStorage endEditing] + 110
27  Foundation                          0x00007fff8d10b434 -[NSMutableAttributedString removeAttribute:range:] + 219
28  AppKit                              0x00007fff8e4ca2c1 -[NSTextView setTextColor:] + 156
29  AppKit                              0x00007fff8ea19baf -[_NSTextPlugin showValue:inObject:] + 128
30  AppKit                              0x00007fff8e314797 -[NSValueBinder _adjustObject:mode:observedController:observedKeyPath:context:editableState:adjustState:] + 846
31  AppKit                              0x00007fff8e3143aa -[NSValueBinder _observeValueForKeyPath:ofObject:context:] + 282
32  AppKit                              0x00007fff8e314215 -[NSTextValueBinder _observeValueForKeyPath:ofObject:context:] + 43
33  Foundation                          0x00007fff8d09af28 NSKeyValueNotifyObserver + 387
34  Foundation                          0x00007fff8d0d7ed1 -[NSObject(NSKeyValueObservingPrivate) _notifyObserversForKeyPath:change:] + 1115
35  AppKit                              0x00007fff8e306d88 -[NSController _notifyObserversForKeyPath:change:] + 209
36  AppKit                              0x00007fff8e4385ff -[NSArrayController didChangeValuesForArrangedKeys:objectKeys:indexKeys:] + 125
37  AppKit                              0x00007fff8e62179f -[NSArrayController _removeObjectsAtArrangedObjectIndexes:contentIndexes:objectHandler:] + 724
38  AppKit                              0x00007fff8e621d1f -[NSArrayController _removeObjects:objectHandler:] + 502

在我调试出现问题或实施NSTask / tail -f方法之前,我想知道:

这个问题有更优雅的解决方案吗?

2 个答案:

答案 0 :(得分:2)

这是一个不同步的访问问题。通知在一个线程上执行,而fsevent回调在另一个线程上执行,并且它们都同时访问ArrayController的底层数组和textview。

选项1 - 快速和脏修复

跨线程同步访问权限。这是通过获取对正在访问的特定资源的锁定来完成的:执行线程获取锁定,并且所有尝试访问该资源的线程将被阻塞,直到锁定线程释放锁定。更多信息可以在Threadding programming guide

中找到

您的代码因此变为:

- (void)_fsEventsCallback:(NSArray *)eventPaths{
    if ([eventPaths containsObject:theLogfile.path])
        @synchronized(self.logContent) {
            [self willChangeValueForKey:@"logContent"];
            [self didChangeValueForKey:@"logContent"];
        }
        @synchronized(_myAppDel.logTextView.string) {
            [_myAppDel.logTextView scrollRangeToVisible:NSMakeRange([[_myAppDel.logTextView string] length], 0)];
        }
    }
}

- (void)removeBackupObject:(NSNotification *)notification
{
    if (notification.object) {
      @synchronized(self.backupsArrayController) {
          [self.backupsArrayController removeObject:notification.object];
      }
    }
}

这很可能会解决您当前的问题,但是,这是一个非常便宜并且非常有效,并且每次都会有效地让您的应用程序的线程相互等待。

选项2 - 更好的方法

始终在主线程上更新你的ui并在辅助线程上进行实际工作。

在辅助线程上调用FSEvents回调,您发布的NSNotification将在另一个辅助线程上响应,并且所有这些回调都对非线程安全的对象进行操作。通常,NSMutable *对象在访问时是线程安全的,但在突变时不是。换句话说,如果你改变他们的内容,你最好注意什么时候做什么。 :)

有关哪些Cocoa对象是线程安全的更多信息以及哪些不可用,请参阅我在上面提到的文档中的Thread Safety部分。 (这是非常好的阅读btw)

这个想法是告诉应用程序更新主线程上的接口,如下所示:

- (void)_fsEventsCallback:(NSArray *)eventPaths{
if ([eventPaths containsObject:theLogfile.path]){
    [self willChangeValueForKey:@"logContent"];
    [self didChangeValueForKey:@"logContent"];
    [[NSApp delegate] performSelectorOnMainThread:@selector(scrollToWhereWeNeedTo) withObject:nil];
}}

<强> AppDelegate.m

- (void)scrollToWhereWeNeedTo
{
    [self.logTextView scrollRangeToVisible:NSMakeRange([[self.logTextView string] length], 0)];
}

- (void)removeBackupObject:(NSNotification *)notification
{
    if (notification.object) {
        [[NSApp delegate] performSelectorOnMainThread:@selector(removeObjectFromArrayController) withObject:notification.object];

    }
}

- (void)removeObjectFromArrayController:(id)theObject
{
       [self.backupsArrayController removeObject:theObject];
}

你在这里有效地做的是你在主线程的runloop上安排滚动操作和删除对象操作,从而消除任何潜在的访问冲突,因为它们将一个接一个地排队

此外,请查看您的应用中可能发生访问冲突的任何其他潜在位置。

我真的希望这会有所帮助,而且不会让你更加困惑。可可起初可能是一种痛苦,但嘿,不会杀死你的东西让你更强壮!

答案 1 :(得分:0)

我在Cătălin Stan的帮助下通过在主线程上执行removeObject:调用来解决问题,如下所示:

- (void)removeBackupObject:(NSNotification *)notification
{
    if (notification.object) {
        dispatch_async(dispatch_get_main_queue(), ^{
        [self.backupsArrayController removeObject:notification.object];
        });
    }
}