NSURLConnection与NSRunLoopCommonModes

时间:2012-04-12 22:46:18

标签: objective-c ios concurrency nsurlconnection nsrunloop

我为我的iOS应用编写了自己的HTTPClient实现,以异步方式下载指定URL的内容。 HTTPClient使用NSOperationQueue将NSURLConnection请求排入队列。我选择了NSOperationQueue,因为我想在任何时候取消任何或所有正在进行的NSURLConnection。

我对如何实现我的HTTPClient进行了大量的研究,我有两个选择来执行NSURLConnection:

1)在单独的辅助线程上执行每个排队的NSURLConnection。 NSOperationQueue在后台执行辅助线程上的每个排队操作,因此除了在重写的NSOperation子类的start方法中启动我的NSURLConnection并为生成的辅助线程运行runloop直到connectionDidFinishLoading或者之外,我不需要做任何显式生成辅助线程的事情。调用connectionDidFailWithError。它看起来如下:

if (self.connection != nil) {
            do {
                [[NSRunLoop currentRunLoop] runMode:NSDefaultRunLoopMode
                                         beforeDate:[NSDate distantFuture]];
            } while (!self.isFinished);
}

2)在主线程上执行每个排队的NSURLConnection。对于start方法中的这个,我使用performSelectorOnMainThread并在主线程上再次调用start方法。通过这种方法,我使用NSRunLoopCommonModes安排NSURLConnection,如下所示:

[self.connection scheduleInRunLoop:[NSRunLoop currentRunLoop] forMode:NSRunLoopCommonModes];

我选择了第二种方法并实施了它。从我的研究来看,第二种方法似乎更好,因为它没有为每个NSURLConnection启动一个单独的辅助线程。现在,在任何时候,在应用程序中可能会有许多请求同时进行,并且使用第一种方法,这意味着将生成相同数量的辅助线程,并且在关联的URL请求完成之前不会返回池。 / p>

我的印象是,我仍然使用NSRunLoopCommonModes安排NSURLConnection与第二种方法并行运行。在使用这种方法的其他方面,我认为我使用NSRunLoopCommonModes而不是多线程来实现并发性,这样NSURLConnection的观察者可以尽快调用connectionDidFinishLaunching或connectionDidFailWithError,无论主要线程在UI处于哪个主要线程。时间。

不幸的是,当我的同事今天早上向我展示了当前的实现时,NSURLConnection在其中一个视图控制器上的滚动视图停止滚动之前不会返回。当滚动视图即将停止滚动时,启动NSURLRequest获取数据,但即使它在滚动视图停止调用之前完成,NSURLConnection也不会回调connectionDidFinishLoading或connectionDidFailWithError,直到滚动视图完全停止滚动。这意味着在主线程上使用NSRunLoopCommonModes调度NSURLConnection以获得与UI操作(触摸/滚动)的真实并发的整个想法被证明是错误的并且NSURLConnection仍然等待直到主线程忙于滚动滚动视图。

我尝试切换到使用辅助线程的第一种方法,它就像一个魅力。当滚动视图仍在滚动时,NSURLConnection仍会调用其协议方法之一。这很清楚,因为现在NSURLConnection没有在主线程上运行,所以它不会等待滚动视图停止滚动。

我真的不想使用第一种方法,因为它由于多线程而很昂贵。

如果我对第二种方法的理解不正确,有人可以告诉我吗?如果它是正确的,那么使用NSRunLoopCommonModes安排NSURLConnection的原因可能无法按预期工作?

如果答案更具描述性,我将非常感激,因为对于我来说,NSRunLoop和NSRunLoopModes的确切运作方式应该让我更加怀疑。只是为了说明我已经多次阅读了这些文档。

4 个答案:

答案 0 :(得分:18)

事实证明这个问题比我想象的要简单。

我在NSOperation子类

的start方法中有这个
self.connection = [[NSURLConnection alloc] initWithRequest:self.urlRequest
                                                              delegate:self];

[self.connection scheduleInRunLoop:[NSRunLoop currentRunLoop] forMode:NSRunLoopCommonModes];

现在的问题是上面的initWithRequest:delegate:方法实际上使用NSDefaultRunLoopMode在默认的runloop中调度NSURLConnection,并完全忽略我实际尝试使用NSRunLoopCommonModes安排它的下一行。通过改变以上两行,按预期工作。

self.connection = [[NSURLConnection alloc] initWithRequest:self.urlRequest
                                                              delegate:self startImmediately:NO];

[self.connection scheduleInRunLoop:[NSRunLoop currentRunLoop] forMode:NSRunLoopCommonModes];

[self.connection start];

这里的实际问题是我必须使用带有参数startImmediately的构造函数方法初始化NSURLConnection。当我为参数startImmediately传递NO时,未使用默认运行循环调度连接。它可以通过调用scheduleInRunLoop:forMode:method在运行循环和选择模式中进行调度。

