为什么NSOperationQueue在主线程上处理大量任务时比GCD或performSelectorOnMainThread更快? [英] Why NSOperationQueue is faster than GCD or performSelectorOnMainThread when they process a large number of tasks on the main Thread?

查看:163
本文介绍了为什么NSOperationQueue在主线程上处理大量任务时比GCD或performSelectorOnMainThread更快?的处理方法,对大家解决问题具有一定的参考价值,需要的朋友们下面随着小编来一起学习吧!

问题描述

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

  //最快
[[NSOperationQueue mainQueue] addOperationWithBlock:^ {
[btnThumb setImage:[UIImage imageWithData:data] forState:UIControlStateNormal];
[scrollView addSubview:btnThumb];
}];
//慢慢
dispatch_async(dispatch_get_main_queue(),^
{
[btnThumb setImage:[UIImage imageWithData:data] forState:UIControlStateNormal];
[scrollView addSubview: btnThumb];
});
//慢慢
[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];
}


解决方案

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

  #importNSAppDelegate.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
{
// test
numOps = 1000;
usleepDuration = 1000;

//设置一个串行队列,这样我们可以在测试用例中或多或少地保持调用NSLog的成本。
gLogQueue = dispatch_queue_create(NULL,DISPATCH_QUEUE_SERIAL);
// Group允许我们在下一个测试开始之前等待一个测试完成
g = dispatch_group_create();

//在此处插入代码以初始化您的应用程序
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;

//暂停主线程
dispatch_async(dispatch_get_main_queue(),^ {
[cond lock];
[cond signal];
[cond wait];
[cond unlock];
});

//等待主线程暂停
[cond lock];
[cond wait];

/ / NSOperationQueue
for(NSUInteger i = 0; i< numOps; ++ i)
{
dispatch_group_enter(g);
[[NSOperationQueue mainQueue] addOperationWithBlock:^ {
NSLogAsync(@NSOpQ任务#%@,@(i));
usleep(usleepDuration); //模拟工作
dispatch_group_leave(g);
}];
}

//取消主线程
[cond signal];
[cond unlock ]。

//标记开始时间
start = [NSDate timeIntervalSinceReferenceDate];
//等待它完成
dispatch_group_wait(g,DISPATCH_TIME_FOREVER);
end = [NSDate timeIntervalSinceReferenceDate];
NSTimeInterval opQDuration = end - start;
NSLogAsync(@NSOpQ:%@ s,@(opQDuration));

//暂停主线程
dispatch_async(dispatch_get_main_queue(),^ {
[cond lock];
[cond signal];
[cond]等等];
[cond unlock];
});

//等待主线程暂停
[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主线程任务#%@,@(i));
usleep(usleepDuration); //模拟工作
dispatch_group_leave(g);
});
}

//取消主线程
[cond signal];
[cond unlock];

//标记开始
start = [NSDate timeIntervalSinceReferenceDate];
dispatch_group_wait(g,DISPATCH_TIME_FOREVER);
end = [NSDate timeIntervalSinceReferenceDate];
NSTimeInterval asyncDuration = end - start;
NSLogAsync(@dispatch_async take:%@ s,@(asyncDuration));

//暂停主线程
dispatch_async(dispatch_get_main_queue(),^ {
[cond lock];
[cond signal];
[cond]等等];
[cond unlock];
});

//等待主线程暂停
[cond lock];
[cond wait];

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

//取消主线程
[cond signal];
[cond unlock];

//标记开始
start = [NSDate timeIntervalSinceReferenceDate];
dispatch_group_wait(g,DISPATCH_TIME_FOREVER);
end = [NSDate timeIntervalSinceReferenceDate];
NSTimeInterval performDuration = end - start;
NSLogAsync(@performSelector take:%@ s,@(performDuration));

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

