Язык Objective-C входит в открытый набор компиляторов GNU GCC, однако использовать этот язык для программирования под платформу PC бессмысленно, и в поставке GCC он выполняет исключительно декоративные функции. Зато под платформу Mac он основной язык программирования. В этой статье мы рассмотрим несколько кодерских приемов для OS X с помощью Objective-C и сопутствующих библиотек.
Несмотря на наличие фреймворков (к примеру, Xamarin или тот же Java SE), позволяющих программировать под макось на привычных языках типа C# или Java, эти средства уходят в тень, когда заходит речь о нативном коде. Если мы хотим максимально заточить приложение под мак, воспользоваться всеми его возможностями — нам придется использовать Objective-C. Он настолько же нативен для Mac, насколько C++ для Windows или Linux. Обойдемся без истории, введения и обзора — мы уже писали об этом — и перейдем сразу к конкретным кодерским рецептам.
Использование памяти
Использование памяти — животрепещущий вопрос на любом компьютере и в любой операционной системе. И на маке он решается по-своему. В Objective-C есть три способа управления памятью:
- автоматический сборщик мусора;
- ручное управление памятью;
- автоматический счетчик ссылок.
Рассмотрим их вкратце.
Формально сборщик мусора — это добро, но, как мы знаем по другим его реализациям (в Java, в CLR), он обеспечивает далеко не самый оптимальный менеджмент памяти, отжирая при этом системные ресурсы. Поэтому в операционной системе iOS этот способ напрочь отсутствует, и после выхода OS X 10.8 (Mountain Lion) он был помечен, как не рекомендуемый к применению. Возможно, в будущих версиях маковской системы он будет совсем удален. Теперь забота о мусоре ложится на наши худые программистские плечи, и решается она так…
Во время создания объекта ссылка на него принимает значение 1. После этого, когда надо создать еще одну ссылку на объект, программист получает ее путем увеличения счетчика на 1 следующим кодом: [myObj retain];. Здесь мы отправляем сообщение retain нашему объекту myObj. В момент, когда программисту больше не нужна определенная ссылка на объект, ему надо вызвать [myObj release];. В результате вызова этого сообщения происходит уменьшение счетчика соответственно на 1. Когда количество ссылок становится равно 0, происходит автоматический вызов метода dealloc, иными словами — деструктора объекта. Деструктор наследуется от базового класса NSObject. Следовательно, он выполняет мало полезного, поэтому тебе надо переопределять его в каждом наследуемом классе для того, чтобы он производил очистку каждой добавленной переменной — члена класса. Впрочем, точно так же мы делаем, когда создаем классовую иерархию на C++: в каждом классе переопределяем деструктор, при этом в базовом классе объявляя его виртуальным. В Objective-C такого делать не надо. Обрати внимание: счетчик ссылок может увеличиваться не только посредством прямой посылки сообщений retain и release, но и с помощью других методов. Так, когда в массив добавляется объект (с помощью метода addObject класса NSMuttableArray), создается новая ссылка и, соответственно, увеличивается счетчик. И напротив, когда из массива удаляется данный объект (методом removeObjectAtIndex класса NSMuttableArray), счетчик уменьшается. Ввиду этого при ручном подсчете необходимо очень внимательно относиться к числу ссылок, так как при посылке сообщения release пустому объекту случится краш приложения.
Кроме самостоятельной посылки каждому объекту сообщения release, можно создать пул удаляемых объектов. Он представлен объектом класса NSAutoreleasePool. Помещенные в него ссылки очищаются тогда, когда достигается конец пула. Хочу подчеркнуть: в пуле хранятся только ссылки, но не сами объекты, поэтому при достижении конца пула для каждой входящей в него ссылки вызывается метод release. Чтобы поместить ссылку на объект в пул, ей надо передать соответствующее сообщение: [myObj autorelease];.
Когда ты создаешь проект типа Foundation из темплейта, в автоматически генерируемом коде присутствует следующий кусок:
@autoreleasepool {
...
}
Он представляет собой не что иное, как пул автоматически удаляемых объектов, и все созданные внутри его ссылки на объекты будут реализованы на его исходе, из чего может следовать удаление или уменьшение счетчика объектов. Но вовсе не все объекты автоматически добавляются в пул: те объекты, которые создаются с помощью методов alloc, copy, mutableCopy, new, не могут быть автоматом добавлены в пул, и программисту придется самому следить за их состоянием и подсчетом ссылок. Тем не менее с помощью посылки сообщения autorelease этот объект можно поместить в пул автоудаляемых объектов. Все это напоминает управление памятью в C++, когда мы создаем объекты в кадре функции, по завершении которой ее стек очищается, и, с другой стороны, когда мы создаем объекты в куче с помощью оператора new и они остаются там, пока мы их принудительно не удалим оператором delete.
Автоматический подсчет ссылок (Automatic Reference Counting — ARC) появился в XCode 4.2, это рекомендуемый механизм управления памятью. ARC позволяет избежать утечек памяти, связанных с ручным подсчетом ссылок. Компилятор создает код, который корректно выделяет и очищает память, занимаемую объектами. При работе с ARC существует два типа ссылок: сильные и слабые. Все создаваемые ссылки по умолчанию сильные, но есть возможность явно это указать ключевым словом: __strong. Итак, в чем же преимущество сильных? Они позволяют избежать утечек памяти путем автоматического удаления «висячих ссылок». Посмотрим такой пример. Пускай имеется класс Chip, у которого создано два экземпляра:
Chip *c1 = [[Chip alloc] init];
Chip *c2 = [[Chip alloc] init];
Теперь мы хотим, чтобы c2 указывал на c1, то есть c2 = c1;. Без использования ARC и сильных ссылок здесь нас ожидает утечка памяти, поскольку область памяти, на которую указывала ссылка c2, стала бесхозной. Чтобы избежать подобной утечки, перед присваиванием надо реализовать ссылку c2: [c2 release];. Именно это «за кулисами» делает ARC при использовании сильных ссылок.
Но если они настолько хороши, зачем тогда нужны слабые ссылки? Представь такой случай: есть два класса, объект 1-го класса содержит ссылку на объект 2-го класса. Таким образом, мы получили циклическую ссылку, что не есть хорошо, так как при удалении объекта 2-го класса объект 1-го будет указывать «на ничто», а это, как мы знаем, грозит крашем приложения при обращении по такой ссылке. В лучшем случае при использовании сильных ссылок объект 2-го класса не будет удален. И вот здесь на помощь приходят слабые ссылки, которые объявляются посредством ключевого слова __weak. Теперь, если ссылку в 1-м классе мы сделаем слабой, а затем удалим 2-й объект, на который она ссылается, то последняя примет значение nil, а обращение по ссылке с этим значением не приведет ни к чему плохому.
Xcode: создание исполняемого файла
Если ты пришел на мак из Windows, то создание исполняемого файла в Xcode по сравнению с Visual Studio может вызвать некоторые затруднения. Чтобы его создать, в маковской среде недостаточно стандартной компиляции и построения приложения. Надо перейти по меню: Product -> Archive, откроется окно органайзера архива.
В списке на первый момент находится одна запись, соответствующая единственной версии сборки. Нажми кнопку Distribute… Появится дополнительное диалоговое окно для создания дистрибутива, оставь в нем выбор по умолчанию: Save Built Product — и нажми Next. Тебе будет предложено задать папку дистрибутива, Save. В результате будет создан подкаталог, содержащий исполняемый файл твоего приложения.
Файлы в шоколаде
Как тебе известно, основной фреймворк в OS X — это Cocoa. Cocoa не только содержит средства для построения и вывода пользовательского интерфейса, но и включает все остальные классы для программирования под OS X. В том числе классы для манипуляции файлами, массивами, строками и так далее. Естественно, Cocoa разделен на части. И все фундаментальные средства для работы с системой, в том числе перечисленные выше, содержатся в части фреймворка под названием Foundation и располагаются в соответствующем заголовочном файле Foundation.h. В качестве примера мы разработаем консольное приложение, отказавшись от красивого GUI, чтобы сконцентрироваться непосредственно на работе с файлами.
В принципе, OS X предоставляет тот же набор средств для работы с файловой системой, как и любой другой современный системный API. Тем не менее в связи с использованием другого языка и операционной системы правила применения различаются в сравнении с тем же Win32 и/или Posix. Итак, давай начнем разрабатывать приложение, которое работает с файлами в текущем каталоге проекта, и по ходу написания разберем файловые функции и их детали. Создай новый проект Command Line для OS X типа Foundation (проект filesWork). После создания проекта для того, чтобы работать с файлами из текущей директории, надо указать в Xcode соответствующую папку. Это делается в меню редактирования схемы: Product -> Scheme -> Edit Scheme.
В открывшемся окне в списке слева выбери пункт Run <имя приложения>, затем в правой части, отметив флажок Use custom working directory, в строке ниже введи или выбери с помощью диалога текущее расположение проекта. Вначале создадим экспериментальный файл. Его можно создать не отходя от кассы, то есть из Xcode: File -> New -> File, в окне в левом списке выбираем Other, справа Empty, щелкаем Next, вводим желаемое имя — file, нажимаем Create.
В открывшийся файл введи любые тестовые значения, сохрани. Над ним мы будем экспериментировать — множить, переименовывать, удалять. Между тем для начала в коде необходимо создать файловый менеджер — объект класса NSFileManager, с помощью которого осуществляются все операции над файлами и папками. Открыв в Xcode файл main.m, после открывающей пул autorelease фигурной скобки, объявив три переменные (см. исходник 1 к статье на сайте), напиши: fm = [NSFileManager defaultManager];. Переменные, о которых шла речь выше, — это: 1-я — для хранения имени открываемого файла, 2-я — сам файловый менеджер, 3-я — словарь файловых атрибутов (объект класса NSDictionary). Затем проверяем данный файл на существование:
if ([fm fileExistsAtPath: fName] == NO) {
Сообщение fileExistsAtPath вызывается для файлового менеджера, получает в качестве параметра путь/имя к файлу, наличие которого необходимо проверить. И в случае его присутствия возвращает YES, в ином случае — NO, и мы выводим на консоль соответствующее сообщение. Выяснив, что файл имеется в заданном каталоге, мы копируем его командой copyItemAtPath:
[fm copyItemAtPath: fName toPath: @"newfile" error: NULL];
Метод принимает три параметра: копируемый файл, цель копирования и указатель на объект класса NSError, в который в случае ошибки записывается подробная информация о ней. Если вместо этого объекта передать NULL, как в нашем примере, то сработает стандартная реакция: при успешном выполнении метода он вернет YES, иначе — NO. После этого сравниваем содержимое файла-источника и файла-приемника, воспользовавшись командой contentsEqualAtPath:
[fm contentsEqualAtPath: fName andPath: @"newfile"];
Если оно одинаковое, тогда продолжаем выполнение и переименовываем копию. Для переименования, как тебе известно по опыту работы с другими API, используется функция перемещения, только директория-источник и приемник должны быть одинаковыми:
[fm moveItemAtPath: @"newfile" toPath: @"newfile2" error: NULL];
Следующим действием узнаем размер скопированного и впоследствии переименованного файла и выведем это значение на консоль. Определяем размер файла в два этапа: сначала мы получаем все атрибуты файла в ранее объявленную переменную — словарь, затем изымаем из нее только поле, содержащее размер файла:
attr = [fm attributesOfItemAtPath: @"newfile2" error: NULL];
NSLog(@"Размер файла составляет %llu байт", [[attr objectForKey: NSFileSize] unsignedLongLongValue]);
Теперь удалим оригинальный файл. Это, как всегда, просто — ломать не строить :). Вызываем метод removeItemAtPath файлового менеджера и передаем ему имя удаляемого файла:
[fm removeItemAtPath: fName error: NULL]
Под конец приложения выведем содержимое созданного файла. Для этого методу stringWithContentsOfFile класса NSString первым параметром передаем имя файла, содержимое которого надо вывести, вторым — нужную кодировку, в которой будет осуществлен вывод, третьим — NULL:
NSLog(@"%@", [NSString stringWithContentsOfFile: @"newfile2" encoding:NSUTF8StringEncoding error:NULL]);
В ожидающем твоего внимания примере каждое действие сопровождается проверкой, по которой в случае краша можно легко вычислить, на каком действии падает программа.
Если создать исполняемый файл (как это сделать — смотри во врезке), поместить в рабочую папку наш тестовый file, запустить приложение на выполнение, то оно выведет содержимое картинки «Вывод приложения в терминале» (в зависимости от содержимого файла).
Вдобавок для работы с директориями существует несколько методов: currentDirectoryPath возвращает текущую для программы папку, changeCurrentDirectoryPath изменяет текущую директорию, copyItemAtPath копирует структуру директории, createDirectoryAtPath создает новую папку, NSHomeDirectory позволяет получить путь к домашней директории текущего пользователя, а также другие. К примеру, первая из этого списка функция использована в приведенной программе, в итоге в начале выполнения программа выводит свою домашнюю директорию на консоль.
Модификация файлов
Часто появляется необходимость работы с «сырыми» данными внутри файлов. В таком случае надо читать/записывать/обрабатывать не обязательно текстовые данные, поэтому хранить их в строках (NSString) не получится. В Cocoa нас выручит класс NSData. Попросту говоря, объект данного класса представляет собой буфер для временного хранения данных. Например, можно скопировать файл побайтно, не пользуясь при этом системной функцией. Прочитать данные из файла можно, воспользовавшись следующей строчкой:
NSData *fileData = [fm contentsAtPath: @"file"];
То есть в объявленный буфер класса NSData fileData мы помещаем содержимое файла file, имя которого передается методу в параметре. Сохраняем данные в другом файле методом createFileAtPath, ему передаются три параметра: имя нового файла, содержимое — данные из буфера fileData и атрибуты — nil:
[fm createFileAtPath: @"newfile3" contents: fileData attributes: nil];
Этим возможности класса NSData не ограничиваются. Вместе с классом NSFileHandle он приобретает дополнительные способности: объект последнего, являясь указателем на источник и/или приемник, позволяет осуществить гибкие манипуляции над данными, такие как считывание или запись в произвольную позицию файла. Класс NSFileHandle содержит следующие методы (перечень далеко не полный): fileHandleForReadingAtPath — открывает файл для чтения, fileHandleForWritingAtPath — открывает файл для записи, fileHandleForUpdatingAtPath — для обновления (чтение + запись), seekToFileOffset — перемещается на указанное смещение. Разберемся с методами класса на конкретном примере. Пускай программа читает наш старый файл и если файла для вывода не существует, то создает его и пишет в него содержимое первого файла, а в случае наличия файла для вывода дополняет его содержимым прочитанного файла. Таким образом, при каждом последующем запуске программы файл вывода будет расти.
Итак, создай новый проект. Для получения полного листинга смотри исходник (проект fileMod), я буду приводить краткие комментарии. Вначале объявляем два файловых хэндла (объекты класса NSFileHandle): на читаемый и записываемый, также объявляем буфер данных и создаем файловый менеджер. Открываем файл file для чтения:
inFile = [NSFileHandle fileHandleForReadingAtPath: @"file"];
Затем проверяем, существует ли файл для вывода, если нет, то создаем его. Далее открываем этот файл для записи, получая его хэндл:
outFile = [NSFileHandle fileHandleForWritingAtPath: @"fileout"];
Следующим действием переводим текущую позицию файла в его конец:
[outFile seekToEndOfFile];
Если в нем содержатся данные, то последующая запись будет осуществлена после имеющейся инфы, если же файл пуст (имеет нулевой размер), тогда ничего не произойдет. Потом читаем из входного файла данные в буфер — объект класса NSData и пишем их в файл вывода:
buffer = [inFile readDataToEndOfFile];
[outFile writeData: buffer];
Закрываем оба файла и последним действием выводим на консоль все содержимое файла вывода.
Советую обратить внимание на класс NSFileHandle, поскольку он содержит широкие возможности управления файловыми потоками. Кроме того, объекты этого класса используются для ввода/вывода информации в сокеты и устройства.
Шоколадная сеть
На нижнем уровне сеть в OS X устроена так же, как и в других операционных системах, — посредством Berkley Sockets API. Однако в прикладном API, таком как Cocoa, имеются различия для реализации поддержки Obj-C и внесения удобства работы с сокетами. К счастью, чтобы разобраться в работе с сокетами в Cocoa, нам потребуется совсем немного времени. Существует только три класса, используемых для поддержки сетевого взаимодействия: NSURL, NSURLRequest, NSURLConnection, а также их модифицируемые аналоги с ключевым словом Mutable, присутствующим в названии.
Объекты первого класса списка, как можно догадаться, представляют локаторы — как удаленных, так и локальных ресурсов. Для создания объекта используется методURLWithString: NSURL* myURL = [NSURL URLWithString: @"yazevsoft.blogspot.com"];
. Класс содержит методы для получения любой части URL и для создания относительных адресов. Создание пути к локальному файлу осуществляется с помощью метода fileUrlWithPath, например, так: NSURL* myFile = [NSURL fileURLWithPath:@"/Applications/"];
.
Экземпляры класса NSURLRequest определяют способ доступа к объекту, на который указывает NSURL. Создание происходит с помощью метода requestWithURL, которому передается инициализированный объект NSURL. С помощью объекта рассматриваемого класса можно определить вид доступа по протоколу HTTP (GET, POST, PUT), задать время задержки перед ответом и другое.
Экземпляры третьего класса списка представляют непосредственно соединение. Для создания асинхронного соединения используется метод sendAsynchronousRequest.
Разработаем сетевое приложение с оконным пользовательским интерфейсом, в процессе чего рассмотрим все три шага установки соединения. Наша прога будет просто скачивать изображение из свободного ресурса в Сети (из моего блога). Создай новое Cocoa Application и задай параметры (проект imageLoader). Открой файл <...>Document.h, где <...> — имя префикса для класса, заданное в мастере генерации проекта (в моем случае — App), здесь в объявлении интерфейса AppDocument в фигурных скобках добавь описание аутлета imageView класса NSImageView, с помощью которого наша программа будет взаимодействовать с визуальным компонентом этого же класса:
IBOutlet NSImageView *imageView;
Теперь создадим интерфейс, состоящий из лежащего на поверхности формы изображения. Сначала открой файл AppDocument.xib, из палитры компонентов перенеси на форму компонент Image Well (объект класса NSImageView). Растяни его пошире. Свяжем компонент с аутлетом, чтобы получить управление над первым из кода. Для этого, удерживая клавишу Ctrl на клаве, от объекта File’s Owner перетащи связывающую синюю линию на объект Image Well. В результате появится меню Outlets, в котором будет только один пункт imageView — объявленная нами переменная, щелкни на этом пункте. Аутлет связан. Кстати, эти действия удобно совершать во второй слева панели окна редактирования xib-файла. На следующем шаге добавим код для загрузки изображения в компонент. Открой файл AppDocument.m и дополни функцию
- (void)windowControllerDidLoadNib:(NSWindowController *)aController
следующим кодом:
NSString* urlString = [NSString stringWithFormat:@"http://1.bp.blogspot.com/--8pJeTHLC2g/Un8Uc373peI/AAAAAAAACMY/SWBK5HDP-fU/s1600/ProjectGenom+2013-11-10+09-45-43-15_1.png"];
NSURL* url = [NSURL URLWithString:urlString];
NSURLRequest* request = [NSURLRequest requestWithURL:url];
[NSURLConnection sendAsynchronousRequest:request
queue:[NSOperationQueue mainQueue]
completionHandler:^(NSURLResponse * response,
SData * data,
NSError * error) {
NSImage* image = [[NSImage alloc] initWithData:data];
imageView.image = image;
}];
Судя по комментарию, оставленному в функции при генерации проекта, наш код выполнится сразу после создания окна. Итак, первым делом в нем объявляется и инициализируется строка-ссылка (прости за столь абсурдную абракадабру), затем на ее основе создается локатор для удаленной картинки, после этого на базе локатора формируется запрос — объект класса NSURLRequest. Далее с помощью метода sendAsynchronousRequest объекта класса NSURLConnection мы создаем соединение с указанным в NSURL сетевым узлом. Этому методу мы передаем объект класса NSURLRequest, создаем очередь операций (объект класса NSOperationQueue), а последним параметром передаем блок кода, который получит управление в момент, когда данные (изображение) будут полностью загружены. Внутри блока кода полученные данные преобразуются в объект класса NSImage, а затем этот объект выводится в компонент imageView через присвоение содержимого свойству image компонента. Более подробную информацию о блоках кода (что они собой представляют и с чем их едят) ты можешь узнать из статьи прошлого номера ][.
Пришло время протестировать нашу прогу: откомпиль и построй приложение, если в коде нет ошибок и ссылка введена правильно, то в окне приложения должен загрузиться скриншот из нашей игры Project Genom.
Заключение и планы на будущее
Сегодня мы затронули три большие темы: управление памятью в OS X, работу с файлами и сетевое взаимодействие с помощью Cocoa. Тем не менее за бортом осталась масса важных и интересных тем, которые мы еще рассмотрим ближайших номерах — ведь интерес к платформам от Apple только растет. Желаю удачи, и до встречи на страницах ][.