Текст книги "iOS. Приемы программирования"
Автор книги: Вандад Нахавандипур
Жанр: Зарубежная компьютерная литература, Зарубежная литература
сообщить о неприемлемом содержимом
Текущая страница: 28 (всего у книги 59 страниц)
7.11. Синхронное выполнение задач с помощью операций
Постановка задачиНеобходимо синхронно выполнить серию задач.
РешениеСоздавайте операции и запускайте их вручную:
@interface AppDelegate ()
@property (nonatomic, strong) NSInvocationOperation *simpleOperation;
@end
Реализация делегата приложения такова:
– (void) simpleOperationEntry:(id)paramObject{
NSLog(@"Parameter Object = %@", paramObject);
NSLog(@"Main Thread = %@", [NSThread mainThread]);
NSLog(@"Current Thread = %@", [NSThread currentThread]);
}
– (BOOL) application:(UIApplication *)application
didFinishLaunchingWithOptions:(NSDictionary *)launchOptions{
NSNumber *simpleObject = [NSNumber numberWithInteger:123];
self.simpleOperation = [[NSInvocationOperation alloc]
initWithTarget: self
selector:@selector(simpleOperationEntry:)
object: simpleObject];
[self.simpleOperation start];
self.window = [[UIWindow alloc] initWithFrame:
[[UIScreen mainScreen] bounds]];
self.window.backgroundColor = [UIColor whiteColor];
[self.window makeKeyAndVisible];
return YES;
}
Вывод этой программы (в окне консоли) будет примерно таким:
Parameter Object = 123
Main Thread = <NSThread: 0x68 10280>{name = (null), num = 1}
Current Thread = <NSThread: 0x68 10280>{name = (null), num = 1}
Из имени данного класса (NSInvocationOperation) понятно[6]6
Invocation (англ.) – «активизация». – Примеч. пер.
[Закрыть], что основное применение объекта такого типа связано с активизацией метода в объекте. Это наиболее непосредственный способ активизации метода в объекте с помощью операций.
Операция активизации, как объяснялось в разделе 7.0, позволяет активизировать метод в объекте. «Что же в этом особенного?» – спросите вы. Потенциал активизирующей операции можно продемонстрировать, когда такая операция добавляется в операционную очередь. Примененная вместе с операционной очередью, активизирующая операция может асинхронно запустить метод в заданном объекте параллельно тому потоку, который начал операцию. Внимательно рассмотрев вывод с консоли (приведенный в подразделе «Решение» данного раздела), вы заметите, что актуальный поток в методе, запущенный активизирующей операцией, равен главному потоку. Действительно, главный поток в методе application: didFinishLaunchingWithOptions: запускает операцию, пользуясь ее методом start. В разделе 7.12 мы научимся эффективно использовать операционные очереди для асинхронного выполнения задач.
Кроме активизирующих операций, вы можете применять блоковые или обычные операции для синхронного выполнения задач. Вот пример использования блоковой операции для подсчета чисел от 0 до 999 (это происходит в. h-файле делегата приложения):
@interface AppDelegate ()
@property (nonatomic, strong) NSBlockOperation *simpleOperation;
@end
@implementation AppDelegate
А вот реализация делегата приложения (.m-файл):
– (BOOL) application:(UIApplication *)application
didFinishLaunchingWithOptions:(NSDictionary *)launchOptions{
self.simpleOperation = [NSBlockOperation blockOperationWithBlock: ^{
NSLog(@"Main Thread = %@", [NSThread mainThread]);
NSLog(@"Current Thread = %@", [NSThread currentThread]);
NSUInteger counter = 0;
for (counter = 0;
counter < 1000;
counter++){
NSLog(@"Count = %lu", (unsigned long)counter);
}
}];
/* Запуск операции. */
[self.simpleOperation start];
/* Выводим что-нибудь на консоль, просто чтобы проверить,
должны мы дожидаться, пока выполнится блок кода, или нет.*/
NSLog(@"Main thread is here");
self.window = [[UIWindow alloc] initWithFrame:
[[UIScreen mainScreen] bounds]];
self.window.backgroundColor = [UIColor whiteColor];
[self.window makeKeyAndVisible];
return YES;
}
Если запустить приложение, мы увидим, что на экране выводятся значения от 0 до 999, а за ними следует сообщение Main thread is here (Это главный поток):
Main Thread = <NSThread: 0x68 10280>{name = (null), num = 1}
Current Thread = <NSThread: 0x68 10280>{name = (null), num = 1}
…
Count = 991
Count = 992
Count = 993
Count = 994
Count = 995
Count = 996
Count = 997
Count = 998
Count = 999
Main thread is here
Итак, убеждаемся, что, поскольку блоковая операция была запущена в методе application: didFinishLaunchingWithOptions:, который сам работает в главном потоке, код внутри блока также выполняется в главном потоке. Основные сведения, которые мы получаем из этих регистрационных записей (логов), сводятся к следующему: операция блокировала главный поток, и потребовалось вернуться к выполнению кода основного потока после того, как была завершена работа блоковой операции. Это образец очень непрофессионального программирования. На самом деле программисты, работающие с iOS, должны идти на любые уловки и пользоваться любыми известными им приемами, чтобы обеспечивать отклик основного потока в любой момент и чтобы этот поток мог заниматься своим основным делом – обработкой пользовательского ввода. Вот что об этом пишет Apple.
«Необходимо внимательно отслеживать, какие задачи вы решаете в главном потоке вашего приложения. Именно в главном потоке ваша программа отрабатывает события касания и другой пользовательский ввод. Чтобы гарантировать, что приложение в любой момент будет откликаться на действия пользователя, никогда не следует загружать главный поток выполнением долговременных задач либо выполнением задач с потенциально неопределенным концом. Таковы, в частности, задачи, связанные с доступом к сети. Напротив, подобные задачи следует решать в фоновых потоках. Оптимальный способ решения таких задач – заключение их в объект операции и добавление этого объекта к операционной очереди. Но вы можете и сами создавать потоки вручную».
Подробнее эта тема рассматривается в документе Performance Tuning («Повышение производительности») в справочной библиотеке iOS. Документ расположен по адресу https://developer.apple.com/library/ios/documentation/iphone/conceptual/iphoneosprogrammingguide/PerformanceTuning/PerformanceTuning.html#//apple_ref/doc/uid/TP400 07072-CH8-SW1.
Кроме вызовов и блоковых операций, вы можете также создавать подклассы от NSOperation и выполнять вашу задачу в этом классе. Перед тем как переходить к работе, обратите внимание на некоторые нюансы, связанные с созданием подклассов от NSOperation.
Если вы не планируете пользоваться операционной очередью, то необходимо открепить от вашего потока новый поток. Это делается в методе start, относящемся к операции. Если вы не хотите пользоваться операционной очередью, а также не собираетесь выполнять данную операцию асинхронно с другими операциями, запускаемыми вручную, то можно просто вызвать метод main операции в методе start.
В реализации операции необходимо переопределить два важных метода экземпляра NSOperation – методы isExecuting и isFinished. Их может вызывать любой другой объект. В этих методах необходимо возвращать потоковобезопасное значение, которым можно будет управлять прямо из операции. Как только операция начинается, она должна посредством механизма «уведомления наблюдателей об изменениях в свойствах наблюдаемого объекта» (KVO) сообщать всем слушателям о том, что вы изменяете возвращаемые значения для двух этих методов. В примере с кодом мы рассмотрим, как это происходит на практике.
В методе main, относящемся к операции, необходимо создать собственный автоматически высвобождаемый пул на случай, если когда-нибудь в будущем операция будет добавлена в операционную очередь. Необходимо убедиться в том, что операции можно задействовать обоими способами – как при запуске вручную, так и при запуске в рамках операционной очереди.
У вас должен быть метод-инициализатор для ваших операций. Это обязательно должен быть специальный метод, выделенный под конкретную операцию. Все остальные методы-инициализаторы, в том числе применяемый по умолчанию метод init, должны вызывать вышеупомянутый специальный инициализатор, который содержит наибольшее количество параметров. Другие методы-инициализаторы должны гарантировать, что они передают подходящие параметры методу-инициализатору (если вообще передают).
Вот объявление объекта операции (.h-файл):
#import <Foundation/Foundation.h>
@interface CountingOperation: NSOperation
/* Выделенный инициализатор */
– (id) initWithStartingCount:(NSUInteger)paramStartingCount
endingCount:(NSUInteger)paramEndingCount;
@end
Реализация операции (записываемая в. m-файле) несколько длинновата, но, надеюсь, вполне понятна:
#import «CountingOperation.h»
@implementation CountingOperation
@property (nonatomic, unsafe_unretained) NSUInteger startingCount;
@property (nonatomic, unsafe_unretained) NSUInteger endingCount;
@property (nonatomic, unsafe_unretained, getter=isFinished) BOOL finished;
@property (nonatomic, unsafe_unretained, getter=isExecuting) BOOL executing;
@end
@implementation CountingOperation
– (instancetype) init {
return([self initWithStartingCount:0
endingCount:1000]);
}
– (instancetype) initWithStartingCount:(NSUInteger)paramStartingCount
endingCount:(NSUInteger)paramEndingCount{
self = [super init];
if (self!= nil){
/* Сохраните эти значения для главного метода. */
startingCount = paramStartingCount;
endingCount = paramEndingCount;
}
return(self);
}
– (void) main {
@try {
/* Это автоматически высвобождаемый пул. */
@autoreleasepool {
/* Сохраняем здесь локальную переменную, которая
должна быть установлена в YES всякий раз, когда
мы завершаем выполнение задачи. */
BOOL taskIsFinished = NO;
/* Создаем здесь цикл while, существующий лишь в том случае,
когда переменная taskIsFinished устанавливается в YES
или операция отменяется. */
while (taskIsFinished == NO &&
[self isCancelled] == NO){
/* Здесь выполняется задача. */
NSLog(@"Main Thread = %@", [NSThread mainThread]);
NSLog(@"Current Thread = %@", [NSThread currentThread]);
NSUInteger counter = startingCount;
for (counter = startingCount;
counter < endingCount;
counter++){
NSLog(@"Count = %lu", (unsigned long)counter);
}
/* Очень важно. Здесь мы можем выйти из цикла, по-прежнему
соблюдая правила, по которым отменяются операции. */
taskIsFinished = YES;
}
/* Соответствие KVO. Генерируем требуемые уведомления KVO. */
[self willChangeValueForKey:@"isFinished"];
[self willChangeValueForKey:@"isExecuting"];
finished = YES;
executing = NO;
[self didChangeValueForKey:@"isFinished"];
[self didChangeValueForKey:@"isExecuting"];
}
}
@catch (NSException * e) {
NSLog(@"Exception %@", e);
}
}
@end
Операцию можно начать так:
@interface AppDelegate ()
@property (nonatomic, strong) CountingOperation *simpleOperation;
@end
@implementation AppDelegate
– (BOOL) application:(UIApplication *)application
didFinishLaunchingWithOptions:(NSDictionary *)launchOptions{
self.simpleOperation = [[CountingOperation alloc] initWithStartingCount:0
endingCount:1000];
[self.simpleOperation start];
NSLog(@"Main thread is here");
self.window = [[UIWindow alloc] initWithFrame:
[[UIScreen mainScreen] bounds]];
self.window.backgroundColor = [UIColor whiteColor];
[self.window makeKeyAndVisible];
return YES;
}
@end
Запустив данный код, мы увидим в окне консоли следующие результаты, точно как при применении блоковой операции:
Main Thread = <NSThread: 0x68 10260>{name = (null), num = 1}
Current Thread = <NSThread: 0x68 10260>{name = (null), num = 1}
…
Count = 993
Count = 994
Count = 995
Count = 996
Count = 997
Count = 998
Count = 999
Main thread is here
Раздел 7.12.
7.12. Асинхронное выполнение задач с помощью операций
Постановка задачиТребуется параллельно выполнять операции.
РешениеВоспользуйтесь операционными очередями. В качестве альтернативы можно создавать подклассы от NSOperation и откреплять новый поток в методе main.
ОбсуждениеКак говорилось в разделе 7.11, операции по умолчанию работают в том потоке, который вызывает метод start. Обычно операции запускаются в основном потоке, но в то же время мы ожидаем, что операции будут выполняться в собственных потоках и, соответственно, не будут тратить процессорное время, уделяемое главному потоку. Наилучшим решением для обеспечения такой работы будет применение операционных очередей. Однако если вы хотите управлять своими операциями вручную, чего бы я не рекомендовал, то можно было бы создавать подклассы от NSOperation и откреплять новый поток в главном методе. Подробнее об открепленных потоках поговорим в разделе 7.15.
Идем дальше. Попробуем воспользоваться операционной очередью и добавим к ней две простые инициирующие операции (подробнее об инициирующих операциях рассказано в разделе 7.0). Дополнительные примеры кода, описывающие инициирующие операции, имеются в разделе 7.11. Вот объявление (.hm-файл) делегата приложения, в котором используются операционная очередь и две инициирующие операции:
@interface AppDelegate ()
@property (nonatomic, strong) NSOperationQueue *operationQueue;
@property (nonatomic, strong) NSInvocationOperation *firstOperation;
@property (nonatomic, strong) NSInvocationOperation *secondOperation;
@end
@implementation AppDelegate
А вот и внутренняя часть файла реализации делегата приложения:
– (void) firstOperationEntry:(id)paramObject{
NSLog(@"%s", __FUNCTION__);
NSLog(@"Parameter Object = %@", paramObject);
NSLog(@"Main Thread = %@", [NSThread mainThread]);
NSLog(@"Current Thread = %@", [NSThread currentThread]);
}
– (void) secondOperationEntry:(id)paramObject{
NSLog(@"%s", __FUNCTION__);
NSLog(@"Parameter Object = %@", paramObject);
NSLog(@"Main Thread = %@", [NSThread mainThread]);
NSLog(@"Current Thread = %@", [NSThread currentThread]);
}
– (BOOL) application:(UIApplication *)application
didFinishLaunchingWithOptions:(NSDictionary *)launchOptions{
NSNumber *firstNumber = @111;
NSNumber *secondNumber = @222;
self.firstOperation =[[NSInvocationOperation alloc]
initWithTarget: self
selector:@selector(firstOperationEntry:)
object: firstNumber];
self.secondOperation = [[NSInvocationOperation alloc]
initWithTarget: self
selector:@selector(secondOperationEntry:)
object: secondNumber];
self.operationQueue = [[NSOperationQueue alloc] init];
/* Добавляем операции в очередь. */
[self.operationQueue addOperation: self.firstOperation];
[self.operationQueue addOperation: self.secondOperation];
NSLog(@"Main thread is here");
self.window = [[UIWindow alloc] initWithFrame:
[[UIScreen mainScreen] bounds]];
self.window.backgroundColor = [UIColor whiteColor];
[self.window makeKeyAndVisible];
return YES;
}
Вот что происходит в реализации данного кода.
У нас есть два метода, firstOperationEntry: и secondOperationEntry:. Каждый из этих методов принимает в качестве параметра объект и выводит в окне консоли информацию об актуальном потоке, главном потоке и этом параметре. Это входные методы инициирующих операций, которые будут добавляться в операционную очередь.
Мы инициализируем два метода типа NSInvocationOperation и задаем целевой селектор в точке входа каждой операции (эти точки входа были описаны выше).
Затем инициализируем объект типа NSOperationQueue. (Он может создаваться и до того, как созданы методы входа.) Объект очереди будет обеспечивать параллелизм в работе операционных объектов. На данном этапе операционная очередь может немедленно начать (а может и не начать) запускать инициирующие операции, пользуясь их методами start. При этом очень важно помнить, что после добавления операции в операционную очередь от вас не требуется запускать операции вручную. Обеспечением запуска занимается операционная очередь.
Итак, еще раз запустим код примера и посмотрим, что же у нас на консоли:
[Running_Tasks_Asynchronously_with_OperationsAppDelegate firstOperationEntry: ]
Main thread is here
Parameter Object = 111
[Running_Tasks_Asynchronously_with_OperationsAppDelegate secondOperationEntry: ]
Main Thread = <NSThread: 0x68 10260>{name = (null), num = 1}
Parameter Object = 222
Current Thread = <NSThread: 0x6805c20>{name = (null), num = 3}
Main Thread = <NSThread: 0x68 10260>{name = (null), num = 1}
Current Thread = <NSThread: 0x6b2d1d0>{name = (null), num = 4}
Блестяще! Это доказывает, что инициирующие операции параллельно выполняются каждая в своем потоке и в то же время параллельно главному потоку, вообще не блокируя его. Теперь еще пару раз прогоним этот же код и посмотрим, какой вывод будет появляться в окне консоли. В таком случае вы можете получить совершенно иной результат, например:
Main thread is here
[Running_Tasks_Asynchronously_with_OperationsAppDelegate firstOperationEntry: ]
[Running_Tasks_Asynchronously_with_OperationsAppDelegate secondOperationEntry: ]
Parameter Object = 111
Main Thread = <NSThread: 0x68 10260>{name = (null), num = 1}
Current Thread = <NSThread: 0x68247c0>{name = (null), num = 3}
Parameter Object = 222
Main Thread = <NSThread: 0x68 10260>{name = (null), num = 1}
Current Thread = <NSThread: 0x6819b00>{name = (null), num = 4}
Очевидно, что главный поток не блокируется и что обе инициирующие операции работают параллельно с главным потоком. Это доказывает, что в операционной очереди сохраняется параллелизм даже тогда, когда в нее добавляются две непараллельные операции. Операционная очередь управляет потоками, необходимыми для осуществления операций.
Если бы вы создавали подклассы от NSOperation и добавляли в операционную очередь экземпляры нового класса, то ситуация складывалась бы несколько иначе. Не забывайте о некоторых моментах.
Если обычные операции, являющиеся подклассами от NSOperation, добавлять в операционную очередь, то они будут работать асинхронно. Поэтому необходимо переопределить метод экземпляра isConcurrent, относящийся к классу NSOperation, и возвратить значение YES.
Необходимо подготовить операцию к отмене, периодически проверяя значение метода isCancelled при осуществлении основной задачи операции, а также в методе start еще до запуска самой операции. В таком случае метод start вызывается операционной очередью после того, как операция будет добавлена в очередь. В этом методе проверяется, не отменена ли операция. Это делается с помощью метода isCancelled. Если операция отменена, просто верните такое значение от метода start. В противном случае вызовите метод main из метода start.
Переопределите метод main собственной реализацией основной задачи, которую должна выполнять операция. Обязательно выделите и инициализируйте в этом методе ваш собственный автоматически высвобождаемый пул и высвободите его непосредственно перед актом возврата.
Переопределите методы isFinished и isExecuting операции и верните соответствующие логические (BOOL) значения, показывающие, завершена операция или продолжается в настоящий момент.
Вот объявление операции (.h-файл):
#import <Foundation/Foundation.h>
@interface SimpleOperation: NSOperation
/* Выделенный инициализатор */
– (id) initWithObject:(NSObject *)paramObject;
@end
Реализация операции такова:
#import «SimpleOperation.h»
@implementation SimpleOperation
– (instancetype) init {
return([self initWithObject:@123]);
}
– (instancetype) initWithObject:(NSObject *)paramObject{
self = [super init];
if (self!= nil){
/* Сохраните эти значения для главного метода. */
_givenObject = paramObject;
}
return(self);
}
– (void) main {
@try {
@autoreleasepool {
/* Сохраняем здесь локальную переменную, которая должна быть
установлена в YES всякий раз, когда мы завершаем
выполнение задачи. */
BOOL taskIsFinished = NO;
/* Создаем здесь цикл while, существующий лишь в том случае,
когда переменная taskIsFinished устанавливается в YES
или операция отменяется. */
while (taskIsFinished == NO &&
[self isCancelled] == NO){
/* Здесь выполняется задача. */
NSLog(@"%s", __FUNCTION__);
NSLog(@"Parameter Object = %@", givenObject);
NSLog(@"Main Thread = %@", [NSThread mainThread]);
NSLog(@"Current Thread = %@", [NSThread currentThread]);
/* Очень важно. Здесь мы можем выйти из цикла, по-прежнему
соблюдая правила, по которым отменяются операции. */
taskIsFinished = YES;
}
/* Соответствие KVO. Генерируем требуемые уведомления KVO. */
[self willChangeValueForKey:@"isFinished"];
[self willChangeValueForKey:@"isExecuting"];
finished = YES;
executing = NO;
[self didChangeValueForKey:@"isFinished"];
[self didChangeValueForKey:@"isExecuting"];
}
}
@catch (NSException * e) {
NSLog(@"Exception %@", e);
}
}
– (BOOL) isConcurrent{
return YES;
}
@end
Теперь этот операционный класс можно использовать в любом другом классе, например в делегате вашего приложения. Вот объявление делегата приложения, в котором задействуется этот новый класс операции, добавляемый в операционную очередь:
@interface AppDelegate ()
@property (nonatomic, strong) NSOperationQueue *operationQueue;
@property (nonatomic, strong) SimpleOperation *firstOperation;
@property (nonatomic, strong) SimpleOperation *secondOperation;
@end
@implementation AppDelegate
Реализация делегата приложения такова:
– (BOOL) application:(UIApplication *)application
didFinishLaunchingWithOptions:(NSDictionary *)launchOptions{
NSNumber *firstNumber = @111;
NSNumber *secondNumber = @222;
self.firstOperation = [[SimpleOperation alloc]
initWithObject: firstNumber];
self.secondOperation = [[SimpleOperation alloc]
initWithObject: secondNumber];
self.operationQueue = [[NSOperationQueue alloc] init];
/* Добавляем операции в очередь. */
[self.operationQueue addOperation: self.firstOperation];
[self.operationQueue addOperation: self.secondOperation];
NSLog(@"Main thread is here");
self.window = [[UIWindow alloc] initWithFrame:
[[UIScreen mainScreen] bounds]];
self.window.backgroundColor = [UIColor whiteColor];
[self.window makeKeyAndVisible];
return YES;
}
В окне консоли отобразятся результаты, подобные уже виденным ранее – тем, которые мы получали при применении параллельных инициирующих операций:
Main thread is here
-[SimpleOperation main]
-[SimpleOperation main]
Parameter Object = 222
Parameter Object = 222
Main Thread = <NSThread: 0x68 10260>{name = (null), num = 1}
Main Thread = <NSThread: 0x68 10260>{name = (null), num = 1}
Current Thread = <NSThread: 0x6a10b90>{name = (null), num = 3}
Current Thread = <NSThread: 0x6a13f50>{name = (null), num = 4}
Правообладателям!
Это произведение, предположительно, находится в статусе 'public domain'. Если это не так и размещение материала нарушает чьи-либо права, то сообщите нам об этом.