Постоянно записывать содержимое файла журнала в 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];
        });
    }
}
Другие вопросы по тегам