0

我试图在后台线程上发出网络请求,我决定使用 NSBlockOperations。我正在使用ADNKit来处理我的获取请求。这是代码:

- (void)reloadPosts 
{
    dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{

        __block NSArray *additionalPosts;
        __block ANKAPIResponseMeta *additionalMeta;
        __block NSError *additionalError = nil;

        NSBlockOperation *completionOperation = [NSBlockOperation blockOperationWithBlock:^{
            PRSPostStreamDataController *strongSelf = weakSelf;

            // update data or handle error
            [strongSelf.data setPosts:additionalPosts withMeta:additionalMeta];
        }];

        NSBlockOperation *firstPostsOperation = [NSBlockOperation blockOperationWithBlock:^{
            PRSPostStreamDataController *strongSelf = weakSelf;
            NSDictionary *response = [strongSelf refreshPostsInPart:PartFirst];
            firstPosts = [response objectForKey:@"posts"];
            firstMeta = [response objectForKey:@"meta"];
            firstError = [response objectForKey:@"error"];
        }];

        [completionOperation addDependency:firstPostsOperation];
        [self.queue addOperation:firstPostsOperation];
        [self.queue addOperation:completionOperation];

    });
}


- (NSDictionary *)refreshPostsInPart:(StreamPart)part
{
    // get pagination IDs from data object
    ANKPaginationSettings *pagination = [[ANKPaginationSettings alloc] init];
    pagination.beforeID = [data beforeIDForPart:part];
    pagination.sinceID = [data sinceIDForPart:part];
    pagination.count = 20;

    // authenticatedClient is an object returned from a singleton managing accounts
    ANKClient *client = [authenticatedClient clientWithPagination:pagination];
    __block NSMutableArray *posts = [NSMutableArray new];
    __block ANKAPIResponseMeta *m = nil;

    __block BOOL isMore = YES;
    __block NSError *err;

    dispatch_semaphore_t semaphore = dispatch_semaphore_create(0);

    __block NSString *originalMaxID;
    while ((isMore) && (!err)) {

        self.apiCallMaker(client, ^(id responseObject, ANKAPIResponseMeta *meta, NSError *error){
            if (!error) {
                if (!originalMaxID) {
                    originalMaxID = meta.maxID;
                }
                m = meta;
                [posts addObjectsFromArray:(NSArray *)responseObject];

                client.pagination.beforeID = meta.minID;
                isMore = meta.moreDataAvailable;

            } else {
                err = error;
            }

            // signal that we are ready for the next iteration of the while loop
            dispatch_semaphore_signal(semaphore);
        });

        // wait for the signal from the completion block
        dispatch_semaphore_wait(semaphore, DISPATCH_TIME_FOREVER);
    }

    if (!err) {
        m.maxID = originalMaxID;
    }

    NSMutableDictionary *response = [NSMutableDictionary new];
    if (posts) [response setObject:posts forKey:@"posts"];
    if (m) [response setObject:m forKey:@"meta"];
    if (err) [response setObject:err forKey:@"error"];

    return response;
}

...
typedef void (^APIPostListCallback)(id responseObject, ANKAPIResponseMeta *meta, NSError *error);


- (void (^)(ANKClient *client, APIPostListCallback callback))apiCallMaker
{
    return [^(ANKClient *client, APIPostListCallback callback) {
        [client fetchPostsMentioningUser:self.user completion:callback];
    } copy];
}

我的代码应该是自我解释的,但是当我调用 self.apiCallMaker 时,我引用的是配置对象中定义的属性。请参阅我之前提出的这个问题,以获取有关该属性发生了什么的更多详细信息

当我尝试获取超过 40 个帖子时,我无法保持界面不卡顿。我将数据划分为 1 - 5 个部分,每个部分可以包含 1 - 200 多个帖子。当然,如果可以的话,我会把这些剪掉。我的问题是,当我重新加载所有数据时,我会使用这些 NSBlockOperations 之一重新加载每个部分。我在这里只展示了一个,以使其尽可能简洁。我已经在仪器中对此进行了测试,每次 ANKClient 对象将其 JSON 响应转换为 ANKPost 对象时,我的 CPU 都固定在 100% 以上,并且我的界面卡顿了。

