Як встановити час очікування за допомогою 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.