6

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

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

1) 在单独的辅助线程上执行每个入队的 NSURLConnection。NSOperationQueue 在后台的辅助线程上执行每个入队操作,因此我不需要明确地执行任何操作来生成辅助线程,除了在 NSOperation 子类的重写启动方法中启动我的 NSURLConnection 并为生成的辅助线程运行运行循环直到 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 请求完成之前不会返回到池中。

我的印象是我仍然通过使用 NSRunLoopCommonModes 调度 NSURLConnection 与第二种方法同时运行。换句话说,我认为我使用 NSRunLoopCommonModes 而不是多线程来实现并发性,以便 NSURLConnection 的观察者会尽快调用 connectionDidFinishLaunching 或 connectionDidFailWithError ,而不管主线程在那个时候对 UI 做了什么时间。

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

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

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

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

如果答案更具描述性,我将不胜感激,因为它应该为我清除更多关于 NSRunLoop 和 NSRunLoopModes 如何工作的疑虑。只是为了说明我已经阅读了很多次的文档。

4

4 回答 4

19

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

我在 NSOperation 子类的 start 方法中有这个

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

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

现在的问题是上面的 initWithRequest:delegate: 方法实际上使用 NSDefaultRunLoopMode 在默认运行循环中调度 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: 方法将其安排在运行循环和选择的模式中。

现在,从方法 scrollViewWillEndDragging:withVelocity:targetContentOffset 发起的 NSURLConnection 正在调用它的委托方法 connectionDidFinishLoading/connectionDidFailWithError 而滚动视图仍在滚动且尚未完成滚动。

我希望这可以帮助别人。

于 2012-04-13T01:37:26.140 回答
1

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

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

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

通常,例如,NSURLConnection立即维修并不重要。通常允许它等待主线程的运行循环来处理它是可以的。NSURLConnection考虑到,出于与滚动时不会服务的运行循环源相同的原因,在后台线程上处理它不可能产生用户可见的效果。例如,它将如何影响您的应用程序的 UI?它会使用-performSelectorOnMainThread:..或类似的东西来安排更新。NSURLConnection但这与运行循环源一样可能会被饿死。

但是,如果您绝对不能忍受这种可能的延迟,那么在将您NSURLConnection的 s 安排在主线程上或将它们全部安排在单独的线程上之间有一个中间立场。您可以将它们全部安排在同一个线程上,但不能安排在主线程上。您可以创建一个驻留在其运行循环中的单个线程。然后,在您当前正在执行-performSelectorOnMainThread:...的操作中,您可以改为执行-performSelector:onThread:....

于 2012-04-13T00:31:06.837 回答
0

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

部分代码如下:

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: 方法创建连接并为 startImmediately 参数提供 NO,则可以在使用 start 方法启动连接之前将连接安排在不同的运行循环或模式上。您可以在多个运行循环和模式上安排连接,或者在多个模式下在同一个运行循环上安排连接。您无法在连接开始后重新安排它。

于 2013-08-15T14:58:49.080 回答
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
于 2017-06-30T14:36:31.857 回答