问题:

  1. 将 JSON 响应转换为由 ANKClient 的完成处理程序执行的 ANKPost 对象是否在主线程之外完成?
  2. 每个 NSBlockOperation 中的所有内容都是在主线程之外执行的吗?
  3. 一切都是在refreshPostsInPart:主线程之外执行的吗?
  4. 我的数据对象的方法是setPosts:withMeta:在主线程之外执行的吗?
  5. 如果我删除了该dispatch_async块,上述问题的任何答案都会改变吗?
4

3 回答 3

1

从做过很多网络开发并写过一本书的人的角度来看(http://www.amazon.com/Professional-iOS-Network-Programming-Connecting/dp/1118362403),这段代码似乎过于复杂你正在尝试做。通常,当我开始分层并发 API 时,警报开始响起。

首先:您不需要在重新加载帖子中使用 dispatch_async 块。该代码将很快发生,队列工作应该在后台线程上发生。我说应该是因为没有看到队列是如何创建的,我不会确定它是否是背景。

信号量操作在我看来也是可疑的。NSMutableArray 不是线程安全的,但有更好的方法来保护它。将 addObjectFromArray 包装在 @synchronized(posts) { ... } 块中,这将大大简化事情。

在由于阻塞而导致无法解释的 UI 卡顿的情况下,我使用 Instruments 来观察发生这种情况时发生的情况,并查看主线程上实际运行的代码。一旦我确定了主线程上的代码,或者阻塞了主线程上的某些东西,我就会回到为什么该代码在主线程上的答案。

于 2014-02-26T17:49:04.300 回答
0

因此,在对 Instruments 进行了一些实验并研究了 AFNetworking 之后,我发现成功和失败会阻止 AFNetworking 调用在主线程上运行。但是,如果您有权访问获取数据的 AFHTTPRequestOperation,则可以设置成功和失败块在其上执行的 GCD 队列。

在我的问题中,我提到了 Stack Overflow 上的另一个问题。现在,感谢@berg(在 App.net 上) apiCallMaker返回了一个我可以捕获的 ANKJSONRequestOperation(AFHTTPRequestOperation 的子类)的实例。这是代码:

// in init
self.callbackQueue = dispatch_queue_create("com.Prose.fetchCallbackQueue", NULL);


- (NSDictionary *)refreshPostsInPart:(StreamPart)part
    ...

    while ((isMore) && (!err)) {

        ANKJSONRequestOperation *op = self.apiCallMaker(client, ^(id responseObject, ANKAPIResponseMeta *meta, NSError *error){
            if (!error) {
                if (!originalMaxID) {
                    originalMaxID = meta.maxID;
                }

                m = meta;
                [posts addObjectsFromArray:(NSArray *)responseObject];
                client.pagination.beforeID = meta.minID;
                isMore = meta.moreDataAvailable;

            } else {
                err = error;
            }

            dispatch_semaphore_signal(semaphore);
        });

        op.successCallbackQueue = self.callbackQueue;

        dispatch_semaphore_wait(semaphore, DISPATCH_TIME_FOREVER);
    }
    ... 
}

谢谢各位的意见。

于 2014-02-26T23:47:28.733 回答
0

我试图给出一个解决方案。

首先,您现在的代码不会运行。此外,虽然我认为我已经理解了主要问题,但我并没有完全理解细节——尤其是所有参数以及 ANK 的工作原理。尽管如此,这些细节似乎与示例解决方案无关。

问题:

因此,您似乎必须反复调用一个异步任务,该任务应串行(而不是并行)执行,从而消耗流中的数据,直到它到达末尾。此外,您希望在众所周知的执行上下文(例如,主线程或特定队列)上执行完成处理程序。

此外,在我看来,您的方法reloadPostsrefreshPostsInPart都是异步的。

笔记:

  • 每个异步方法或操作都应具有向调用站点发出完成信号的方法。在许多情况下,这是通过完成处理程序实现的,但也有其他方法。

  • 调用另一个异步方法或操作的方法自身变为异步的。否则,如果调用方法是同步的,则表示代码异味。

所以,看看你的方法:

  1. reloadPosts

    • 它没有完成处理程序:(
    • 奇怪的是,它似乎包含了异步任务和完成处理程序。
  2. refreshPostsInPart

    • 它没有完成处理程序。嗯,停止 - 它不是异步的!但是等等,它实际上异步的,但是你通过使用阻塞调用站点线程直到完成的信号量强制它变为同步:大代码气味:)

