I am using BFTasks to perform some SpriteKit drawing in the background, but I'm not sure I'm using them correctly, as the drawing is locking up the main thread.
Each object is made up of several SKSpriteNodes, that are flattened before rendering. I'd like each one to render as soon as it's been flattened, i.e. when I call [self addChild:miniNode];
But it waits until all have been created, (locking the main thread) and then they appear all at once.
I've simplified my code below to show the chain of tasks:
- (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];
}
The MiniRows class:
- (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];
}
The Row class:
- (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];
}
I've tried running the addMiniTask method on a background thread, but it doesn't seem to make a difference. I wonder if I'm misunderstanding the concept of BFTasks - I figured they're automatically run on a background thread, but perhaps not?
BFTasks are NOT run on a background thread by default !
If you do:
BFTask * immediateTask = [BFTask taskWithResult: @"1"];
immediateTask completes, i.e. the completed property is YES, immediately in the current thread.
Also, if you do:
[task continueWithBlock:^id(BFTask *task) {
// some long running operation
return nil;
}];
Once task completes, the block is executed in the default executor, which executes blocks immediately in the current thread unless the call stack is too deep in which case it is offloaded to a background dispatch queue. The current thread being the one where continueWithBlock is called. So unless you're calling the previous code in a background thread, the long running operation will block the current thread.
However, you can offload a block to a different thread or queue using an explicit executor:
BFTask * task = [BFTask taskFromExecutor:executor withBlock:^id {
id result = ...; // long computation
return result;
}];
Choosing the right executor is critical:
Depending on executor you will get different behaviour.
The advantage of BFTasks is that you can chain and synchronise tasks running in different threads. For example, to update the UI in the main thread after long running background operation, you would do:
// 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 method executes the block with the default executor, so if you call that method from the main thread, there is a great chance that the block will also execute in the main thread. In your case, although I know nothing about SpriteKit, I would fetch all sprites, then update the 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;
}];
}
But with this solution, all sprites are fetch (flattened ?) then the UI update. If you want to update the UI, every time a sprite is fetched, you could do something like this:
- (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;
}];
}
Here the middle task create a task that will complete once all renderUpdate tasks have completed.
Hope this help.
B