0

今天早些时候,我问了以下问题:iOS 块在视图推送时被停止

我提到的操作(OP1)实际上是对我的服务器的“http get”,使用 NSURLConnection。
经过更多调查后,我发现该块实际上并没有“死亡”。真正发生的是请求实际上是发送的(服务器端记录它),即使在视图被推送之后(通过 [NSThread sleep:10] 验证)。服务器响应,但如果 view2 已被推送,则应用程序端不会发生任何事情!几乎就好像连接失去了它的代表!我正在研究的另一种可能性是“NSURLConnection 与rsMainLoop相关的事实?”

任何人都可以帮忙吗?

请不要忘记:
0。只要在操作完成之前不推送 view2,一切正常。
1. 请求是异步发送的
2. 我设置了委托,只要视图不改变它就可以工作
3. 视图1使用“单例对象引用”属性“OP1Completed”开始操作
4. 视图2检查 OP1 的完成情况通过“singleton object reference”上的属性
5. 视图2通过转到“singleton.OP1Result”属性获取“结果”

编辑1:
好的,让我们有一些代码。首先是我的单例的相关代码(名为“交互”):

-(void)loadAllContextsForUser:(NSString *)username{
userNameAux = username;
_loadingContextsCompleted = NO;
if (contextsLoaderQueue == NULL) {
    contextsLoaderQueue = dispatch_queue_create("contextsLoaderQueue", NULL);
}

dispatch_async(contextsLoaderQueue, ^{
    NSLog(@"Loading all contexts block started");
    [self requestConnectivity];

    dispatch_async(dispatch_get_main_queue(), ^{
        [Util Get:[NSString stringWithFormat:@"%@/userContext?username=%@", Util.azureBaseUrl, [username stringByAddingPercentEscapesUsingEncoding:NSUTF8StringEncoding]]
     successBlock:^(NSData *data, id jsonData){
         NSLog(@"Loading all contexts block succeeded");
         if([userNameAux isEqualToString:username]){
             _allContextsForCurrentUser = [[NSSet alloc]initWithArray: jsonData];
         }
     } errorBlock:^(NSError *error){
         NSLog(@"%@",error);
     } completeBlock:^{
         NSLog(@"load all contexts for user async block completed.");
         _loadingContextsCompleted = YES;
         [self releaseConnectivity];
     }];
    });

    while (!_loadingContextsCompleted) {
        NSLog(@"loading all contexts block waiting.");
        [NSThread sleepForTimeInterval:.5];
    }
});
NSLog(@"Load All Contexts Dispatched. It should start at any moment if it not already.");
}

这是 Util 类,它实际上处理请求/响应

-(id)initGet:(NSString *)resourceURL successBlock:(successBlock_t)successBlock errorBlock:(errorBlock_t)errorBlock completeBlock:(completeBlock_t)completeBlock;{
if(self=[super init]){
    _data=[[NSMutableData alloc]init];
}

_successBlock = [successBlock copy];
_completeBlock = [completeBlock copy];
_errorBlock = [errorBlock copy];

NSURL *url = [NSURL URLWithString:resourceURL];
NSMutableURLRequest *request = [NSURLRequest requestWithURL:url];
[[NSURLConnection alloc] initWithRequest:request delegate:self startImmediately:YES];
//[_conn scheduleInRunLoop:[NSRunLoop mainRunLoop] forMode:NSDefaultRunLoopMode];
//[_conn start];
NSLog(@"Request Started.");

return self;
}

- (void)connection:(NSURLConnection *)connection didReceiveResponse:(NSURLResponse *)response
{
    [_data setLength:0];
}

- (void)connection:(NSURLConnection *)connection didReceiveData:(NSData *)data
{
    [_data appendData:data];
}
- (void)connectionDidFinishLoading:(NSURLConnection *)connection
{
id jsonObjects = [NSJSONSerialization JSONObjectWithData:_data options:NSJSONReadingMutableContainers error:nil];

id key = [[jsonObjects allKeys] objectAtIndex:0];
id jsonResult = [jsonObjects objectForKey:key];

_successBlock(_data, jsonResult);
_completeBlock();
}

- (void)connection:(NSURLConnection *)connection didFailWithError:(NSError *)error
{
_errorBlock(error);
_completeBlock();
}

最后是相关部分 VC1(推入 VC2)

- (IBAction)loginClicked {
NSLog(@"login clicked. Preparing to exibit next view");

UIStoryboard *storyboard = [UIStoryboard storyboardWithName:@"MainStoryboard_iPhone" bundle:nil];
AuthenticationViewController *viewController = (AuthenticationViewController *)[storyboard instantiateViewControllerWithIdentifier:@"ContextSelectionView"];

NSLog(@"Preparation completed. pushing view now");

[self presentViewController:viewController animated:YES completion:nil];
}
4