一个可能的解决方案:

幸运的是,您的代码可以重组并大大简化。

首先,通用完成处理程序可以声明如下:

typedef void (^completion_block_t)(id result, NSError* error);

您的方法refreshPostsInPart基本上应该实现上述问题。当任务完成时,它应该通过调用完成处理程序向调用站点发出此事件的信号。调用站点提供完成处理程序并定义发生这种情况时要做什么:

- (void) refreshPostsInPart:(StreamPart*)part completion:(completion_block_t);

注意:没有返回值!但是,您会从完成处理程序中获得结果。

现在,由于我不清楚您真正想要完成什么,因此后续解决方案变得抽象- 但我希望您知道如何将这个抽象解决方案“映射”到您的实际问题。

此外,我将使用实现“承诺”概念的第三方库。承诺代表异步任务的最终结果——如果发生这种情况,包括错误。承诺只是向调用站点发出完成(或错误)信号的另一种方法:

- (RXPromise*)refreshPostsInPart:(StreamPart*)part;

请注意,没有完成处理程序,而是该方法返回“Promise”。该方法实际上是异步的,并且像往常一样立即返回一个 Promise 对象。尽管承诺还没有“解决”——也就是说,它还没有包含结果——甚至没有错误。承诺 MSUT 最终由异步任务解决。一旦发生这种情况,呼叫站点就可以获得任务的结果。

通过使用 Promise,您可以删除 NSOperationQueues、NSOperations 以及解决方案中的块。RXPromise 支持取消(此示例中未显示),并且 Promise 也确实有一个更强大的概念来“链接”操作(在此示例中也未在每个细节中显示)。如果您阅读文档,我将不胜感激,并可能在网上查阅更多信息;)

另见维基:期货和承诺

当该承诺最终被异步任务解决时,它要么已经完成,要么被错误拒绝。然后您的呼叫站点可能想要对结果做一些事情。您可以通过注册成功和失败处理程序来完成此操作:

[self refreshPostsInPart:part]
.then(^id(id result){
    // On Success:
    NSLog(@"This is the result: %@", result);
    return nil;
}, ^id (NSError* error){
    // On Failure:
    NSLog(@"Error: %@", error);
    return nil;
});

也就是说,您使用该then表达式“设置”您的成功和/或失败处理程序:

[self refreshPostsInPart:part].then(<success-handler>, <failure-handler>); 

这是以下的简写形式:

RXPromise* resultPromise = [self refreshPostsInPart:part];
resultPromise.then(<success-handler>, <error-handler>);

请注意 - 这里 - 完成处理程序将在某些私有执行上下文中执行。但是,在 RXPromise 中,您可以显式定义要在处理程序执行的位置使用的队列thenOn,例如:

resultPromise.thenOn(dispatch_queue_get_main_queue(), 
^id(id result){
    // do something with result on the main queue
    return nil;
}, nil /*error handler not used*/);

现在,我们可以看一下该方法的实现refreshPostsInPart:

假设,您的类StreamPartNSArray在此解决方案中表示为 a,它模拟了一个相当简单的对象“流”。

该解决方案还利用了 RXPromise 库中的一个类方法,repeat它极大地简化了“异步循环”:

repeat声明如下:

typedef RXPromise* (^rxp_nullary_task)();

+ (RXPromise*) repeat:(rxp_nullary_task)block;

它在一个连续的循环中异步执行该块,直到该块返回nil或返回的 Promise 将被拒绝。

注意,repeat它本身是异步的,因此它返回一个 Promise。

因此, a 的规范实现repeat如下所示:

RXPromise* allFinishedPromise = [RXPromise repeat:^RXPromise*{
    if (no more input) {
        return nil;  // terminate the asynchronous loop
    }
    return [input asyncTask];  // asynchronously execute the next task with input
}];

可能的用法是:

allFinishedPromise.then(^id(id result){
    // in case of success, the result of repeat is always @"OK" 
}, 
^id(NSError* error){
    // if an error occurred, this is the error of the failing task
});

因此,我们还需要“任务”,它基本上是每次迭代执行的代码,这是一个异步任务(因此,它返回一个承诺):

