Як встановити час очікування за допомогою AFNetworking


79

У моєму проекті використовується AFNetworking.

https://github.com/AFNetworking/AFNetworking

Як встановити тайм-аут? Банкомат без підключення до Інтернету блок блокування не спрацьовує протягом приблизно 2 хвилин. Чекай довго ...


2
Я настійно не рекомендую будь-яке рішення, яке намагається замінити інтервали очікування, особливо ті, що використовуються performSelector:afterDelay:...для ручного скасування існуючих операцій. Будь ласка, перегляньте мою відповідь, щоб дізнатися більше
— mattt

Відповіді:


110

Зміна інтервалу очікування майже напевно не є найкращим рішенням проблеми, яку ви описуєте. Натомість, здається, що саме ви хочете, щоб клієнт HTTP обробляв мережу, яка стає недосяжною, ні?

AFHTTPClientвже має вбудований механізм , щоб ви знаєте , коли підключення до Інтернету втрачається, -setReachabilityStatusChangeBlock:.

Запити можуть тривати довго в повільних мережах. Краще довірити iOS, щоб він знав, як поводитись із повільними зв’язками, і визначити різницю між цим та відсутністю зв’язку взагалі.


Щоб розширити мої міркування щодо того, чому слід уникати інших підходів, згаданих у цій темі, ось кілька думок:

  • Запити можуть бути скасовані ще до їх початку. Подання запиту в чергу не дає жодних гарантій щодо того, коли він фактично починається.
  • Інтервали очікування не повинні скасовувати тривалі запити, особливо POST. Уявіть, якщо ви намагалися завантажити або завантажити 100 Мб відео. Якщо запит виконується якнайкраще в повільній мережі 3G, чому б вам не потрібно його зупиняти, якщо це займає трохи більше часу, ніж очікувалося?
  • Це performSelector:afterDelay:...може бути небезпечним у багатопотокових програмах. Це відкриває себе перед незрозумілими та важкими для налагодження умовами перегонів.

Я вважаю, що це тому, що питання, незважаючи на його назву, полягає в тому, як якомога швидше провалитися у справах "Без Інтернету". Правильний спосіб зробити це - використовувати доступність. Використання тайм-аутів або не працює, або має високі шанси внести важко помічені / виправлені помилки.
— Mihai Timar

2
@mattt, хоча я погоджуюся з вашим підходом, я боюся з проблемою підключення, коли мені справді потрібна функція очікування. Річ у тому, що я взаємодію з пристроєм, який має "точку доступу WiFi". При підключенні до цієї "мережі WiFi", з точки зору доступності, я не підключений, хоча я можу виконувати запити на пристрій. З іншого боку, я хочу мати можливість визначити, чи доступний сам пристрій. Будь-які думки? Дякую
— Ставаш

42
хороші бали, але не відповідає на питання, як встановити тайм-аут
— Max MacLeod

3
У AFNetworking Reference "Доступність мережі - це діагностичний інструмент, за допомогою якого можна зрозуміти, чому запит міг провалитись. Його не слід використовувати для визначення запиту чи ні". у розділі 'setReachabilityStatusChangeBlock'. Тому я думаю, що 'setReachabilityStatusChangeBlock' не є рішенням ...
— LKM

1
Я розумію, чому нам слід двічі подумати про встановлення тайм-ауту вручну, але ця прийнята відповідь не дає відповіді на запитання. Мій додаток повинен якнайшвидше знати про втрату з’єднання з Інтернетом. Використовуючи AFNetworking, ReachabilityManager не виявляє випадку, коли пристрій підключено до точки доступу Wi-Fi, але сама точка доступу втрачає Інтернет (на її виявлення часто потрібні хвилини). Отже, запит до Google із таймаутом ~ 5 секунд, здається, є моїм найкращим варіантом у цьому сценарії. Якщо немає чогось, чого мені не вистачає?
— Таннер Семерад

44

Я настійно рекомендую поглянути на відповідь Метта вище - хоча ця відповідь не порушує проблем, про які він згадує загалом, для оригінальних питань плакатів перевірка доступності набагато краще підходить.