5 回答 5

1

您可能会感到惊讶,但有几个解决方案 - 其中一些非常常见并且可以很容易地实现;)尽管这个答案很荒谬,但您的问题的实际解决方案不会超过几行代码. :)

您遇到了一个典型的“异步问题”——嗯,它不是问题,而是当今典型的编程任务。

你所拥有的是一个异步任务,OP1。这将从 ViewController 1 (VC1) 中开始,并且在稍后的某个不确定的时间,它最终会产生结果或错误。

OP1的最终结果应稍后在 VC2 中处理。

客户端可以通过几种方法获得最终结果,例如:通过 KVO、委托方法、完成块、回调函数、future 或 promise 以及每个通知。

上述这些方法有一个共同点:调用站点由异步结果提供程序通知(反之亦然)。

在结果可用之前轮询结果是一种不好的方法。同样,挂在信号量中并阻塞当前线程直到结果“发出信号”同样不是最理想的。

您可能熟悉完成块。当结果可用时通知调用站点的典型异步方法如下所示:

typedef void (^completion_block_t)(id result);

- (void) doSomethingAsyncWithCompletion:(completion_block_t)completionHandler;

注意:调用站点提供完成处理程序,而异步任务在完成调用块,并将其结果(或错误)传递给块的结果参数。除非另有说明,否则将执行块的执行上下文(即线程或调度队列或 NSOperationQueue)是未知的。

但是在考虑您的问题时,简单的异步函数和完成处理程序并不能产生可行的解决方案。你不能轻易地将这个“方法”从 VC1 传递到 VC2,然后再以某种方式在 VC2 中“附加”一个完成块。

幸运的是,任何异步任务都可以封装到NSOperation. AnNSOperation有一个完成块作为属性,可以由调用站点或其他地方设置。并且一个NSOperation对象可以很容易地从 VC1 传递到 VC2。VC2 只是简单地在操作中添加一个完成块,并最终在其完成并且结果可用时得到通知。

但是,虽然这对于您的问题来说是一个可行的解决方案 - 实际上这种方法存在一些问题 - 我不想详细说明,而是提出一个更好的解决方案:“Promises”。

“Promise”代表异步任务的最终结果。也就是说,即使异步任务的结果尚未评估,promise 也会存在。Promise 是一个可以发送消息的普通对象。因此,Promises 可以像 NSOperations 一样被传递。Promise 是异步方法/函数的返回值:

-(Promise*) doSomethingAsync;

不要将 Promise 与异步函数/方法/任务/操作不匹配 - Promise 只是任务最终结果的表示。

Promise 必须最终由异步任务解决——也就是说,任务必须向 Promise 发送“fulfill”消息以及结果值,或者它必须向 Promise 发送“reject”消息以及错误。承诺保留从任务传递的结果值的引用。

一个 Promise 只能解决一次!

为了获得最终结果 ,客户端可以“注册”一个成功处理程序和一个错误处理程序。当任务履行承诺(即,它成功)时,将调用成功处理程序,当任务拒绝承诺时,将调用错误处理程序,并将原因作为错误对象传递。

假设一个 Promise 的特定实现,解决一个 Promise 可能看起来像这样:

- (Promise*) task {
    Promise* promise = [Promise new];
    dispatch_async(private_queue, ^{
        ...
        if (success) {
            [promise fulfillWithValue:result];
        }
        else {
            NSError* error = ...; 
            [promise rejectWithReason:error];
        }
    });     
    return promise;
}

客户端“注册”处理程序以获取最终结果,如下所示:

Promise* promise = [self fetchUsers];

promise.then( <success handler block>, <error handler block> );

成功处理程序和错误处理程序块声明如下:

typedef  id (^success_handler_block)(id result);
typedef  id (^error_handler_block)(NSError* error);

为了只“注册”一个成功处理程序(对于这种情况,异步任务“成功返回”),可以这样写:

promise.then(^id(id users) {
    NSLog(@"Users:", users);
    return nil;
}, nil);

如果任务成功,将调用处理程序 - 它将用户打印到控制台。当任务失败时,将不会调用成功处理程序。

为了只是“注册”一个错误处理程序(对于这种情况,异步任务失败),我们会写:

promise.then(nil, ^id(NSError* error) {
    NSLog(@"ERROR:", error);
    return nil;
}, nil);

如果任务成功,则不会调用错误处理程序。仅当任务失败(或任何子任务)时,才会调用此错误处理程序。

当异步任务的结果最终可用时,处理程序中的代码将“在某些未指定的执行上下文中”执行。这意味着,它可以在任何线程上执行。(注意:有一些方法可以指定执行上下文,比如主线程)。

一个 Promise 可以注册多个处理程序对。您可以根据需要添加任意数量的处理程序,以及在何时何添加。现在,您应该了解与实际问题的联系:

