Можно ли проверить, что основной поток простаивает / истощить основной цикл выполнения?
Я только что прочитал следующий пост и попытался реализовать подход, описанный там:
Написание iOS приемочных тестов с использованием Kiwi - Быть проворным
Все вещи, описанные там, работают отлично. Но! есть одна вещь, которая нарушает детерминизм, когда я выполняю свои приемочные тесты.
Вот репозиторий на Github, где автор поста продвинул свои эксперименты (это можно найти в нижней части страницы в комментариях): https://github.com/moredip/2012-Olympics-iOS--iPad-and-iPhone--source-code/tree/kiwi-acceptance-mk1
Рассмотрим этот код, который он использует для касания вида:
- (void) tapViewViaSelector:(NSString *)viewSelector{
[UIAutomationBridge tapView:[self viewViaSelector:viewSelector]];
sleepFor(0.1); //ugh
}
...где sleepFor
имеет следующее определение:
#define sleepFor(interval) (CFRunLoopRunInMode(kCFRunLoopDefaultMode, interval, false))
Это наивная попытка ("наивный" не об авторе, а о том, что это первое, что приходит в голову) - подождать крошечный промежуток времени, пока все анимации не будут обработаны, и пропитать все возможные события, которые были (или могут быть) запланированы в основной цикл выполнения (см. также этот комментарий).
Проблема в том, что этот наивный код не работает детерминистически. Существует множество взаимодействий пользовательского интерфейса, которые вызывают нажатие следующей кнопки fx до исчезновения клавиатуры текущего отредактированного текстового поля и т. Д.
Если я просто увеличу время с 0,1 до 1, все проблемы исчезнут, но это приведет к тому, что каждое отдельное взаимодействие, такое как "заполнить текстовое поле текстом..." или "нажать кнопку с заголовком...", станет стоить второй!
Таким образом, я имею в виду не просто увеличение времени ожидания, а скорее способ сделать такое искусственное ожидание гарантией того, что я могу продолжить свой тестовый случай следующим шагом.
Я надеюсь, что это должен быть более надежный способ подождать, пока все вещи, вызванные текущим действием (все переходы / анимации или любой другой материал цикла выполнения) будут выполнены.
Подводя итог всему этому нужно задать вопрос:
Есть ли способ исчерпать / осушить / замочить весь материал, запланированный для основного потока и его цикла выполнения, чтобы убедиться, что основной поток простаивает, а его цикл выполнения "пуст"?
Это было мое первоначальное решение:
// DON'T like it
static inline void runLoopIfNeeded() {
// https://developer.apple.com/library/mac/#documentation/CoreFOundation/Reference/CFRunLoopRef/Reference/reference.html
while (CFRunLoopRunInMode(kCFRunLoopDefaultMode, 0.1, YES) == kCFRunLoopRunHandledSource);
// DON'T like it
if (CFRunLoopRunInMode(kCFRunLoopDefaultMode, 0.1, YES) == kCFRunLoopRunHandledSource) runLoopIfNeeded();
}
3 ответа
Ты можешь попробовать это
while (CFRunLoopRunInMode(kCFRunLoopDefaultMode, 0, true) == kCFRunLoopRunHandledSource);
это будет работать до тех пор, пока в цикле выполнения больше не останется ничего. Вы можете попробовать изменить временной интервал на 0,1, если 0 не работает.
Чтобы проверить состояние цикла выполнения, связанного с потоком, и зарегистрировать обратные вызовы для отдельных фаз, вы можете использовать CFRunLoopObserverRef
, Это позволяет очень тонко контролировать, когда вызываются обратные вызовы. Кроме того, вам не нужно зависеть от случайных тайм-аутов и тому подобного.
Один может быть добавлен так (обратите внимание, я добавляю один в основной цикл выполнения)
CFRunLoopObserverRef obs = CFRunLoopObserverCreateWithHandler(kCFAllocatorDefault, kCFRunLoopAllActivities, true, 0 /* order */, handler);
CFRunLoopAddObserver([NSRunLoop mainRunLoop].getCFRunLoop, obs, kCFRunLoopCommonModes);
CFRelease(obs);
В зависимости от действий, для которых вы регистрируетесь, ваш обработчик будет вызываться соответствующим образом. В приведенном выше примере наблюдатель слушает все действия. Вам, вероятно, нужно только kCFRunLoopBeforeWaiting
Ваш обработчик может выглядеть так
id handler = ^(CFRunLoopObserverRef observer, CFRunLoopActivity activity) {
switch (activity) {
case kCFRunLoopEntry:
// About to enter the processing loop. Happens
// once per `CFRunLoopRun` or `CFRunLoopRunInMode` call
break;
case kCFRunLoopBeforeTimers:
case kCFRunLoopBeforeSources:
// Happens before timers or sources are about to be handled
break;
case kCFRunLoopBeforeWaiting:
// All timers and sources are handled and loop is about to go
// to sleep. This is most likely what you are looking for :)
break;
case kCFRunLoopAfterWaiting:
// About to process a timer or source
break;
case kCFRunLoopExit:
// The `CFRunLoopRun` or `CFRunLoopRunInMode` call is about to
// return
break;
}
};
Вот мое текущее решение, я добавлю некоторые комментарии и объяснения к коду чуть позже, если никто не скажет мне, что я не прав или сначала предложу лучший ответ:
// It is much better, than it was, but still unsure
static inline void runLoopIfNeeded() {
// https://developer.apple.com/library/mac/#documentation/CoreFOundation/Reference/CFRunLoopRef/Reference/reference.html
__block BOOL flag = NO;
// http://stackru.com/questions/7356820/specify-to-call-someting-when-main-thread-is-idle
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_LOW, 0), ^{
dispatch_async(dispatch_get_main_queue(), ^{
flag = YES;
});
});
while (CFRunLoopRunInMode(kCFRunLoopDefaultMode, 0.1, YES) == kCFRunLoopRunHandledSource);
if (flag == NO) runLoopIfNeeded();
}
Сейчас у меня нет идей, как это можно сделать более эффективным.