How to handle looping code with blocks? [closed]

2019-03-22 01:57发布

I have some code which requires the use of blocks. The block fetches a number of data items from a web service, and then possibly needs to fetch more, and then more again after that, then returns all of the data items once it has all required. I'm unsure how to put this into code. Here's an example of what I mean:

NSMutableArray *array = [[NSMutableArray alloc] init];

[webService getLatestItemsWithCount:50 completion:^(NSArray *objects) {
    //Some code to deal with these items.

    if (moreItemsNeeded == YES) {
        //I now need it to loop this block until I'm done
    }
}];

How can I get this to work?

EDIT:

Ok, this is what i'm working with - it's the Evernote API. It should be a better example of what I need:

[noteStore findNotesMetadataWithFilter:filter
                                offset:0
                              maxNotes:100
                            resultSpec:resultSpec
                               success:^(EDAMNotesMetadataList *metadataList) {
    for (EDAMNoteMetadata *metadata in metadataList.notes) {
        NSDate *timestamp = [NSDate endateFromEDAMTimestamp:metadata.updated];

        if (timestamp.timeIntervalSince1970 > date.timeIntervalSince1970) {
            [array addObject:metadata];
        }
        else {
            arrayComplete = YES;
        }
    }

    //I need it to loop this code, increasing the offset, until the array is complete.

}failure:^(NSError *error) {
    NSLog(@"Failure: %@", error);
}];

4条回答
够拽才男人
2楼-- · 2019-03-22 02:42

You should create a variable that references the block to make possible the recursive invocation. It must be noted that at the moment that you assign the block, it is still nil, so if you call it inside the block itself (aka recursively), you'll get a crash while trying to execute a nil block. So the block should have a *__block* storage:

void (^__block myBlock) (NSArray*) = ^(NSArray *objects) {
    //Some code to deal with these items.

    if (moreItemsNeeded == YES) {
        //I now need it to loop this block until I'm done
        myBlock(objects);
        myBlock= nil; // Avoid retain cycle
    }
}];
[webService getLatestItemsWithCount:50 completion: myBlock];

The block in your specific case is "translated" as this one:

void (^__block handler) (EDAMNotesMetadataList)= ^(EDAMNotesMetadataList* metadataList) {
    for (EDAMNoteMetadata *metadata in metadataList.notes) {
        NSDate *timestamp = [NSDate endateFromEDAMTimestamp:metadata.updated];

        if (timestamp.timeIntervalSince1970 > date.timeIntervalSince1970) {
            [array addObject:metadata];
        }
        else {
            arrayComplete = YES;
        }
    }

    //I need it to loop this code, increasing the offset, until the array is complete.
    if(!arrayComplete)
        handler(metadataList);
    handler= nil; // Avoid retain cycle
};

Then you can normally call that method passing myBlock as argument.

About retain cycles

To avoid a retain cycle, you should set to nil the pointer to the block when the recursion finishes.

查看更多
趁早两清
3楼-- · 2019-03-22 02:44