您可以在 VC1 中启动一个异步任务,并获得一个 Promise。然后将此承诺传递给 VC2。在 VC2 中,您可以添加处理程序,当结果最终可用时将调用该处理程序。

当将 Promise 传递给 VC2 时,即当 Promise 已经解决时,不要担心结果实际上已经可用。您仍然可以添加处理程序,并且它们会被正确触发(立即)。

您还可以“链接”多个任务 - 即,在 task1 完成时调用 task2 一次。四个异步任务的“链”或“延续”如下所示:

Promise* task4Promise = 
[self task1]
.then(^id(id result1){
    return [task2WithInput:result1];
}, nil)
.then(^id(id result2){
    return [task3WithInput:result2];
}, nil)
.then(^id(id result3){
    return [task4WithInput:result3];
}, nil);

task4Promise表示task4WithInput:.

还可以并行执行任务,例如 taskB 和 taskC,当 taskA 成功完成时,它们将并行启动:

Promise* root = [self taskA];
root.then(^id(id result){
    return [self taskB];
}, nil);
root.then(^id(id result){
    return [self taskC];
}, nil);

使用这种方案,可以定义任务的非循环图,其中每个任务都依赖于其后继者(“父级”)的成功执行。“错误”将传递到根,并由最后一个错误处理程序(如果有)处理。

Objective-C 有一些实现。我自己写了一个:“RXPromise”(可在 GitHub 上获得)。最强大的功能之一是“取消”——它不是 Promise 的标准功能,但在 RXPromise 中实现。有了这个,您可以有选择地取消异步任务树。

还有很多关于承诺的内容。您可以搜索网络,尤其是在 JavaScript 社区中。

于 2013-09-05T21:50:40.737 回答
1

我不确定我是否理解第一个控制器中进行的工作流程——具体来说,用户为启动下载做了什么,以及在下一个控制器出现之前他还做了什么(以及当该​​控制器被实例化时)。当我过去制作需要从多个类下载的应用程序时,我创建了一个下载类来创建 NSURLConnection,并实现所有回调。它有一个委托协议方法将数据(原始数据或错误对象)发送回其委托。

我使用两个按钮制作了一个简单的测试用例来模拟我认为您的工作流程。一个实例化一个 Downloader 类实例,创建下一个控制器,将其设置为下载器的委托,然后开始下载。第二个按钮会推动第二个控制器。无论何时发生推送,这都有效,但我不知道它是否与您的情况相关(我使用网络链接调节器进行测试以模拟慢速连接)。

第一个控制器:

#import "ViewController.h"
#import "ReceivingViewController.h"
#import "Downloader.h"

@interface ViewController ()
@property (strong,nonatomic) ReceivingViewController *receiver;
@end

@implementation ViewController

-(IBAction)buttonClicked:(id)sender {
    Downloader *loader = [Downloader new];
    self.receiver = [self.storyboard instantiateViewControllerWithIdentifier:@"Receiver"];
    loader.delegate = self.receiver;
    [loader startLoad];
}

-(IBAction)goToReceiver:(id)sender {
    [self.navigationController pushViewController:self.receiver animated:YES];
}

下载类 .h:

@protocol DownloadCompleted <NSObject>
-(void)downloadedFinished:(id) dataOrError;
@end

@interface Downloader : NSObject

@property (strong,nonatomic) NSMutableData *receivedData;
@property (weak,nonatomic) id <DownloadCompleted> delegate;

-(void)startLoad;

下载器.m:

-(void)startLoad {
    NSLog(@"start");
    NSURLRequest *request = [NSURLRequest requestWithURL:[NSURL URLWithString:@"http://www.google.com"] cachePolicy:NSURLRequestReloadIgnoringLocalAndRemoteCacheData timeoutInterval:10];
    NSURLConnection *connection = [NSURLConnection connectionWithRequest:request delegate:self];
    if (connection) self.receivedData = [NSMutableData new];
}

-(void)connection:(NSURLConnection *)connection didReceiveResponse:(NSURLResponse *)response {
    self.receivedData.length = 0;
}


-(void)connection:(NSURLConnection *)connection didReceiveData:(NSData *)data {
    [self.receivedData appendData:data];
}


-(void)connection:(NSURLConnection *)connection didFailWithError:(NSError *)error {
    [self.delegate downloadedFinished:error];
}


-(void)connectionDidFinishLoading:(NSURLConnection *)connection {
    [self.delegate downloadedFinished:self.receivedData];
}

-(void)dealloc {
    NSLog(@"In Downloader dealloc. loader is: %@",self);
}

第二个控制器:

@interface ReceivingViewController ()
@property (strong,nonatomic) NSData *theData;
@end

@implementation ReceivingViewController


-(void)downloadedFinished:(id)dataOrError {
    self.theData = (NSData *)dataOrError;
    NSLog(@"%@",self.theData);
}

