我遇到了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
方法之前,我想知道:
这个问题有更优雅的解决方案吗?
答案 0 :(得分:2)
这是一个不同步的访问问题。通知在一个线程上执行,而fsevent回调在另一个线程上执行,并且它们都同时访问ArrayController的底层数组和textview。
跨线程同步访问权限。这是通过获取对正在访问的特定资源的锁定来完成的:执行线程获取锁定,并且所有尝试访问该资源的线程将被阻塞,直到锁定线程释放锁定。更多信息可以在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];
}
}
}
这很可能会解决您当前的问题,但是,这是一个非常便宜并且非常有效,并且每次都会有效地让您的应用程序的线程相互等待。
始终在主线程上更新你的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];
});
}
}