Можно ли проверить, что основной поток простаивает / истощить основной цикл выполнения?

Я только что прочитал следующий пост и попытался реализовать подход, описанный там:

Написание 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();
}

Сейчас у меня нет идей, как это можно сделать более эффективным.

Другие вопросы по тегам