BFTask 在后台绘制 SpriteKit 对象正在锁定主线程

Posted

技术标签:

【中文标题】BFTask 在后台绘制 SpriteKit 对象正在锁定主线程【英文标题】:BFTask to draw SpriteKit objects in background is locking main thread 【发布时间】:2016-01-19 17:48:58 【问题描述】:

我正在使用 BFTasks 在后台执行一些 SpriteKit 绘图,但我不确定我是否正确使用它们,因为绘图锁定了主线程。

每个对象由多个 SKSpriteNode 组成,在渲染之前会被展平。我希望每个都在展平后立即渲染,即当我调用 [self addChild:miniNode]; 时,它会等到所有内容都创建完毕(锁定主线程),然后它们会同时出现。

我已经简化了下面的代码以显示任务链:

- (void)drawLocalRelationships

    [ParseQuery getLocalRelationships:_player.relationships block:^(NSArray *objects, NSError *error) 
        [[[self drawRelationships:objects forMini:_player]
          continueWithBlock:^id(BFTask *task) 
              //this continues once they've all been drawn and rendered 
              return nil;
          ];
    ];


- (BFTask *)drawRelationships:(NSArray *)relationships forMini:(Mini *)mini

    return [_miniRows drawSeriesRelationships:relationships forMini:mini];

MiniRows 类:

- (BFTask *)drawSeriesRelationships:(NSArray *)relationships forMini:(Mini *)mini

    BFTask *task = [BFTask taskWithResult:nil];

    for (Relationship *relationship in relationships) 
        task = [task continueWithBlock:^id(BFTask *task) 
            return [self drawRelationship:relationship mini:mini];
        ];
    
    return task;


- (BFTask *)drawRelationship:(Relationship *)relationship mini:(Mini *)mini

    //code to determine 'row'
    return [row addMiniTask:otherMini withRelationship:relationship];

Row 类:

- (BFTask *)addMiniTask:(Mini*)mini withRelationship:(Relationship *)relationship

    //drawing code
    MiniNode *miniNode = [self nodeForMini:mini size:size position:position scale:scale];
    [self addChild:miniNode]; //doesn't actually render here
    return [BFTask taskWithResult:nil];

我尝试在后台线程上运行 addMiniTask 方法,但似乎没有什么不同。我想知道我是否误解了 BFTasks 的概念 - 我认为它们会自动在后台线程上运行,但也许不是?

【问题讨论】:

我的理解是,您无法在后台线程中更新视图(以及通过SpriteKit,最终呈现在UIView 中)。视觉更新应该在主线程上完成。 我尝试使用 dispatch 在主线程上运行 addChild 方法,但没有区别... 【参考方案1】:

BFTasks 默认不在后台线程上运行!

如果你这样做:

BFTask * immediateTask = [BFTask taskWithResult: @"1"];

immediateTask 在当前线程中立即完成,即completed 属性为YES。

另外,如果你这样做:

[task continueWithBlock:^id(BFTask *task) 
    // some long running operation 
    return nil;
];

一旦任务完成,块在默认执行器中执行,它立即在当前线程中执行块,除非调用堆栈太深,在这种情况下它会被卸载到后台调度队列。 当前线程是调用 continueWithBlock 的线程。 所以除非你在后台线程中调用前面的代码,否则长时间运行的操作会阻塞当前线程。

但是,您可以使用显式执行器将块卸载到不同的线程或队列:

BFTask * task = [BFTask taskFromExecutor:executor withBlock:^id 
    id result = ...; // long computation
    return result;
];

选择合适的执行者很关键:

executor = [BFExecutor defaultExecutor] 任务块在当前线程(执行任务创建的线程)上运行,如果调用堆栈太深,则卸载到后台队列。所以很难预测会发生什么; executor = [BFExecutor immediateExecutor] 任务块与前一个任务在同一个线程上运行(参见下面的链接)。但是如果上一个任务是由默认执行器运行的,你就真的不知道它是哪个线程; executor = [BFExecutor mainThreadExecutor] 任务块在主线程上运行。这是用于在长时间运行操作后更新您的 UI。 executor = [BFExecutor executorWithDispatchQueue:gcd_queue] 任务的块在提供的 gcd 队列中运行。创建一个带有后台队列的队列以执行长时间运行的操作。队列的类型(串行或并发)取决于要执行的任务及其依赖关系。

根据执行者的不同,你会得到不同的行为。

BFTasks 的优点是您可以链接和同步在不同线程中运行的任务。例如,要在长时间运行后台操作后更新主线程中的 UI,您可以这样做:

// From the UI thread
BFTask * backgroundTask = [BFTask taskFromExecutor:backgroundExecutor withBlock:^id 
    // do your long running operation here
    id result = ...; // long computation here
    return result;
];
[backgroundTask continueWithExecutor:[BFExecutor mainThreadExecutor] withSuccessBlock:^id(BFTask* task) 
    id result = task.result;
    // do something quick with the result - we're executing in the UI thread here
    return nil
];

PFQuery findInBackgroundWithBlock 方法使用默认执行程序执行块,因此如果您从主线程调用该方法,则该块也很有可能在主线程中执行。 在你的情况下,虽然我对 SpriteKit 一无所知,但我会获取所有精灵,然后更新 UI:

- (void)queryRenderAllUpdateOnce 

    NSThread *currentThread = [NSThread currentThread];
    NSLog(@"current thread is %@ ", currentThread);

    // replace the first task by [query findObjectsInBackground]
    [[[BFTask taskFromExecutor:[Tasks backgroundExecutor] withBlock:^id _Nonnull

        NSLog(@"[%@] - Querying model objects", [NSThread currentThread]);
        return @[@"Riri", @"Fifi", @"LouLou"];

    ] continueWithExecutor:[BFExecutor immediateExecutor] withBlock:^id _Nullable(BFTask * _Nonnull task) 

        NSLog(@"[%@] - Fetching sprites for model objects", [NSThread currentThread]);
        NSArray<NSString *> * array = task.result;
        NSMutableArray * result = [[NSMutableArray alloc] init];
        for (NSString * obj in array) 
            // replace with sprite 
            id sprite = [@"Rendered " stringByAppendingString:obj];
            [result addObject:sprite];
        
        return result;

    ] continueWithExecutor:[BFExecutor mainThreadExecutor] withBlock:^id _Nullable(BFTask * _Nonnull task) 

        NSLog(@"[%@] - Update UI with all sprite objects: %@", [NSThread currentThread], task.result);
        // TODO update the UI here.
        return nil;
    ];


但使用此解决方案,所有精灵都被获取(扁平化?)然后更新 UI。如果你想更新 UI,每次获取一个精灵时,你可以这样做:

- (void)queryRenderUpdateMany 

    NSThread *currentThread = [NSThread currentThread];
    NSLog(@"current thread is %@ ", currentThread);

    [[[BFTask taskFromExecutor:[Tasks backgroundExecutor] withBlock:^id _Nonnull

        NSLog(@"[%@] - Querying model objects", [NSThread currentThread]);
        return @[@"Riri", @"Fifi", @"LouLou"];

    ] continueWithExecutor:[BFExecutor immediateExecutor] withBlock:^id _Nullable(BFTask * _Nonnull task) 

        NSArray<NSString *> * array = task.result;
        NSMutableArray * result = [[NSMutableArray alloc] init];
        for (NSString * obj in array) 

            BFTask *renderUpdate = [[BFTask taskFromExecutor:[BFExecutor immediateExecutor] withBlock:^id _Nonnull

                NSLog(@"[%@] - Fetching sprite for %@", [NSThread currentThread], obj);
                return [@"Rendered " stringByAppendingString:obj];

            ] continueWithExecutor:[BFExecutor mainThreadExecutor] withBlock:^id _Nullable(BFTask * _Nonnull task) 

                NSLog(@"[%@] - Update UI with sprite %@", [NSThread currentThread], task.result);
                return nil;

            ];
            [result addObject: renderUpdate];
        

        return [BFTask taskForCompletionOfAllTasks:result];

    ] continueWithExecutor:[BFExecutor mainThreadExecutor] withBlock:^id _Nullable(BFTask * _Nonnull task) 
        NSLog(@"[%@] - Updated UI for all sprites", [NSThread currentThread]);
        return nil;
    ];


这里的中间任务创建了一个在所有 renderUpdate 任务都完成后完成的任务。

希望对您有所帮助。

B

【讨论】:

哇,很棒的解释,完全值得赏金。我没有完全掌握 BFTasks,也没有正确看待 Executors,因为我认为它们超出了我想要做的范围,但似乎它们正是我需要的。谢谢!将考虑到这一点。

以上是关于BFTask 在后台绘制 SpriteKit 对象正在锁定主线程的主要内容,如果未能解决你的问题,请参考以下文章

如何在swift(SpriteKit)中绘制一个作为单个节点的圆点[关闭]

如何让一条线跟随触摸而不是在 spritekit 中绘制无限?

CAShapeLayer 和 SpriteKit

SpriteKit:如何在没有 SKShapeNode 的情况下绘制自定义形状?

如何在 SpriteKit 中使用 SKShapeNode 通过触摸特定路径来绘制路径

如何在 Swift 3 中使用 BFTask?