Однак, якщо ви все-таки хочете встановити тайм-аут (без усіх проблем, властивих performSelector:afterDelay:і т. Д., Тоді запит на витяг, про який згадує Lego, описує спосіб зробити це як один із коментарів, ви просто робите:

NSMutableURLRequest *request = [client requestWithMethod:@"GET" path:@"/" parameters:nil];
[request setTimeoutInterval:120];

AFHTTPRequestOperation *operation = [client HTTPRequestOperationWithRequest:request success:^{...} failure:^{...}];
[client enqueueHTTPRequestOperation:operation];

але див. застереження @KCHarwood згадує, що, схоже, Apple не дозволяє це змінювати для запитів POST (що виправлено в iOS 6 і вище).

Як зазначає @ChrisopherPickslay, це не загальний тайм-аут, це тайм-аут між отриманням (або надсиланням даних). Я не знаю жодного способу розумно зробити загальний тайм-аут. У документації Apple для setTimeoutInterval сказано:

Інтервал очікування, в секундах. Якщо під час спроби з'єднання запит залишається неактивним довше, ніж інтервал очікування, запит вважається тимчасовим. Інтервал часу очікування за замовчуванням - 60 секунд.


Так, я спробував, і це не працює при виконанні запиту POST.
— borisdiakur

7
sidenote: Apple виправила це в iOS 6
— Raptor

2
Це не робить того, що ви пропонуєте. timeoutInterval- таймер простою, а не час очікування запиту. Отже, вам не доведеться отримувати жодних даних протягом 120 секунд, щоб зазначений час закінчився. Якщо дані повільно надходять, запит може тривати нескінченно довго.
— Крістофер Піклай,

Це справедливо, що я насправді мало говорив про те, як це працює - я сподіваюсь, зараз додав цю інформацію, дякую!
— JosephH


26

Ви можете встановити інтервал очікування через метод requestSerializer setTimeoutInterval. Ви можете отримати requestSerializer з екземпляра AFHTTPRequestOperationManager.

Наприклад, щоб зробити запит на публікацію з таймаутом 25 секунд:

    NSDictionary *params = @{@"par1": @"value1",
                         @"par2": @"value2"};

    AFHTTPRequestOperationManager *manager = [AFHTTPRequestOperationManager manager];

    [manager.requestSerializer setTimeoutInterval:25];  //Time out after 25 seconds

    [manager POST:@"URL" parameters:params success:^(AFHTTPRequestOperation *operation, id responseObject) {

    //Success call back bock
    NSLog(@"Request completed with response: %@", responseObject);


    } failure:^(AFHTTPRequestOperation *operation, NSError *error) {
     //Failure callback block. This block may be called due to time out or any other failure reason
    }];

7

На мою думку, наразі вам доведеться це виправити вручну.

Я підкласую AFHTTPClient і змінив файл

- (NSMutableURLRequest *)requestWithMethod:(NSString *)method path:(NSString *)path parameters:(NSDictionary *)parameters

метод додаванням

[request setTimeoutInterval:10.0];

у AFHTTPClient.m, рядок 236. Звичайно, було б непогано, якби це можна було налаштувати, але, наскільки я бачу, це наразі неможливо.


7
Інша річ, яку слід врахувати, - це те, що Apple замінює час очікування для POST. Автоматично щось приблизно 4 хв. Я думаю, і ти НЕ МОЖЕШ змінити це.
— kcharwood

Я теж це бачу. Чому це так? Недобре змусити користувача чекати 4 хвилини, перш ніж з’єднання вийде з ладу.
— slatvick

2
Щоб додати до відповіді @KCHarwood. Станом на iOS 6 apple не замінює час очікування публікації. Це виправлено в iOS 6.
— АДАМ,

7

Нарешті з’ясували, як це зробити за допомогою асинхронного запиту POST:

- (void)timeout:(NSDictionary*)dict {
    NDLog(@"timeout");
    AFHTTPRequestOperation *operation = [dict objectForKey:@"operation"];
    if (operation) {
        [operation cancel];
    }
    [[AFNetworkActivityIndicatorManager sharedManager] decrementActivityCount];
    [self perform:[[dict objectForKey:@"selector"] pointerValue] on:[dict objectForKey:@"object"] with:nil];
}

- (void)perform:(SEL)selector on:(id)target with:(id)object {
    if (target && [target respondsToSelector:selector]) {
        [target performSelector:selector withObject:object];
    }
}

- (void)doStuffAndNotifyObject:(id)object withSelector:(SEL)selector {
    // AFHTTPRequestOperation asynchronous with selector                
    NSDictionary *params = [NSDictionary dictionaryWithObjectsAndKeys:
                            @"doStuff", @"task",
                            nil];

    AFHTTPClient *httpClient = [[AFHTTPClient alloc] initWithBaseURL:[NSURL URLWithString:baseURL]];

    NSMutableURLRequest *request = [httpClient requestWithMethod:@"POST" path:requestURL parameters:params];
    [httpClient release];

    AFHTTPRequestOperation *operation = [[[AFHTTPRequestOperation alloc] initWithRequest:request] autorelease];

    NSDictionary *dict = [NSDictionary dictionaryWithObjectsAndKeys:
                          operation, @"operation", 
                          object, @"object", 
                          [NSValue valueWithPointer:selector], @"selector", 
                          nil];
    [self performSelector:@selector(timeout:) withObject:dict afterDelay:timeout];

    [operation setCompletionBlockWithSuccess:^(AFHTTPRequestOperation *operation, id responseObject) {            
        [NSObject cancelPreviousPerformRequestsWithTarget:self selector:@selector(timeout:) object:dict];
        [[AFNetworkActivityIndicatorManager sharedManager] decrementActivityCount];
        [self perform:selector on:object with:[operation responseString]];
    }
    failure:^(AFHTTPRequestOperation *operation, NSError *error) {
        NDLog(@"fail! \nerror: %@", [error localizedDescription]);
        [NSObject cancelPreviousPerformRequestsWithTarget:self selector:@selector(timeout:) object:dict];
        [[AFNetworkActivityIndicatorManager sharedManager] decrementActivityCount];
        [self perform:selector on:object with:nil];
    }];

    NSOperationQueue *queue = [[[NSOperationQueue alloc] init] autorelease];
    [[AFNetworkActivityIndicatorManager sharedManager] incrementActivityCount];
    [queue addOperation:operation];
}

Я протестував цей код, дозволивши своєму серверу sleep(aFewSeconds).

Якщо вам потрібно зробити синхронний запит POST, НЕ використовуйте [queue waitUntilAllOperationsAreFinished];. Замість цього використовуйте той самий підхід, що і для асинхронного запиту, і зачекайте, доки буде запущена функція, яку ви передаєте в аргументі селектора.


18
Ні ні, ні, ні, будь ласка , не використовуйте це в реальному додатку. Насправді ваш код - це скасування запиту через проміжок часу, який починається під час створення операції, а не під час її запуску . Це може спричинити скасування запитів ще до початку.
— mattt

5
@mattt Будь ласка, надайте зразок коду, який працює. Насправді те, що ви описуєте, - саме те, що я хочу, щоб сталося: я хочу, щоб інтервал часу почав тикати саме в той момент, коли я створюю операцію.
— borisdiakur

Дякую Lego! Я використовую AFNetworking, і випадково близько 10% часу мої операції з AFNetworking ніколи не викликають блоки успіху чи відмови [сервер знаходиться в США, тестовий користувач знаходиться в Китаї]. Це велика проблема для мене, оскільки я навмисно блокую частини інтерфейсу, поки ці запити виконуються, щоб запобігти користувачеві надсилати занадто багато запитів одночасно. Врешті-решт я реалізував версію бази на цьому рішенні, яка передає блок завершення як параметр через perforselector withdelay і гарантує, що блок буде виконаний, якщо! Operation.isFinished - Матт: Дякуємо за AFNetworking!
— Кори

5

На основі відповідей інших людей та пропозицій @ mattt щодо пов'язаних з цим питань проекту, ось швидкий випуск, якщо ви підкласуєте AFHTTPClient:

@implementation SomeAPIClient // subclass of AFHTTPClient

// ...

- (NSMutableURLRequest *)requestWithMethod:(NSString *)method path:(NSString *)path parameters:(NSDictionary *)parameters {
  NSMutableURLRequest *request = [super requestWithMethod:method path:path parameters:parameters];
  [request setTimeoutInterval:120];
  return request;
}

- (NSMutableURLRequest *)multipartFormRequestWithMethod:(NSString *)method path:(NSString *)path parameters:(NSDictionary *)parameters constructingBodyWithBlock:(void (^)(id <AFMultipartFormData> formData))block {
  NSMutableURLRequest *request = [super requestWithMethod:method path:path parameters:parameters];
  [request setTimeoutInterval:120];
  return request;
}

@end

Перевірено для роботи на iOS 6.


0

Чи не можемо ми зробити це за допомогою такого таймера:

У файлі .h

{
NSInteger time;
AFJSONRequestOperation *operation;
}

У файлі .m

-(void)AFNetworkingmethod{

    time = 0;

    NSTtimer *timer = [NSTimer scheduledTimerWithTimeInterval:1.0 target:self selector:@selector(startTimer:) userInfo:nil repeats:YES];
    [timer fire];


    operation = [AFJSONRequestOperation JSONRequestOperationWithRequest:request success:^(NSURLRequest *request, NSHTTPURLResponse *response, id JSON) {
        [self operationDidFinishLoading:JSON];
    } failure:^(NSURLRequest *request, NSHTTPURLResponse *response, NSError *error, id JSON) {
        [self operationDidFailWithError:error];
    }];
    [operation setJSONReadingOptions:NSJSONReadingMutableContainers];
    [operation start];
}

-(void)startTimer:(NSTimer *)someTimer{
    if (time == 15&&![operation isFinished]) {
        time = 0;
        [operation invalidate];
        [operation cancel];
        NSLog(@"Timeout");
        return;
    }
    ++time;
}

0

Тут є два різні значення у визначенні "тайм-аут".

Час очікування як у timeoutInterval

Ви хочете скинути запит, коли він перебуває в режимі очікування (більше не передається) довше, ніж довільний проміжок часу. Приклад: ви встановили timeoutIntervalзначення 10 секунд, ви починаєте запит о 12:00:00, він може передавати деякі дані до 12:00:23, тоді з’єднання буде очікувано на 12:00:33. Цей випадок охоплюється майже всіма відповідями тут (включаючи Джозефа Х., Мостафу Абдельєтеєфа, Корнеліуса та Гурпартапа Сінгха).

Час очікування як у timeoutDeadline

Ви хочете відхилити запит, коли він досягне довільного терміну, який відбудеться довільно пізніше. Приклад: deadlineу майбутньому ви встановите 10 секунд, ви починаєте запит о 12:00:00, він може спробувати передати деякі дані до 12:00:23, але час з’єднання закінчиться раніше о 12:00:10. Цю справу висвітлює борисдіакур.

Я хотів би показати, як застосувати цей термін у Swift (3 та 4) для AFNetworking 3.1.

let sessionManager = AFHTTPSessionManager(baseURL: baseURL)
let request = sessionManager.post(endPoint, parameters: parameters, progress: { ... }, success: { ... }, failure: { ... })
// timeout deadline at 10 seconds in the future
DispatchQueue.global().asyncAfter(deadline: .now() + 10.0) {
    request?.cancel()
}

І для прикладу, який можна перевірити, цей код повинен надрукувати "невдача" замість "успіх" через негайний тайм-аут у 0,0 секунди в майбутньому:

let sessionManager = AFHTTPSessionManager(baseURL: URL(string: "https://example.com"))
sessionManager.responseSerializer = AFHTTPResponseSerializer()
let request = sessionManager.get("/", parameters: nil, progress: nil, success: { _ in
    print("success")
}, failure: { _ in
    print("failure")
})
// timeout deadline at 0 seconds in the future
DispatchQueue.global().asyncAfter(deadline: .now() + 0.0) {
    request?.cancel()
}

-2

Погодьтеся з Меттом, не варто намагатися змінити інтервал тайм-ауту. Але вам також не слід покладатися на перевірку доступності, щоб визначити, чи плануєте ви встановити з'єднання, ви не знаєте, поки не спробуєте.

Як зазначено в документі Apple:

Як правило, ви не повинні використовувати короткі інтервали очікування, а натомість повинні надавати користувачеві простий спосіб скасувати тривалу операцію. Для отримання додаткової інформації прочитайте “Проектування для реальних мереж”.

Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.