现在NSURLConnection从方法scrollViewWillEndDragging启动:withVelocity:targetContentOffset在滚动视图仍在滚动且尚未完成滚动时调用其委托方法connectionDidFinishLoading / connectionDidFailWithError。

我希望这有助于某人。

答案 1 :(得分:1)

调度运行循环源不允许源的回调与其他源的回调同时运行。

在网络通信的情况下,无论应用程序做什么,内核处理的内容(如接收和缓冲数据包)都会同时发生。然后,内核将套接字标记为可读或可写,例如,如果线程在此类调用中被阻止,则可以唤醒select()kevent()调用。如果您的线程正在执行其他操作,例如处理滚动事件,那么在执行返回到运行循环之前,它不会注意到套接字的可读性/可写性。只有这样,NSURLConnection的运行循环源才会调用它的回调,让NSURLConnection处理套接字状态改变,并可能调用你的委托方法。

接下来是当运行循环有多个源并且多个源准备就绪时会发生什么的问题。例如,事件队列中有更多滚动事件,您的套接字也是可读或可写的。理想情况下,您可能想要一个公平的算法来为运行循环源提供服务。实际上,GUI事件可能优先于其他运行循环源。此外,运行循环源可以具有相对于其他源的固有优先级(“顺序”)。

通常情况下,提及NSURLConnection即时服务并不重要。通常可以让它等待主线程的运行循环来绕过它。考虑到这一点,出于同样的原因,在滚动时不会为NSURLConnection的运行循环源提供服务,因此无法在后台线程上处理它可能具有用户可见的效果。例如,它会如何影响您应用的用户界面?它将使用-performSelectorOnMainThread:..或类似的东西来安排更新。但这很可能会被NSURLConnection运行循环源所困扰。

但是,如果您绝对不能遵守这种可能的延迟,那么在主线程上安排NSURLConnection或在不同的线程上安排它们之间存在中间立场。您可以在同一个线程上安排所有这些线程,但不能在主线程上安排。您可以创建一个在其运行循环中停放的单个线程。然后,您当前正在执行-performSelectorOnMainThread:...,您可以改为-performSelector:onThread:...

答案 2 :(得分:0)

我的测试在第二个线程上的“scheduleInRunLoop:(NSRunLoop *)aRunLoop forMode:(NSString *)mode”成功,也可以将scheduleInRunLoop从第二个线程调回到主运行循环。

部分代码如下:

NSRunLoop *runloop; //global

self.connection = [[NSURLConnection alloc] initWithRequest:self.urlRequest delegate:self startImmediately:NO];

[self.connection scheduleInRunLoop:runloop forMode:NSRunLoopCommonModes];

[self.connection start];

如果你想在另一个线程中运行NSURLConnection,你应该在线程的main方法中创建一个这样的运行循环(线程应该在上面的代码开始之前启动):

runloop = [NSRunLoop currentRunLoop];

while (!finished)
{
   [[NSRunLoop currentRunLoop] runUntilDate:[NSDate dateWithTimeIntervalSinceNow:1]];
}

官方文件非常有用:

  

默认情况下,会在当前线程上安排连接   创建时的默认模式。如果您创建了一个连接   initWithRequest:delegate:startImmediately:方法并为其提供NO   startImmediately参数,您可以在a上安排连接   使用start方法启动之前的不同运行循环或模式。   您可以在多个运行循环和模式上安排连接,也可以安装   多个模式下的相同运行循环。你不能重新安排一个   它开始后的连接。

答案 3 :(得分:0)

使用Ken Thomases的指导'回答我为复制粘贴类型的编码器制作了这个:

static NSThread *connectionProcessingThread;
static NSTimer *keepRunloopBusy;
static NSRunLoop *oauth2runLoop;

+ (void)initialize
{
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
    connectionProcessingThread = [[NSThread alloc] initWithBlock:^{
        oauth2runLoop = [NSRunLoop currentRunLoop];
        keepRunloopBusy = [NSTimer timerWithTimeInterval:DBL_MAX repeats:YES block:^(NSTimer* timer) {
            NSLog(@"runloop is kept busy with this keepalive work");
        }];
        [oauth2runLoop addTimer:keepRunloopBusy forMode:NSRunLoopCommonModes];
        [oauth2runLoop run];
    }];
    [connectionProcessingThread start];
    atomic_thread_fence(memory_order_release);
});
}

然后你分叉

NSURLConnection *aConnection = [[NSURLConnection alloc] initWithRequest:startRequest delegate:self startImmediately:NO];    // don't start yet
if( [NSRunLoop currentRunLoop] != [NSRunLoop mainRunLoop]) {
    atomic_thread_fence(memory_order_acquire);
    [aConnection scheduleInRunLoop:oauth2runLoop forMode:NSRunLoopCommonModes];
} else {
    [aConnection scheduleInRunLoop:[NSRunLoop mainRunLoop] forMode:NSRunLoopCommonModes]; // let's first schedule it in the main runloop.
}
[aConnection start];    // now start