为什么NSOperationQueue在主线程上处理大量任务时比GCD或performSelectorOnMainThread更快?

时间:2013-01-08 01:41:41

标签: ios multithreading grand-central-dispatch nsoperationqueue

例如,我有100次for循环。并且需要更新UIImageView,最后2个方法慢慢相同。为什么?他们之间有什么不同?

//fastest
    [[NSOperationQueue mainQueue] addOperationWithBlock:^{
                       [btnThumb setImage:[UIImage imageWithData:data] forState:UIControlStateNormal];
                        [scrollView addSubview:btnThumb];
                   }];
//slowly
                     dispatch_async(dispatch_get_main_queue(), ^
                    {
                        [btnThumb setImage:[UIImage imageWithData:data] forState:UIControlStateNormal];
                        [scrollView addSubview:btnThumb];
                    });       
//slowly
                   [btnThumb setImage:[UIImage imageWithData:data] forState:UIControlStateNormal];
                   [self performSelectorOnMainThread:@selector(testMethod:) withObject:[NSArray arrayWithObjects:scrollView, btnThumb, nil] waitUntilDone:NO];

    -(void) testMethod:(NSArray*)objs
    {

        UIScrollView *scroll = [objs objectAtIndex:0];
        UIButton *btn = [objs lastObject];
        [scroll addSubview:btn];
    }

1 个答案:

答案 0 :(得分:8)

对于后人,为了避免对此有任何疑问,让我们考虑以下测试工具,内置在一个简单的空应用程序模板中。它使用每种机制执行1000次操作,并且还有一个运行循环观察器,因此我们可以看到我们的排队异步任务如何与主运行循环的旋转相关。它记录到控制台,但是异步执行,因此NSLog的成本不会混淆我们的测量。它还有意阻塞主线程,同时将NSOperations / dispatch_asyncs / performSelectors任务排入队列,这样排队行为也不会干扰。这是代码:

#import "NSAppDelegate.h"

dispatch_queue_t gLogQueue;
#define NSLogAsync(...) dispatch_async(gLogQueue, ^{ NSLog(__VA_ARGS__); });

@implementation NSAppDelegate
{
    dispatch_group_t g;
    NSUInteger numOps;
    useconds_t usleepDuration;
}

static void MyCFRunLoopObserverCallBack(CFRunLoopObserverRef observer, CFRunLoopActivity activity, void *info);

