Постоянно записывать содержимое файла журнала в NSTextview
У меня проблемы с NSTextview, который должен постоянно обновляться с содержимым файла журнала. Приложение представляет собой пользовательский интерфейс master-detail, главное представление содержит массив "резервных" объектов, а подробное представление содержит NSTabView с одной из вкладок, содержащих NSTextview.
В основном я хочу что-то вроде tail -f logfile
положить его вывод в NSTextview. Вместо того, чтобы использовать NSTask и т. Д., Я решил связать "Attributed String" 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
:
Приложение 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 ответа
Это проблема несинхронизированного доступа. Уведомление выполняется в одном потоке, а обратный вызов fsevent - в другом, и оба они одновременно обращаются к базовому массиву ArrayController и к текстовому представлению.
Вариант 1 - Быстрое и грязное исправление
Синхронизировать доступ по потокам. Это делается путем получения блокировки на конкретный ресурс, к которому осуществляется доступ: исполняющий поток получает блокировку, и все потоки, которые пытаются получить доступ к этому ресурсу, будут заблокированы, пока блокирующий поток не снимет блокировку. Более подробную информацию можно найти в руководстве по программированию Threadding.
Ваш код, таким образом, становится:
- (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 - лучший способ
Всегда обновляйте свой интерфейс в главном потоке и выполняйте реальную работу над вторичными потоками.
Обратный вызов FSEvents вызывается во вторичном потоке, на отправляемое вами уведомление NSNotification отвечает другой вторичный поток, и все они работают с объектами, которые в действительности не являются потокобезопасными. Как правило, NSMutable* объекты потокобезопасны при доступе, но не при мутациях. Другими словами, если вы изменяете их содержание, вам лучше обратить внимание, кто что делает и когда.:)
Больше информации о том, какие объекты Какао являются потокобезопасными, а какие нет, можно найти здесь, в разделе " Безопасность потоков " в документе, который я упоминал выше. (Между прочим, это неплохое чтение)
Идея состоит в том, чтобы сообщить приложению обновить интерфейс в главном потоке, например так:
- (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];
}
То, что вы эффективно делаете здесь, - это то, что вы планируете операцию прокрутки и операции удаления объекта в цикле выполнения основного потока, таким образом устраняя любой потенциальный конфликт доступа, потому что они будут в очереди, один за другим.
Кроме того, обратите внимание на любые другие потенциальные места в вашем приложении, где могут возникнуть конфликты доступа.
Я очень надеюсь, что это поможет и не смущает вас еще больше. Поначалу какао может быть болезненным, но то, что не убивает, делает тебя сильнее!
Я решил проблему с помощью Cătălin Stan, выполнив removeObject:
вызов в основной ветке так:
- (void)removeBackupObject:(NSNotification *)notification
{
if (notification.object) {
dispatch_async(dispatch_get_main_queue(), ^{
[self.backupsArrayController removeObject:notification.object];
});
}
}