-(RXPromise*) asyncTask;

这基本上实现了你的reloadPosts. 我暂时省略这个实现。

请注意,您的原始实现包含完成处理程序代码。我们不这样做。相反,我们像往常一样注册成功和失败处理程序。

此外,您可能希望继续处理一些特定的代码,一旦处理程序完成,这些代码就会在之后执行。也许您的处理程序本身就是一个异步方法!您可以以简洁的方式执行此类操作,例如:

假设,你已经实现asyncTask了(它基本上实现了你方法的任务部分reloadPosts),然后当它完成时想要执行另一个异步方法,然后当它也完成时想要打印结果:

[self asyncTask]
.then(^id (id result){
    // take result of asyncTask and pass it through another asynchronous task (which returns a Promise:
    return [self anotherAsyncTaskWithInput:result];
}, nil)
.then(^id (id result) {
     // here: result is the result of "anotherAsyncTaskWithInput:"
     NSLog(@"Final Result: %@", result);
}, nil);

现在,您知道如何“链接”异步任务,我们终于可以编写该方法的示例实现了refreshPostsInPart:

- (RXPromise*) refreshPostsWithArray(NSArray* inputs)
{
    const NSUInteger count = [inputs count];
    __block NSUInteger i = 0;
    return [RXPromise repeat:^RXPromise*{
        // Check termination condition:
        if (i >= count) {
            return nil;  // stream at EOS
        }
        RXPromise* finalResult = [inputs[i++] asyncTask]
        .then(^id(id result){
            // do something with result (that is your completion handler part of `reloadPosts `
            return nil;  // note: intermediate result will not be used in repeat!
        }, nil);
        return finalResult;
    }];
}

注意:我NSArrayStreamPart. 我们只需要知道输入何时被消耗,以便我们可以返回nil(而不是 Promise)以终止repeat循环。我想您知道如何将其映射到您的 StreamPart 类。

最后,我复制了一个功能性的 Foundation 控制台示例。这需要链接到RXPromise库。随意尝试它。

运行控制台示例

#import <Foundation/Foundation.h>
#import <RXPromise/RXPromise.h>
#import <RXPromise/RXPromise+RXExtension.h>

// As an example add a category for NSString to simulate an asynchronous task
@implementation NSString (Example)

- (RXPromise*) asyncTask
{
    RXPromise* promise = [[RXPromise alloc] init];
    
    dispatch_async(dispatch_get_global_queue(0, 0), ^{
        int count = 10;
        while (count--) {
            usleep(100*1000);
        }
        if ([self isEqualToString:@"X"]) {
            [promise rejectWithReason:@"Bad Input"];
        }
        else {
            [promise fulfillWithValue:[self capitalizedString]];
        }
    });
    
    return promise;
}

@end


RXPromise* performTasksWithArray(NSArray* inputs)
{
    const NSUInteger count = [inputs count];
    __block NSUInteger i = 0;
    return [RXPromise repeat:^RXPromise*{
        if (i >= count) {
            return nil;
        }
        return [inputs[i++] asyncTask].then(^id(id result){
            NSLog(@"%@", result);
            return nil;  // intermediate result not used in repeat
        }, nil);
    }];
}


int main(int argc, const char * argv[])
{
    @autoreleasepool {
        
        NSArray* inputs = @[@"a", @"b", @"c", @"X", @"e", @"f", @"g"];
        
        [performTasksWithArray(inputs)
        .then(^id(id result){
            NSLog(@"Finished: %@", result);
            return nil;
        }, ^id(NSError* error){
            NSLog(@"Error occured: %@", error);
            return nil;
        }) runLoopWait];
        
    }
    return 0;
}

注意:当前输入模拟错误:@"X" 导致 asyncTask 失败!

控制台日志:

2014-02-26 23:06:44.412 Sample7[1410:1003] A
2014-02-26 23:06:45.422 Sample7[1410:2403] B
2014-02-26 23:06:46.434 Sample7[1410:1003] C
2014-02-26 23:06:47.578 Sample7[1410:2403] Error occured: Error Domain=RXPromise Code=-1000 "The operation couldn’t be completed. Bad Input" UserInfo=0x10010ba00 {NSLocalizedFailureReason=Bad Input}
于 2014-02-26T23:56:30.997 回答