- (void)applicationDidFinishLaunching:(NSNotification *)aNotification
{
    // parameters of test
    numOps = 1000;
    usleepDuration = 1000;

    // Set up a serial queue so we can keep the cost of calling NSLog more or less out of our test case.
    gLogQueue = dispatch_queue_create(NULL, DISPATCH_QUEUE_SERIAL);
    // Group allows us to wait for one test to finish before the next one begins
    g = dispatch_group_create();

    // Insert code here to initialize your application
    CFRunLoopObserverRef rlo = CFRunLoopObserverCreate(NULL, kCFRunLoopAllActivities, YES, 0, MyCFRunLoopObserverCallBack, NULL);
    CFRunLoopAddObserver(CFRunLoopGetCurrent(), rlo, kCFRunLoopCommonModes);
    CFRelease(rlo);

    NSCondition* cond = [[NSCondition alloc] init];


    dispatch_async(dispatch_get_global_queue(0, 0), ^{
        NSTimeInterval start = 0, end = 0;

        // pause the main thread
        dispatch_async(dispatch_get_main_queue(), ^{
            [cond lock];
            [cond signal];
            [cond wait];
            [cond unlock];
        });

        // wait for the main thread to be paused
        [cond lock];
        [cond wait];

        // NSOperationQueue
        for (NSUInteger i = 0; i < numOps; ++i)
        {
            dispatch_group_enter(g);
            [[NSOperationQueue mainQueue] addOperationWithBlock:^{
                NSLogAsync(@"NSOpQ task #%@", @(i));
                usleep(usleepDuration); // simulate work
                dispatch_group_leave(g);
            }];
        }

        // unpause the main thread
        [cond signal];
        [cond unlock];

        // mark start time
        start = [NSDate timeIntervalSinceReferenceDate];
        // wait for it to be done
        dispatch_group_wait(g, DISPATCH_TIME_FOREVER);
        end = [NSDate timeIntervalSinceReferenceDate];
        NSTimeInterval opQDuration = end - start;
        NSLogAsync(@"NSOpQ took: %@s", @(opQDuration));

        // pause the main thread
        dispatch_async(dispatch_get_main_queue(), ^{
            [cond lock];
            [cond signal];
            [cond wait];
            [cond unlock];
        });

        // wait for the main thread to be paused
        [cond lock];
        [cond wait];

        // Dispatch_async
        for (NSUInteger i = 0; i < numOps; ++i)
        {
            dispatch_group_enter(g);
            dispatch_async(dispatch_get_main_queue(), ^{
                NSLogAsync(@"dispatch_async main thread task #%@", @(i));
                usleep(usleepDuration); // simulate work
                dispatch_group_leave(g);
            });
        }

        // unpause the main thread
        [cond signal];
        [cond unlock];

        // mark start
        start = [NSDate timeIntervalSinceReferenceDate];
        dispatch_group_wait(g, DISPATCH_TIME_FOREVER);
        end = [NSDate timeIntervalSinceReferenceDate];
        NSTimeInterval asyncDuration = end - start;
        NSLogAsync(@"dispatch_async took: %@s", @(asyncDuration));

        // pause the main thread
        dispatch_async(dispatch_get_main_queue(), ^{
            [cond lock];
            [cond signal];
            [cond wait];
            [cond unlock];
        });

        // wait for the main thread to be paused
        [cond lock];
        [cond wait];

        // performSelector:
        for (NSUInteger i = 0; i < numOps; ++i)
        {
            dispatch_group_enter(g);
            [self performSelectorOnMainThread: @selector(selectorToPerfTask:) withObject: @(i) waitUntilDone: NO];
        }

        // unpause the main thread
        [cond signal];
        [cond unlock];

        // mark start
        start = [NSDate timeIntervalSinceReferenceDate];
        dispatch_group_wait(g, DISPATCH_TIME_FOREVER);
        end = [NSDate timeIntervalSinceReferenceDate];
        NSTimeInterval performDuration = end - start;
        NSLogAsync(@"performSelector took: %@s", @(performDuration));

        // Done.
        dispatch_async(dispatch_get_main_queue(), ^{
            CFRunLoopRemoveObserver(CFRunLoopGetCurrent(), rlo, kCFRunLoopCommonModes);
            NSLogAsync(@"Done. NSOperationQueue: %@s dispatch_async: %@s performSelector: %@", @(opQDuration), @(asyncDuration), @(performDuration));
        });
    });
}

- (void)selectorToPerfTask: (NSNumber*)task
{
    NSLogAsync(@"performSelector task #%@", task);
    usleep(usleepDuration); // simulate work
    dispatch_group_leave(g);
}

static NSString* NSStringFromCFRunLoopActivity(CFRunLoopActivity activity)
{
    NSString* foo = nil;
    switch (activity) {
        case kCFRunLoopEntry:
            foo = @"kCFRunLoopEntry";
            break;
        case kCFRunLoopBeforeTimers:
            foo = @"kCFRunLoopBeforeTimers";
            break;
        case kCFRunLoopBeforeSources:
            foo = @"kCFRunLoopBeforeSources";
            break;
        case kCFRunLoopBeforeWaiting:
            foo = @"kCFRunLoopBeforeWaiting";
            break;
        case kCFRunLoopAfterWaiting:
            foo = @"kCFRunLoopAfterWaiting";
            break;
        case kCFRunLoopExit:
            foo = @"kCFRunLoopExit";
            break;
        default:
            foo = @"ERROR";
            break;

    }
    return foo;
}

static void MyCFRunLoopObserverCallBack(CFRunLoopObserverRef observer, CFRunLoopActivity activity, void *info)
{
    NSLogAsync(@"RLO: %@", NSStringFromCFRunLoopActivity(activity));
}