This is (as far as I've been able to figure out) - sort of an annoying connundrum - and one of blocks' few shortcomings... The following is the go-to archetype I refer to if I REALLY wanna make sure I'm being safe about it..

// declare a "recursive" prototype you will refer to "inside" the block.
id __block (^enumerateAndAdd_recurse)(NSArray*);        
// define the block's function - like normal.
id         (^enumerateAndAdd)        (NSArray*) = ^(NSArray*kids){ 
   id collection = CollectionClass.new;
   for (ArrayLike* littleDarling in kids) 
       [collection add:enumerateAndAdd_recurse(littleDarling)];
   return collection;
};      
enumerateAndAdd_recurse = enumerateAndAdd; // alias the block "to itself" before calling.
enumerateAndAdd(something); // kicks it all off,  yay. 
查看更多
再贱就再见
4楼-- · 2019-03-22 02:52

I prefer to use the fixed-point combinator structure to write block recursion. This way I don't have to mess with __block variables or risk a retain cycle when I forget to set the block to nil at the end of the recursion. All credit for this goes to Mike Ash who shared this code snippet.

Here's my version of his code (which I placed in a globally shared file so I can access this function from anywhere):

// From Mike Ash's recursive block fixed-point-combinator strategy (https://gist.github.com/1254684)
dispatch_block_t recursiveBlockVehicle(void (^block)(dispatch_block_t recurse))
{
    // assuming ARC, so no explicit copy
    return ^{ block(recursiveBlockVehicle(block)); };
}

typedef void (^OneParameterBlock)(id parameter);
OneParameterBlock recursiveOneParameterBlockVehicle(void (^block)(OneParameterBlock recurse, id parameter))
{
    return ^(id parameter){ block(recursiveOneParameterBlockVehicle(block), parameter); };
}

I know this looks super weird and confusing... but it's not too bad once you understand it. Here's what a simple recursive block might look like:

dispatch_block_t run = recursiveBlockVehicle(^(dispatch_block_t recurse) 
{
    if (! done)
    {
        // Continue recursion
        recurse();
    }
    else
    {
        // End of recursion
    }
});
run();

When you call recursiveBlockVehicle, you're passing a block that contains your code. recursiveBlockVehicle's job is take this block that you passed and do three things:

  1. Execute the block
  2. Pass the block back through recursiveBlockVehicle and pass that resultant as the parameter to the block
  3. Encapsulate steps 1 and 2 within a simple block and return that

Now, inside your block's code, if you were to call the special recurse block parameter, you're in turn calling your own block all over again (achieving recursion). The nice thing about this strategy is that the memory management is fairly straight-forward. The use of parameters to pass your own code back to yourself reduces the risk of retain cycles. I use this method instead of defining a __block variable of my code because I'm afraid I might forget to set the __block variable to nil at the end of a recursion and result in a nasty retain cycle.

With that in mind, here's how I would implement your function:

OneParameterBlock run = recursiveOneParameterBlockVehicle(^(OneParameterBlock recurse, id parameter)
{
    NSNumber *offset = parameter;
    [noteStore
        findNotesMetadataWithFilter:filter
        offset:offset.intValue
        maxNotes:100
        resultSpec:resultSpec
        success:^(EDAMNotesMetadataList *metadataList)
        {
            for (EDAMNoteMetadata *metadata in metadataList.notes)
            {
                NSDate *timestamp = [NSDate endateFromEDAMTimestamp:metadata.updated];
                if (timestamp.timeIntervalSince1970 > date.timeIntervalSince1970)
                {
                    [array addObject:metadata];
                }
                else
                {
                    arrayComplete = YES;
                }
            }

            //I need it to loop this code, increasing the offset, until the array is complete.
            if (! arrayComplete)
            {
                recurse([NSNumber numberWithInt:offset.intValue + 100]);
            }
        }
        failure:^(NSError *error)
        {
            NSLog(@"Failure: %@", error);
        }];
});
run(@0);

Again, note that you're not calling callback (the block object) inside of the block itself. The reason why is because the block is passing itself as a parameter recurse and executing recurse is how you achieve recursion.

Also, (in case you've actually read this far and wanted to see more), here's a wikipedia page on FPC: http://en.wikipedia.org/wiki/Fixed-point_combinator

Lastly, I have not personally tested the retain cycle issue of a __block variable. However, Rob Mayoff did a fantastic analysis on the issue: https://stackoverflow.com/a/13091475/588253

查看更多
▲ chillily
5楼-- · 2019-03-22 02:53

Your code will be simpler to understand and less prone to leaking the block if you don't make the block recursive. Instead, wrap it in a method, and make the block call the method if it needs to keep searching.

This example is based on the code in your question:

- (void)appendNotesMetadataToArray:(NSMutableArray *)array
    untilDate:(NSDate *)date withFilter:(EDAMNoteFilter *)filter
    offset:(int32_t)offset resultSpec:(EDAMNotesMetadataResultSpec *)resultSpec
{
    static const int32_t kBatchSize = 100;

    [noteStore findNotesMetadataWithFilter:filter
        offset:offset maxNotes:kBatchSize resultSpec:resultSpec
        success:^(EDAMNotesMetadataList *metadataList) {
            BOOL searchComplete = NO;
            for (EDAMNoteMetadata *metadata in metadataList.notes) {
                NSDate *timestamp = [NSDate endDateFromEDAMTimestamp:metadata.updated];
                if ([timestamp compare:date] == NSOrderedDescending) {
                    [array addObject:metadata];
                } else {
                    searchComplete = YES;
                }
            }

            if (!searchComplete) {
                [self appendNotesMetadataToArray:array untilDate:date
                    withFilter:filter offset:offset + kBatchSize
                    resultSpec:resultSpec];
            }
        } failure:^(NSError *error) {
            NSLog(@"Failure: %@", error);
        }];
}

With this design, you don't need to declare a reference to the block with an inscrutable type signature, and you don't have to worry about the block leaking because it references itself.

In this design, each call to the method creates a new block. The block references self, and (I assume) self references noteStore, and noteStore references the block, so there is a retain cycle. But when the block finishes executing, noteStore releases the block, breaking the retain cycle.

查看更多
登录 后发表回答