- (void)selectorToPerfTask:(NSNumber *)task
{
NSLogAsync(@performSelector task#%@,task);
usleep(usleepDuration); //模拟工作
dispatch_group_leave(g);
}

静态NSString * NSStringFromCFRunLoopActivity(CFRunLoopActivity活动)
{
NSString * foo = nil;
开关(活动){
case kCFRunLoopEntry:
foo = @kCFRunLoopEntry;
休息;
case kCFRunLoopBeforeTimers:
foo = @kCFRunLoopBeforeTimers;
休息;
case kCFRunLoopBeforeSources:
foo = @kCFRunLoopBeforeSources;
休息;
case kCFRunLoopBeforeWaiting:
foo = @kCFRunLoopBeforeWaiting;
休息;
case kCFRunLoopAfterWaiting:
foo = @kCFRunLoopAfterWaiting;
休息;
case kCFRunLoopExit:
foo = @kCFRunLoopExit;
休息;
默认值:
foo = @ERROR;
休息;

}
返回foo;
}

static void MyCFRunLoopObserverCallBack(CFRunLoopObserverRef观察者,CFRunLoopActivity活动,void * info)
{
NSLogAsync(@RLO:%@,NSStringFromCFRunLoopActivity(activity) );
}


@end

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

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

...模式重复...

RLO:kCFRunLoopEntry
RLO:kCFRunLoopBeforeTimers
RLO:kCFRunLoopBeforeSources
RLO:kCFRunLoopBeforeWaiting
RLO:kCFRunLoopAfterWaiting
NSOpQ任务# 999
RLO:kCFRunLoopExit
NSOpQ花了:1.237247049808502s
RLO:kCFRunLoopEntry
RLO:kCFRunLoopBeforeTimers
RLO:kCFRunLoopBeforeSources
RLO:kCFRunLoopBeforeWaiting
RLO:kCFRunLoopAfterWaiting
dispatch_async主线程任务#0
dispatch_async主线程任务#1

...模式重复...

dispatch_async主线程任务#999
dispatch_async花了:1.118762016296387s
RLO:kCFRunLoopExit
RLO:kCFRunLoopEntry
RLO:kCFRunLoopBeforeTimers
RLO:kCFRunLoopBeforeSources
performSelector task#0
performSelector任务#1

...模式重复...

performSelector任务#999
performSelector采取:1.133482992649078s
RLO :kCFRunLoopExit
RLO:kCFRunLoopEntry
RLO:kCFRunLoopBeforeTimers
RLO:kCFRunLoopBeforeSources
完成。 NSOperationQueue:1.237247049808502s dispatch_async:1.118762016296387s performSelector:1.133482992649078

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



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




  • NSOperationQueue:1.2372s

  • dispatch_async:1.1188s

  • performSelector:1.1335s



NSOperationQueue 总是会慢一些,因为旋转运行循环不是免费的。在这个测试工具中,运行循环甚至没有任何实质内容,并且它已经比 dispatch_async 慢10%。如果它正在做任何实质性的事情,比如重新绘制视图,它将会慢慢地 。至于 dispatch_async vs performSelectorOnMainThread:两者都在运行循环的一次旋转中执行所有入队项目,所以区别很大边缘。我希望它能消除发送开销并管理目标上的retain / releases以及 performSelector ... 的参数。



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



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


for example, i have 100 times the for loop. and need to update UIImageView,and the last 2 method is same slowly. why? what is the different between they?

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

解决方案

For posterity, lest there be any doubt about this, let's consider the following test harness, built in a simple empty application template. It does 1000 operations using each mechanism, and also has a run loop observer, so we can see how our enqueued asynchronous tasks relate to the spinning of the main run loop. It logs to console, but does so asynchronously, so that the cost of NSLog isn't confounding our measurement. It also intentionally blocks the main thread while it enqueues NSOperations/dispatch_asyncs/performSelectors tasks, so that the act of enqueuing also doesn't interfere. Here's the code:

#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

In the output of this code, we see the following (with irrelevant/repeating portions removed):

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

What this shows us is that NSOperations enqueued on the main queue get executed one per pass of the run loop. (Incidentally, this is going to allow views to draw for each operation, so if you're updating a UI control in these tasks as the OP was, this will allow them to draw.) With dispatch_async(dispatch_get_main_queue(),...) and -[performSelectorOnMainThread:...] all enqueued blocks/selectors are called one after the other without letting views draw or anything like that. (If you don't forcibly pause the main runloop while you enqueue tasks, you sometimes can see the run loop spin once or twice during the enqueueing process.)

In the end, the results are about what I expected they would be:

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

NSOperationQueue will always be slower, because spinning the run loop isn't free. In this test harness, the run loop isn't even doing anything of substance, and it's already 10% slower than dispatch_async. If it were doing anything of substance, like redrawing a view, it would be much slower. As for dispatch_async vs performSelectorOnMainThread: both execute all enqueued items in one spin of the run loop, so the difference is pretty marginal. I expect it's down to message send overhead and managing the retain/releases on the target and argument of the performSelector....

So contrary to the implication of the question, NSOperationQueue is not, objectively, the fastest of the three mechanisms, but rather the slowest. My suspicion is that in OP's case, NSOperationQueue appears faster because its "time to first visible change" is going to be much shorter, whereas for dispatch_async and performSelector all enqueued operations are going to be executed, and only then will the view redraw and show the new state. In the pathological case, I would expect this to mean that only the last frame was ever seen, although if you don't block the main thread while enqueueing, you could get a handful of visible frames (but you'll effectively be dropping most of the frames on the ground.)

Regardless of which asynchronous execution mechanism is objectively fastest, they're all pretty crappy ways to do animation.

这篇关于为什么NSOperationQueue在主线程上处理大量任务时比GCD或performSelectorOnMainThread更快?的文章就介绍到这了,希望我们推荐的答案对大家有所帮助,也希望大家多多支持IT屋!

查看全文
登录 关闭
扫码关注1秒登录
发送“验证码”获取 | 15天全站免登陆