-(void)viewDidAppear:(BOOL)animated {
    [super viewDidAppear:animated];
    NSLog(@"%@",self.theData);
}
于 2013-09-07T02:19:23.643 回答
0

所以,这是我认为肯定会起作用的:

将标志传递给新控制器。如果标志未完成,则开始在新 VC 中重新加载,并确保在完成加载之前没有显示任何数据。

我确实认为随着新的 VC 被推送而线程停止是很奇怪的,因为当我使用 AFNetworking 调度异步调用时,即使在推送了新的 VC 之后它也会继续加载。也许如果您使用不同的框架,您应该使用 AFNetworking。

所以,如果你的线程在新的 VC 被推送后确实继续运行(我怀疑它确实如此——你只是认为它不会继续运行,因为它会使代码崩溃),然后尝试以下操作:

a) 传递标志,如果操作完成,则正常进行
b) 如果没有,不加载任何东西并在两者之间调用某种委托方法,检查标志是否设置,如果设置则返回数据。

如果您对如何设置代表有任何疑问,请询问,我可以填写一些详细信息。

于 2013-09-05T20:09:33.463 回答
0

正如您在第一个问题的评论中已经提到的那样:您可能有两个问题:

  1. 一个设计问题
  2. 代码问题,导致阻塞。(但如果没有代码,这很难弄清楚)。

让我们提出一个实用的方法:

比如说,我们的单例是一些执行 HTTP 请求的“Loader”类。而不是轮询确定网络请求状态的属性,您应该返回一些您可以请求状态的对象,或者更好的是 VC2 可以注册一个完成块,该完成块在请求完成被调用。

可以NSOperation“使用”表示异步网络请求的最终结果。但这有点笨拙——假设我们有一个子类 RequestOperation:

RequestOperation* requestOp = [[Loader sharedLoader] fetchWithURL:url];

现在,“requestOp”代表您的网络请求,包括最终结果。

您可以在 VC1 中获取此操作。

您可能不想向共享加载器询问特定操作,因为它可能是无状态 的——也就是说,它本身并不跟踪请求操作。考虑一下,您想Loader多次使用 class 来启动网络请求 - 可能是并行的。那么,当您询问其中一个属性告诉您有关请求状态的某些信息时,您指的是哪个请求(它不会工作)。Loader

所以,再次回到工作方法和 VC1:

假设,在 VC1 中,您获得的RequestOperation对象是NSOperation. 假设,RequestOperation有一个属性responseBody——它是一个NSData表示请求操作的最终响应数据的对象。

为了获得请求的最终响应主体,您不能只询问属性:连接可能仍在运行 - 您会得到nil或垃圾,或者您可能会阻塞线程。该行为取决于RequestOperation.

解决方法如下:

在 VC2 中:

我们假设,VC1 已将requestOp “传递”给 VC2(例如在 中prepareForSegue:sender:)。

为了以异步正确的方式检索响应正文,您需要一些额外的步骤:

创建一个NSBlockOperation执行处理响应正文的块,例如:

NSBlockOperation* handlerOp = [NSBlockOperation blockOperationWithBlock:^{
    NSData* body = requestOp.responseBody;
    dispatch_async(dispatch_get_main_queue(), ^{
        self.model = body;
        [self.tableView reloadData];
    });
}];

然后,使handlerOp依赖于requestOp - 即,在requestOp完成时开始执行handlerOp

[handlerOP addDependency:requestOp];

handlerOp添加到队列中,以便执行:

[[NSOperation mainQueue] addOperation:handlerOp];

这仍然需要您“异步”思考 - 没有办法解决这个问题。最好的是,习惯于实​​际的模式和成语。


另一种方法是使用 RXPromise(来自第三方库):

在 VC1 中:

requestPromise = [Loader fetchWithURL:url];

现在,在 VC2 中:

我们假设,VC1 已将requestPromise “传递”给 VC2(例如在 中prepareForSegue:sender:)。

例如在viewDidLoad

requestPromise.thenOn(dispatch_get_main_queue(), ^id(id responseBody){
    // executes on main thread!
    self.model = responseBody;
    [self.tableView reloadData];
    return nil;
}, nil);

奖金:

如果需要,您可以随时通过发送到 Promise来取消网络请求cancel

- (void)viewWillDisappear:(BOOL)animated {
    [super viewWillDisappear:animated];
    [self.requestPromise cancel];
    self.requestPromise = nil;
}
于 2013-09-06T11:28:35.407 回答
0

我已经想通了。在我的第二个视图中(我 w8 操作完成)我不能 w8 使用 ThreadSleep!我必须使用[[NSRunLoop currentRunLoop] runMode:NSDefaultRunLoopMode beforeDate:[NSDate distantFuture]];

于 2013-09-09T18:47:58.233 回答