是否可以检查主线程是否空闲/排出主运行循环?

时间:2013-06-06 23:15:57

标签: ios objective-c runloop

我刚刚阅读了以下文章并尝试实施其中描述的方法:

Writing iOS acceptance tests using Kiwi - Being Agile

那里描述的所有东西确实很有效。但!当我进行验收测试时,有一件事打破了决定论。

以下是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
}

...其中sleepForfollowing definition behind itself

#define sleepFor(interval) (CFRunLoopRunInMode(kCFRunLoopDefaultMode, interval, false))

这是一个天真的尝试('天真'不是关于作者,而是关于这个事实,它是第一个进入头脑中的事实)等待一小段时间,直到所有的动画都被处理和浸泡所有可能被安排到主要运行循环的事件(参见this comment)。

问题是这个天真的代码不能以确定的方式工作。在当前编辑的文本字段的键盘消失之前,有一串UI交互导致fx下一个按钮点击,等等......

如果我只是将时间从0.1增加到fx 1,则所有问题都会消失,但这会导致每次交互,例如“用文本填充文本字段......”或“带标题的点击按钮......”花费一秒钟!

所以我并不是说只是增加一个等待时间,而是一种方法来做出这样的人为的等待,保证我可以继续下一步的测试用例。

我希望它应该是一种更可靠的方式,等待所有由当前操作(所有转换/动画或任何主要运行循环内容)引起的东西完成。

总结这一切都是一个问题:

有没有办法耗尽/排出/浸泡所有计划到主线程及其运行循环的东西,以确保主线程处于空闲状态并且其运行循环为“空”? < / p>

这是我最初的解决方案:

// 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 个答案:

答案 0 :(得分:3)

要检查与线程关联的运行循环的状态并注册单独阶段的回调,可以使用CFRunLoopObserverRef。这允许对调用回调的时间进行非常细粒度的控制。此外,您不必依赖hacky超时等。

可以像这样添加一个(注意我在主循环中添加一个)

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;
    }
};

答案 1 :(得分:2)

你可以试试这个

while (CFRunLoopRunInMode(kCFRunLoopDefaultMode, 0, true) == kCFRunLoopRunHandledSource);

这将运行直到运行循环中没有更多内容。如果0不起作用,您可以尝试将时间间隔更改为0.1。

答案 2 :(得分:1)

这是我目前的解决方案,稍后我会在代码中添加一些注释和解释,如果没有人告诉我我错了或先建议更好的答案:

// 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://stackoverflow.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();
}

目前我对如何提高效率我没有任何想法。