Цель моей статьи — дать начальное представление читателю о том, как работать с памятью в Objective C, рассказать о работе с KeyChain и показать новую версию своего приложения для монтирования SSHFS, которая была написана всего за несколько дней (в сумме), но уже вполне может составлять конкуренцию громоздкому Macfusion.app, и которая работает без напильника и не пишет ваши пароли в открытом виде в системный лог.
Вообще, конечно, начиная с Mac OS X 10.5 управление памятью стало намного проще, поскольку добавилась поддержка сборки мусора (GC). Соответственно, можно её просто включить по умолчанию в вашем проекте и забыть об этом, как о страшном сне.
Тем не менее, если вы внимательно почитаете описание, то там будет написано следующее: «All Objective-C code linked or loaded by this application must also be GC capable». То есть, если вы используете сторонние фреймворки в закрытом виде, которые были скомпилированы без поддержки GC — придется либо от них отказаться, либо использовать более старые механизмы управления памятью, в виде retain/release. Также, поддержка GC отсутствует в iPhone, так что если вы решили написать своё приложение под iPhone OS, то в любом случае вам придется с этим столкнуться.
Я буду предполагать, что у вас все-таки какие-то начальные знания об Objective C есть, к примеру, вы уже читали этот топик.
В Objective C используется стандартный механизм подсчета ссылок: у каждого создаваемого объекта, являющегося наследником NSObject/NSProxy есть внутренний счётчик ссылок, который после создания устанавливается равным в единицу. Чтобы увеличить счётчик ссылок на объект, нужно вызвать retain, для уменьшения счётчика ссылок — release. После того, как счётчик ссылок достигнет 0, объект освобождается.
Существует очень удобный механизм, который по большей части избавляет вас от необходимости ручного подсчета ссылок, и называется NSAutoreleasePool — согласно соглашению, если метод возвращает ссылку на объект, и при этом не содержит ключевых слов alloc, copy, то счётчик ссылок всё равно устанавливается в 1, но после завершения обработки события, или после опустошения пула вручную, его счётчик ссылок автоматически уменьшается на единицу (вы можете также послать вручную выделенный вручную объект в autorelease-пул, послав ему сообщение autorelease вместо release).
Если ваш код выполняется в отдельной нити, то нужно создать NSAutoreleasePool в этой нити вручную, иначе вся память от autorelease объектов будет утекать (о чём вы сможете легко догадаться из-за огромного количества записей в консоли).
В XCode есть отличные утилиты для отладки и анализа работы с памятью, например можно выбрать пункт Build → Build and Analyze, и тогда ваш код будет проанализирован статическим анализатором Clang, который может вам указать на типичные ошибки при управлении памятью (и не только). Также, есть возможность запустить приложение с целью обнаружить утечки, для этого запустите Run → Run with Performance Tool → Leaks в вашем проекте. Она отслеживает утечки памяти прямо в процессе работы приложения, причём она может находить утечки не только в коде Objective C, но и в обычном C (с malloc/free), и даже внутри закрытых фреймворков (в том числе и от Apple)!
Функции для работы с Keychain достаточно низкоуровневые (в отличие от большинства фреймворков, которые работают с пользовательским интерфейсом), и используют API на языке C. В документации от Apple есть очень объемное руководство по всем вызовам, которые поддерживаются подсистемой Keychain Services, но я бы хотел показать, насколько просто можно делать базовые вещи.
При работе с вызовами на языке C, Apple в основном использует CoreFoundation. CoreFoundation использует и поддерживает практически те же самые типы данных, которые используются в Objective C с фреймворком Cocoa, и даже поддерживает прозрачное приведение типов CoreFoundation <-> Cocoa. Все вызовы CoreFoundation имеют префикс CF (ср. с NS), а имена типов получаются с помощью замены NS на CF и звездочки [*] на суффикс Ref (reference, ссылка) в конец (к примеру, NSString* <-> CFStringRef, NSArray* <-> CFArrayRef). Для работы с памятью используются CFRelease(CFTypeRef) / CFRetain(CFTypeRef), о назначении и способе использования которых можете догадаться сами.
Итак, чтобы начать работу с Keychain, нужно добавить пару заголовков и подключить фреймворк Security.framework:
Чтобы добавить интернет-пароль для вашего приложения в связку ключей по умолчанию, используйте вызов SecKeychainAddInternetPassword. У него очень много аргументов, большАя из которых не является обязательной. Пример использования находится ниже:
Для получения интернет-пароля из Keychain, мы используем вызов SecKeychainFindInternetPassword, пример работы с которым можно увидеть ниже:
Заметьте, что мы нигде не предполагали, на какой ОС этот код будет запущен, так что он будет работать и в Mac OS X и в iPhoneOS (причём в iPhoneOS для доступа к Keychain из приложения не требуется подтверждение пользователя).
Я уже неоднократно писал на Хабре про свою утилиту, которая позволяет подключать удаленные ФС с помощью SSHFS и GUI, но хочу сообщить, что она наконец-то доросла до состояния, когда ей вполне можно пользоваться:
Она позволяет монтировать удаленную файловую систему с использованием MacFUSE и/или утилиты с сайта pqrs.org. Есть список последних серверов, безопасная передача пароля утилите ssh (предмет моей особой гордости :)), опциональное хранение паролей в Keychain, поддержка сжатия. По сравнению с MacFusion.app, она, безусловно, имеет меньше возможностей, но она проще в использовании, кодовая база в 11 (!) раз меньше (28 Кб против 318 Кб), не кидается кучей запросов от Keychain на старте, не требует запуска отдельного демона, и т.д. А по сравнению с ExpanDrive бесплатная и opensource.
P.S. Этой мой восьмой хабратопик, прошу особо не пинать.
Управление памятью
Вообще, конечно, начиная с Mac OS X 10.5 управление памятью стало намного проще, поскольку добавилась поддержка сборки мусора (GC). Соответственно, можно её просто включить по умолчанию в вашем проекте и забыть об этом, как о страшном сне.
Тем не менее, если вы внимательно почитаете описание, то там будет написано следующее: «All Objective-C code linked or loaded by this application must also be GC capable». То есть, если вы используете сторонние фреймворки в закрытом виде, которые были скомпилированы без поддержки GC — придется либо от них отказаться, либо использовать более старые механизмы управления памятью, в виде retain/release. Также, поддержка GC отсутствует в iPhone, так что если вы решили написать своё приложение под iPhone OS, то в любом случае вам придется с этим столкнуться.
Я буду предполагать, что у вас все-таки какие-то начальные знания об Objective C есть, к примеру, вы уже читали этот топик.
В Objective C используется стандартный механизм подсчета ссылок: у каждого создаваемого объекта, являющегося наследником NSObject/NSProxy есть внутренний счётчик ссылок, который после создания устанавливается равным в единицу. Чтобы увеличить счётчик ссылок на объект, нужно вызвать retain, для уменьшения счётчика ссылок — release. После того, как счётчик ссылок достигнет 0, объект освобождается.
NSString *someString = [[NSString alloc] initWithFormat:@"usage: %s first-arg second-arg\n", argv[0]]; // счетчик ссылок равен 1
// ... сделать что-нибудь с этой строкой
[someString release]; // освобождаем память
Существует очень удобный механизм, который по большей части избавляет вас от необходимости ручного подсчета ссылок, и называется NSAutoreleasePool — согласно соглашению, если метод возвращает ссылку на объект, и при этом не содержит ключевых слов alloc, copy, то счётчик ссылок всё равно устанавливается в 1, но после завершения обработки события, или после опустошения пула вручную, его счётчик ссылок автоматически уменьшается на единицу (вы можете также послать вручную выделенный вручную объект в autorelease-пул, послав ему сообщение autorelease вместо release).
NSString *someString = [NSString stringWithFormat:@"usage: %s first-arg second-arg\n", argv[0]]; // счётчик ссылок всё равно равен 1, НО объект вручную освобождать НЕ НУЖНО, если только вы не сами не вызывали retain (в этом случае число вызовов release должно соответствовать количеству вызовов retain)
// ... сделать что-нибудь с этой строкой
// всё! объект освободится сам после окончания обработки события или ручного опустошения пула
Если ваш код выполняется в отдельной нити, то нужно создать NSAutoreleasePool в этой нити вручную, иначе вся память от autorelease объектов будет утекать (о чём вы сможете легко догадаться из-за огромного количества записей в консоли).
В XCode есть отличные утилиты для отладки и анализа работы с памятью, например можно выбрать пункт Build → Build and Analyze, и тогда ваш код будет проанализирован статическим анализатором Clang, который может вам указать на типичные ошибки при управлении памятью (и не только). Также, есть возможность запустить приложение с целью обнаружить утечки, для этого запустите Run → Run with Performance Tool → Leaks в вашем проекте. Она отслеживает утечки памяти прямо в процессе работы приложения, причём она может находить утечки не только в коде Objective C, но и в обычном C (с malloc/free), и даже внутри закрытых фреймворков (в том числе и от Apple)!
Работа с Keychain
Функции для работы с Keychain достаточно низкоуровневые (в отличие от большинства фреймворков, которые работают с пользовательским интерфейсом), и используют API на языке C. В документации от Apple есть очень объемное руководство по всем вызовам, которые поддерживаются подсистемой Keychain Services, но я бы хотел показать, насколько просто можно делать базовые вещи.
При работе с вызовами на языке C, Apple в основном использует CoreFoundation. CoreFoundation использует и поддерживает практически те же самые типы данных, которые используются в Objective C с фреймворком Cocoa, и даже поддерживает прозрачное приведение типов CoreFoundation <-> Cocoa. Все вызовы CoreFoundation имеют префикс CF (ср. с NS), а имена типов получаются с помощью замены NS на CF и звездочки [*] на суффикс Ref (reference, ссылка) в конец (к примеру, NSString* <-> CFStringRef, NSArray* <-> CFArrayRef). Для работы с памятью используются CFRelease(CFTypeRef) / CFRetain(CFTypeRef), о назначении и способе использования которых можете догадаться сами.
Итак, чтобы начать работу с Keychain, нужно добавить пару заголовков и подключить фреймворк Security.framework:
#import <CoreFoundation/CoreFoundation.h>
#import <Security/Security.h>
Чтобы добавить интернет-пароль для вашего приложения в связку ключей по умолчанию, используйте вызов SecKeychainAddInternetPassword. У него очень много аргументов, большАя из которых не является обязательной. Пример использования находится ниже:
const char *serverName = "habrahabr.ru";
int serverNameLength = strlen(serverName);
const char *accountName = "youROCK";
int accountNameLength = strlen(accountName);
char *path = "/";
int pathLength = strlen(path);
int port = 22;
const char *passwordData = "myExtremelySecretPassword";
int passwordLength = strlen(passwordData);
/* конечно, протокол может быть другим (не SSH :)), см. описании этой функции в руководстве, в нем можно найти ссылку на список всех поддерживаемых протоколов */
SecKeychainAddInternetPassword(NULL, serverNameLength, serverName, 0, NULL, accountNameLength, accountName, pathLength, path, port, kSecProtocolTypeSSH, kSecAuthenticationTypeDefault, passwordLength, passwordData, NULL);
* This source code was highlighted with Source Code Highlighter.
Для получения интернет-пароля из Keychain, мы используем вызов SecKeychainFindInternetPassword, пример работы с которым можно увидеть ниже:
const char *serverName = "habrahabr.ru";
int serverNameLength = strlen(serverName);
const char *accountName = "youROCK";
int accountNameLength = strlen(accountName);
char *path = "/";
int pathLength = strlen(path);
int port = 22;
UInt32 passwordLength;
void *passwordData;
OSStatus retVal;
if( (retVal = SecKeychainFindInternetPassword(NULL, serverNameLength, serverName, 0, NULL, accountNameLength, accountName, pathLength, path, port, kSecProtocolTypeSSH, kSecAuthenticationTypeDefault, &passwordLength, &passwordData, NULL)) == 0)
{
// дело в том, что passwordData является (void *) и НЕ СОДЕРЖИТ нулевого символа в конце,
// поэтому мы используем следующий код, чтобы получить пригодную для использования строку с паролем
NSString *passValue = [[NSString alloc] initWithBytes:passwordData length:passwordLength encoding:NSUTF8StringEncoding];
// делаем, что хотим с полученным паролем
SecKeychainItemFreeContent(NULL, passwordData);
[passValue release];
}else
{
// обратите внимание на 2 вещи — во-первых, в названии функции есть copy, а значит нам нужно освободить память самим
// вторая вещь — мы используем обычное приведение типов (NSString*), чтобы получить указатель на NSString из CFStringRef
CFStringRef reason = SecCopyErrorMessageString(retVal, NULL);
NSLog(@"Could not fetch info from KeyChain, recieved code %d with following explanation: %@", retVal, (NSString*) reason);
CFRelease(reason);
}
* This source code was highlighted with Source Code Highlighter.
Заметьте, что мы нигде не предполагали, на какой ОС этот код будет запущен, так что он будет работать и в Mac OS X и в iPhoneOS (причём в iPhoneOS для доступа к Keychain из приложения не требуется подтверждение пользователя).
GUI-Утилита для монтирования томов через SSH с использованием SSHFS
Я уже неоднократно писал на Хабре про свою утилиту, которая позволяет подключать удаленные ФС с помощью SSHFS и GUI, но хочу сообщить, что она наконец-то доросла до состояния, когда ей вполне можно пользоваться:
Она позволяет монтировать удаленную файловую систему с использованием MacFUSE и/или утилиты с сайта pqrs.org. Есть список последних серверов, безопасная передача пароля утилите ssh (предмет моей особой гордости :)), опциональное хранение паролей в Keychain, поддержка сжатия. По сравнению с MacFusion.app, она, безусловно, имеет меньше возможностей, но она проще в использовании, кодовая база в 11 (!) раз меньше (28 Кб против 318 Кб), не кидается кучей запросов от Keychain на старте, не требует запуска отдельного демона, и т.д. А по сравнению с ExpanDrive бесплатная и opensource.
P.S. Этой мой восьмой хабратопик, прошу особо не пинать.