@end

在此代码的输出中,我们看到以下内容(删除了不相关/重复部分):

RLO: kCFRunLoopEntry
RLO: kCFRunLoopBeforeTimers
RLO: kCFRunLoopBeforeSources
RLO: kCFRunLoopBeforeWaiting
RLO: kCFRunLoopAfterWaiting
NSOpQ task #0
RLO: kCFRunLoopExit
RLO: kCFRunLoopEntry
RLO: kCFRunLoopBeforeTimers
RLO: kCFRunLoopBeforeSources
RLO: kCFRunLoopBeforeWaiting
RLO: kCFRunLoopAfterWaiting
NSOpQ task #1
RLO: kCFRunLoopExit

... pattern repeats ...

RLO: kCFRunLoopEntry
RLO: kCFRunLoopBeforeTimers
RLO: kCFRunLoopBeforeSources
RLO: kCFRunLoopBeforeWaiting
RLO: kCFRunLoopAfterWaiting
NSOpQ task #999
RLO: kCFRunLoopExit
NSOpQ took: 1.237247049808502s
RLO: kCFRunLoopEntry
RLO: kCFRunLoopBeforeTimers
RLO: kCFRunLoopBeforeSources
RLO: kCFRunLoopBeforeWaiting
RLO: kCFRunLoopAfterWaiting
dispatch_async main thread task #0
dispatch_async main thread task #1

... pattern repeats ...

dispatch_async main thread task #999
dispatch_async took: 1.118762016296387s
RLO: kCFRunLoopExit
RLO: kCFRunLoopEntry
RLO: kCFRunLoopBeforeTimers
RLO: kCFRunLoopBeforeSources
performSelector task #0
performSelector task #1

... pattern repeats ...

performSelector task #999
performSelector took: 1.133482992649078s
RLO: kCFRunLoopExit
RLO: kCFRunLoopEntry
RLO: kCFRunLoopBeforeTimers
RLO: kCFRunLoopBeforeSources
Done. NSOperationQueue: 1.237247049808502s dispatch_async: 1.118762016296387s performSelector: 1.133482992649078

这表明我们在主队列上排队的NSOperation每次运行循环执行一次。 (顺便说一句,这将允许为每个操作绘制视图,因此如果您在OP中更新这些任务中的UI控件,则允许他们绘制。)使用dispatch_async(dispatch_get_main_queue(),...)和{{1所有入队的块/选择器都是一个接一个地调用而不让视图绘制或类似的东西。 (如果在排队任务时没有强行暂停主runloop,那么有时可以在排队过程中看到运行循环旋转一次或两次。)

最后,结果与我的预期相符:

  • NSOperationQueue:1.2372s
  • dispatch_async:1.1188s
  • performSelector:1.1335s

-[performSelectorOnMainThread:...]总是会变慢,因为旋转运行循环不是免费的。在这个测试工具中,运行循环甚至没有任何实质内容,并且它已经比NSOperationQueue慢10%。如果它正在做任何实质性的事情,比如重新绘制视图,它将会慢慢地 。至于dispatch_async vs dispatch_async,它们都会在运行循环的一次旋转中执行所有排队的项目,因此差异非常小。我希望它归结为消息发送开销和管理目标的保留/释放以及performSelectorOnMainThread:的参数。

因此,与问题的含义相反,performSelector...客观上不是三种机制中最快的,而是最慢的。我怀疑在OP的情况下,NSOperationQueue 出现更快,因为它的“第一次可见变化的时间”会短得多,而对于NSOperationQueue和{{1}所有排队的操作都将被执行,只有然后视图才会重绘并显示新状态。在病态情况下,我希望这意味着只有最后一帧被看到,虽然如果你在排队时没有阻止主线程,你可以得到一些可见的帧(但你实际上会有所下降)在地上的框架。)

无论哪种异步执行机制客观上最快,它们都是所有非常糟糕的动画制作方法。