0,0
рейтинг
18 февраля 2015 в 14:56

Разработка → Objective-C Runtime для Си-шников. Часть 1 из песочницы



При первом знакомстве с Objective C он произвёл на меня впечатление уродливого и нелогичного языка. На тот момент я уже имел достаточно сильную базу в C/C++ и ассемблере x86, а так же был знаком с другими высокоуровневыми языками. В документации писалось, что Objective C это расширение языка C. Но, как бы я ни старался, мне всё же не удавалось применить свой опыт в разработке приложений для iOS.

Сегодня он всё так же кажется мне уродливым. Но однажды окунувшись в глубины Objective-C Runtime я влюбился в него. Изучение Objective-C Runtime позволило мне найти те тонкие ниточки, которые связывают Objective C с его «отцом» — великолепным и непревзойдённым языком C. Это тот самый случай, когда любовь превращает недостатки в достоинства.

Если вам интересно взглянуть на Objective C не просто как на набор операторов и базовых фреймворков, а понять его низкоуровневое устройство, прошу под кат.

Небольшое уточнение
В своих статьях я буду часто путать Objective C, Objective-C Runtime, iOS SDK, iOS, iPhone и т.д. Не потому что я не понимаю разницы между ними, а потому что так будет проще объяснить суть вещей, не раздувая статьи до всеобъемлющего мануала по языку C и BSD-based системам. Поэтому большая просьба писать комментарии с уточнениями в терминологии только там, где это действительно имеет принципиальное значение.

Немного о «вызовах методов»


Давайте взглянем на привычную нам конструкцию:

[myObject someMethod];

Обычно это называют «вызвать метод». Дотошные iOS-разработчики называют это «послать сообщение объекту», в чем они, безусловно правы. Потому что какие бы «методы» и каких бы объектов вы ни вызывали, в конечном итоге такая конструкция будет преобразована компилятором в самый обычный вызов функции objc_msgSend:

objc_msgSend(myObject, @selector(someMethod));

Таким образом, все, что делает взятая нами конструкция — всего лишь вызывает функцию objc_msgSend.



Из названия можно понять, что происходит какая-то посылка сообщения. Об этой функции мы поговорим позже, потому что уже с имеющейся у нас на руках информацией мы сталкиваемся с неизвестной для нас конструкцией @selector(), в которой я и предлагаю разобраться в первую очередь.

Знакомимся с селекторами


Посмотрев в документацию, мы узнаем что сигнатура функции objc_msgSend(...) имеет следующий вид:
id objc_msgSend ( id self, SEL op, ... );


Раз обычная Си-шная функция принимает в качестве параметра аргумент типа SEL, значит, об этом самом типе SEL мы можем узнать подробнее, если захотим.

Исходя из документации, мы узнаем что существует два способа получить селектор (для нас — объект типа SEL):

  1. Во время компиляции: SEL aSelector = @selector(methodName);
  2. Во время выполнения: SEL aSelector = NSSelectorFromString(@"methodName");


Что же, нас интересует именно runtime, поэтому опять же из документации имеем следующую информацию:

SEL NSSelectorFromString ( NSString *aSelectorName );

Чтобы создать селектор, NSSelectorFromString передаёт aSelectorName в функцию sel_registerName в виде строки UTF-8 и возвращает значение, полученное из вызываемой функции. Заметьте также, что если селектор не существует, то будет возвращён вновь зарегистрированный селектор.


Вот это уже интереснее и ближе к нашему Си-шному мировосприятию. Просыпается интерес копнуть чуть глубже.

Тут я, конечно же, понимаю, что уже изрядно надоел вам своими ссылками на документацию, но по другому никак. Поэтому снова читаем документацию к методу sel_registerName и, о чудо, эта функция принимает в качестве аргумента самую обычную C-строку!

SEL sel_registerName ( const char *str );

Что ж, это максимальный уровень, до которого мы можем опуститься на основе документации. Все что пишется об этой функции, так это то, что она регистрирует метод в Objective-C Runtime, преобразовывает имя метода в селектор и возвращает его.

В принципе, этого нам достаточно для того, чтобы понять каким образом работает конструкция @selector(). А если недостаточно, то можете посмотреть исходный код этой функции, он доступен прямо на сайте Apple. В первую очередь в этом файле интересна функция

static SEL __sel_registerName(const char *name, int lock, int copy) {

Однако, непонятным остается момент с типом SEL. Все, что мне удалось найти, так это то, что он является указателем на структуру objc_selector:

typedef struct objc_selector 	*SEL;

Исходного кода структуры objc_selector я не нашел. Где-то были упоминания, что это обычная C-строка, но этот тип является полностью прозрачным и я ни в коем случае не должен вдаваться в детали его реализации, потому что Apple может в любой момент её изменить. Но для нас с вами это не ответ на вопрос. Поэтому все что нам остается делать, это вооружиться нашим любимым LLDB и получить эту информацию самостоятельно.

Для этого мы напишем простой код:

#import <Foundation/Foundation.h>

int main(int argc, const char * argv[]) {
  SEL mySelector = NSSelectorFromString(@"mySelector");
  return 0;
}

И добавим точку останова на строку «return 0;».



Путём нехитрых манипуляций с LLDB в Xcode мы узнаем, что переменная mySelector в конечном итоге является обычной C-строкой.

Так что же это за структура objc_selector, которая странным образом превращается в строку? Если вы попытаетесь создать объект типа objc_selector, то вряд ли у вас это получится. Дело в том, что структуры objc_selector просто не существует. Разработчики Apple использовали этот хак, чтобы C-строки не были совместимы с объектами типа SEL. Почему? Потому что механизм селекторов в любой момент может измениться, и абстрагирование от понятия C-строк позволит вам избежать неприятностей при дальнейшей поддержке своего кода.

Тогда логично сделать вывод, что мы можем написать следующий код, который должен работать:

#import <Foundation/Foundation.h>
#import <objc/runtime.h>

@interface TestClass : NSObject
- (void)someMethod;
@end

@implementation TestClass
- (void)someMethod {
  NSLog(@"Hello from method!");
}
@end

int main(int argc, const char * argv[]) {
  TestClass * myObj = [[TestClass alloc] init];
  
  SEL mySelector = (SEL)"someMethod";
  objc_msgSend(myObj, mySelector);
  return 0;
}


Но такой код падает со следующим пояснением:

2015-02-18 14:03:23.152 ObjCRuntimeTest[4756:1861470] *** NSForwarding: warning: selector (0x100000f6d) for message 'someMethod' does not match selector known to Objective C runtime (0x100000f82)-- abort
2015-02-18 14:03:23.178 ObjCRuntimeTest[4756:1861470] -[TestClass someMethod]: unrecognized selector sent to instance 0x1002069c0


Objective C Runtime сказал нам, что он не знает о таком селекторе, которым мы попытались оперировать. И мы уже знаем почему — мы должны зарегистрировать селектор с помощью функции sel_registerName().

Здесь я прошу обратить внимание, что я привел именно две строки вывода ошибок. Дело в том, что когда вы просто оперируете селектором, который получили с помощью @selector(someMethod), и посылаете сообщение какому-то объекту, то вам выдается только ошибка «unrecognized selector sent to instance». Но в нашем случае нам перед этим сказали, что Objective C Runtime не знает такого селектора. На основе этого можно сделать вывод, что селекторы не имеют никакого отношения к объектам. То есть, если у двух объектов совершенно разных классов будет метод:

- (void)myMegaMethod;

, то для вызова этого метода для обоих объектов будет использоваться один и тот же селектор, зарегистрированный вами в runtime с помощью конструкции

SEL myMegaSelector = @selector(myMegaMethod);

Что же значит «зарегистрированный селектор»? Чтобы не вдаваться в детали реализации sel_registerName(), я объясню это так: вы передаете этой функции C-строку, а в ответ она вам возвращает копию этой строки. Почему копию? Потому что он копирует переданный вами идентификатор в свою, более понятную ему область памяти, и отдает вам указатель на строку, которая находится именно в этой самой «понятной» для него области памяти. О том, что это за область памяти, мы поговорим с вами позже.

Подведение итогов


Мы вдоволь начитались документации, поигрались с отладчиком, но теперь надо бы это дело подытожить в наших с вами Си-шных головах.

Теперь мы может сделать «вызов метода» исключительно средствами языка C:

SEL mySelector = sel_registerName("myMethod");
objc_msgSend(myObj, mySelector);

То есть нам не врали: Objective-C действительно совместим с языком C, являясь его расширением.

На этом мне хотелось бы закончить первую часть и позволить вам насладиться этим приятным ощущением хоть и небольшого, но все же понимания принципов работы Objective-C Runtime.

Update

Спасибо за замечание пользователю 1101_debian о том, каким образом мы пришли к выводу о том, что селектор это обычная C-строка. Попробую пояснить.

К такому выводу мы пришли на основании вывода команды «print mySelector» в консоли LLDB. Это можно увидеть на скриншоте. LLDB сообщил нам о том, что это именно строка. Если бы это была не строка, то LLDB и вывел бы наш селектор соответствующим образом. Например, если мы запустим код:

#import <Foundation/Foundation.h>

struct MyStruct {
  char * someString;
};

typedef struct MyStruct* MyNewType;

int main(int argc, const char * argv[]) {
  MyNewType someObj = malloc(sizeof(struct MyStruct));
  someObj->someString = "fakeSelector";
  
  SEL myRealSelector = (SEL)NSSelectorFromString(@"myMethodIsJustString");
  SEL myCStringSelector = (SEL)"myCStringSelector";
  SEL myFakeSelector = (SEL)someObj;
  
  return 0;
}


То в результат мы получим следующий вывод в отладчике (обратите внимание на значения наших переменных):



Но это уже тема другой статьи, имеющей отношение не к Objective-C Runtime, а к разработке на языке C, представлению данных в памяти и использованию отладчиков.
Alex Krzyżanowski @aperechnev
карма
12,0
рейтинг 0,0
iOS Developer
Реклама помогает поддерживать и развивать наши сервисы

Подробнее
Реклама

Самое читаемое Разработка

Комментарии (31)

  • +1
    Программист на фортране — на любом языке может писать на фортране?

    Я ни в коем случае не хочу обидеть язык Си, однако в данном случае я не вижу смысла в происходящем.
    Селектор является «строкой» лишь постольку поскольку. Селектор является абстракцией, указателем в памяти, для удобства, помимо прочего, указывающую на некую описывающую его структуру (в данном случае С-стринг). С большего, этого знания достаточно для работы с этой частью языка.

    Поэтому не дай вам бог использовать это знание в своих проектах.

    P.S. К слову, могу порекомендовать закопаться на какое-то время в этот репозиторий Там все есть. Кроме определения SEL — потому что это определение кодогенерируемое :-)

    objc_selector:

    typedef struct objc_selector *SEL;
    Эта штука имеет обратное значение. objc_selector — является алиасом на *SEL, а не наоборот
    • +1
      Хм, прошу прощения за последнюю ремарку — она ошибочна.
      • 0
        Ничего страшного, Вы еще и ссылку дали на тот самый репозиторий, на который я и ссылался в статье :). А реализации действительно разные.
    • +6
      Меня, честно говоря, совершенно не интересует применение всего мной описанного в реальных проектах, потому что на работе я просто перемещаю формочки по Storyboard.

      Я погрузился в изучение Runtime только из-за спортивного интереса. Меня всю жизнь привлекали низкоуровневые вещи, поэтому изучение runtime приносит мне удовольствие. Кто-то марки коллекционирует, кто-то футбол смотрит, а я низкоуровневое программирование люблю.

      За замечание про objc_selector — спасибо. Только это тема, наверное, отдельной статьи, потому что в каждой реализации Objective-C Runtime этот тип представлен по-разному. Например, вот здесь он действительно является структурой.
      • 0
        Ну с этой точки зрения реализация сущности сообщений, а, главное, их смысл — это действительно жемчужина objective-C, которая позволяет реализовать много действительно красивых и изящных вещей.
      • НЛО прилетело и опубликовало эту надпись здесь
  • +1
    Отличная статья, надеюсь на продолжение! Язык действительно классный, несмотря на необычный синтаксис. Классный именно своими динамическими возможностями.
    Я как-то смотрел исходники рантайма и проводил небольшие эксперименты, с похожей целью — выяснить как же работает этот механизм отправки сообщений. Селекторы, сформированные на этапе компиляции, имеют значения, сильно отличающиеся от селекторов этапа выполнения (видимо, это связано с тем, что существует две таблицы — статическая в секции константных данных и динамическая в куче).
    Очень интересно, как селектор связывается с методом, сравнительный анализ этого с таблицами виртуальных функций (и можно ли эти два механизма объединить), реализация возможностей динамического добавления (и удаления?) методов из классов и т.д.
    • +2
      Продолжение обязательно будет, как и ответы на поставленные Вами вопросы. Как говорил Шеф из Саус Парка: «Всему своё время, и время это — колледж» :).
    • 0
      Как это сильно отличаются? Селектор это строка и ничего более. Никакого name mangling у objc нет. Все скомпилированные методы классов в бинарнике попадают в секцию, где лежат в виде обычных строк. Уже в другой секции есть списки методов со ссылками на этим строки. Так же рядышком лежат type encoding для каждого метода с описанием типов аргументов. Как эти методы были написаны программистом, так они и попадают в скомпилированный код, без единого изменения. Разве что из-за type encoding теряется информация о типах.
      Собственно, если бы что-то сильно отличалось, то были бы проблемы с твиками для джейлбрейка. В твиках мы обычные строки конвертируем в селекторы не имея на руках даже прототипов классов. Просто берется С строка вида «initWithKey:value:» и передается рантайму, чтобы он уже подменял реальный метод с таким селектором нашей реализацией. Мы сами знать не знаем ни о каких хедерах и классах с таким методом, компилятор ничего о них не знает.

      Да, добавлять и подменять в рантайме методы можно и это очень и очень просто. Вот для удаления API нет.
      • 0
        Отличаются в смысле самих числовых значений селекторов.
        Вообще конечно это зависит от компилятора. Я смотрю на виртуалке Snow Leopard, там так. Попробовал сейчас на ideone.com — там иначе.
        Идея следующая. Создаем objc-класс с парой методов.
        @interface c1 : NSObject {}
        - (int) meth1 : (int) arg;
        - (int) meth2 : (int) arg;
        @end
        
        @implementation c1
        - (int) meth1 : (int) arg { return arg+2; }
        - (int) meth2 : (int) arg { return arg+20; }
        @end
        


        Далее, фукнция печати селектора
        void print_sel(SEL s)
        {
        	printf("SEL %08X = %s\n", s, s);
        }


        И код main:
        int main()
        {
        	c1 *obj1 = [[c1 alloc] init];
        	SEL s1 = @selector(meth1);
        	SEL s2 = @selector(meth2);
        	SEL s3 = @selector(methUnexist1);
        	
        	SEL s4 = NSSelectorFromString(@"meth1");
        	SEL s5 = NSSelectorFromString(@"meth2");
        	SEL s6 = NSSelectorFromString(@"methUnexist2");
        
        	SEL s6 = NSSelectorFromString(init);	
        
        	print_sel(s1);
        	print_sel(s2);
        	print_sel(s3);
        	print_sel(s4);
        	print_sel(s5);
        	print_sel(s6);
        	print_sel(s7);	
        
        	return 0;
        }


        В результате имеем: s1 и s2 — селекторы существующих пользовательских методов; s3 — селектор несуществующего метода, но оформленный конструкцией языка selector; s4 и s5 — селекторы существующих пользовательских методов через функцию получения по строке (в рантайме); s6 — селектор несуществующего метода по строке в рантайме; s7 — селектор системного метода.

        Вывод (Snow Leopard):
        SEL 00000DEB = meth1
        SEL 00000DF1 = meth2
        SEL 00000E20 = methUnexist1
        SEL 00000DEB = meth1
        SEL 00000DF1 = meth2
        SEL 0010FF60 = methUnexist2
        SEL 816CD218 = init


        Видим, что селекторы существующих методов (добавленные на этапе компиляции) совпадают для разных способов получения селектора (оператором @ selector или рантаймовой фукнкцией).
        Селектор несуществующего нигде метода methUnexist1 располагается «рядом» с селекторами существующих методов. Возможно, из этого можно сделать вывод, что он также был добавлен в таблицу на этапе компиляции (т.к. @ selector — конструкция времени компиляции)?
        Селектор несуществующего метода methUnexist2 уже отличается от остальных… такое впечатление, что он как раз был создан динамически и добавлен в таблицу уже в рантайме.
        И наконец, селектор системного метода init вообще очень сильно отличается по значению. Интерпретировать это я уже не могу:)
        • 0
          опечатка, не успел исправить: вместо
          SEL s6 = NSSelectorFromString(init);

          следует читать
          SEL s7 = @selector (init);
        • 0
          То, что вы вывели, всего навсего адреса селекторов в памяти. Они ничего принципиально не значат. В конечном итоге каждый селектор это просто С строка и не важно в какой области памяти процесса она лежит. Естественно строковые литералы будут лежать в другом месте совсем — селекторы таковыми и являются. Откройте любой objc бинарник в дизассемблере и среди строковых литералов найдете все методы и классы. Мы можем хоть в зашифрованном виде хранить эти строки и расшифровывать только при вызове метода через какой-нибудь NSInvocation — все будет работать как по маслу. Рантайм всего лишь ждет от нас строку и по ней найдет метод, если он есть у класса. Где бы она ни находилась — в литералах, куче, стеке. В этом весь смак objc runtime.
          • 0
            Да я не против:) Просто интересно было как оно устроено, в том числе с точки зрения компилятора. Концепция мне очень нравится, но интересно, можно ли ее еще улучшить, можно ли интегрировать с другими языками программирования (включая альтернативные реализации динамики — виртуальные функции C++, сигналы-слоты Qt, интерфейсы Go). И как это можно было бы сделать в своем собственном языке программирования.
            Получается, что ObjC хранит все имена всех методов в виде строк; и в основной реализации адреса этих строк и являются по сути селекторами (хотя формально это необязательно — достаточно чтобы было соответствие между текстовым именем метода и некоторым числовым идентификатором). Адреса строк — это самая простая, тупая и быстрая реализация такого соответствия.
            А дальше, насколько я понимаю, строятся какие-то словари, в чем-то похожие на таблицы виртуальных функций, где ключ — селектор, а значение — адрес метода? Надеюсь, автор нам расскажет об этом в следующей статье:)

            • 0
              Естественно там есть таблицы для быстрого поиска методов по селекторам. Никто при каждом вызове strcmp, скажем, не вызывает. В подробности того, какая именно там структура данных используется, я правда не вдавался. Но то, что вызов objc метода делается довольно быстро, это знаем точно www.mikeash.com/pyblog/performance-comparisons-of-common-operations-leopard-edition.html
  • 0
    Я так и не понял с чего вы взяли что селектор это обычная строка?
    • 0
      Спасибо за замечание. Добавил ответ на Ваш вопрос к статье.
      • 0
        Спасибо за обвноление, но есть подозрения что это не так на самом деле.
        Если посмотреть в исходный код LLDB (начиная со строки ~1250), то видно что оно обрабатывает разные встроенные типы по-разному:

        AddCXXSummary(objc_category_sp, lldb_private::formatters::ObjCSELSummaryProvider<false>, "SEL summary provider", ConstString("SEL"), objc_flags);
        AddCXXSummary(objc_category_sp, lldb_private::formatters::ObjCSELSummaryProvider<false>, "SEL summary provider", ConstString("struct objc_selector"), objc_flags);
        AddCXXSummary(objc_category_sp, lldb_private::formatters::ObjCSELSummaryProvider<false>, "SEL summary provider", ConstString("objc_selector"), objc_flags);
        AddCXXSummary(objc_category_sp, lldb_private::formatters::ObjCSELSummaryProvider<true>, "SEL summary provider", ConstString("objc_selector *"), objc_flags);
        AddCXXSummary(objc_category_sp, lldb_private::formatters::ObjCSELSummaryProvider<true>, "SEL summary provider", ConstString("SEL *"), objc_flags);
        


        Я не копал глубже, но подозреваю что там все-таки не строка.
      • 0
        В подтверждение моего комментария предлагаю вам взглянуть на вывод для кастомной структуры и, допустим, NSRect:

        P.S. почуму-то XVim показывает путь к файлу из другого проекта...
        • 0
          Вы абсолютно правы в своих рассуждениях, но ключевой момент тут в том, каким образом будет выглядеть такая структура в памяти. В Вашем случае структура

          struct Dragon {
            char name[10];
          }


          будет представлена в памяти так же как и

          char name[10];


          То есть в исходном коде различия могут и быть, тут вы правы. Но предмет нашего обсуждения это именно время исполнения.
          • 0
            Кстати, я бы не исключал того, что char name[10] представляет собой только первую часть структуры, реализующей собой селектор. В таком случае, приводя селектор к char* — мы и будем видеть только лишь первую часть структуры.

            svn.gna.org/svn/gnustep/libs/libobjc2/trunk/selector.h — вот тут например приводится альтернативный вариант, при котором над char * (которому, к слову не всегда соответствует первая часть структуры, экономии чего-то ради, будучи замененная на некоторый индекс) существует и еще одно поле types, которое хранит в себе, судя по всему, сигнатуру вызова.
            • +1
              Вообще да, к селекторам, по хорошему, так же привязан type encoding, который дает понять примерно какого типа аргументы. Но это информация уже конкретного класса, а не самого селектора. Селектор это имя метода, которое никак не привязано к конкретному классу и никак не описывает типы аргументов. Селектор «value» может возвращать какой угодно тип в зависимости от того, какому классу этот селектор дать. Он даже может означать одновременно метод класса и метод экземпляра. Лишний раз можно заглянуть в рантайм API developer.apple.com/library/mac/documentation/Cocoa/Reference/ObjCRuntimeRef/index.html

              Функции для работы с селекторами не позволяют получить никаких типов. Только имя, а это та самая С строка. Тип получается только для методов с помощью method_getTypeEncoding. Туда передается тип Method, который можно получить только от конкретного класса, передав ему тот самый универсальный селектор. И вот Method уже должен внутри себя содержать и селектор, и type encoding.

              Так что представленный код выглядит довольно странно. Не вписывается в логику самого языка и его рантайма. И не вписывает в то, как это все хранится в бинарнике, где селекторы лежат отдельным списком С литералов, а отдельно лежат метаданные классов со ссылкой на эти литералы для каждого конкретного метода.
          • 0
            Я к тому, что полагаться на вывод дебаггера не совсем правильно.
  • 0
    Вам не хватает бэкграунда Smalltalk — именно оттуда растут ноги селекторов Objective-C. Селекторы были придуманы в своё время для экономии памяти и быстрого сравнения — все селекторы сидят в общем словаре, и если где-то объявляется уникальный, он туда добавляется, а потом, если где встречается ещё раз — уже добавляется только ссылка на него. Соответственно, сравнение селекторов для системы очень быстрая операция, проверяются только указатели.

    Такие дела. А Смолток волшебен, у меня по нему старая добрая ностальния, хнык-хнык (
  • –1
    И к сожалению Apple решили весь этот динамизм закопать при переходе на Swift…
    • 0
      Думаю, Вы не правы. Apple говорит, что в основе Swift лежит любимый нами Objective-C Runtime. Это вдвойне радует, потому что наши знания не пропадут даром даже после перехода на Swift :).
      • 0
        Знания никогда не пропадут, а вот возможности запросто. Apple говорит, но там есть хитрость. Насколько помню, если swift класс используется в objc коде, то там действительно все как в старом добром objc runtime. Но если swift класс не используется в objc, то он превращается, по сути, в подобие C++ классов с жестко заданной таблицей виртуальных функций, которая находится после всех метаданных. Никаких селекторов, прямые вызовы методов. Swift на самом деле сильно другой внутри. Есть вот такая лекция небольшая на эту тему www.youtube.com/watch?v=Ii-02vhsdVk
        • –2
          Более того, из свифта принципиально нельзя дернуть retain/release или performSelector. Очень больно, учитывая что компилятор over-release местами себе устраивает.
      • 0
        Не совсем так. В Свифте объявление класса можно написать двумя способами:

        class Foo : NSObject {
        

        либо

        class Foo {
        

        В первом случае Foo будет полноценным NSObject'ом, с которым можно пользоваться Objective-C рантаймом как нам хочется. Во втором случае это чисто свифтовый объект и вызов методов будет идти через свифтовый рантайм.
        • 0
          Вы еще не упомянули ключевое слово dynamic для свойств.
  • 0
    Селекторы концептуально похожи на Java-овский String.intern :)

    Мне тоже нравится Objective C. Это такая красивая нашлёпка над C, которая позволяет писать высокоуровневые программы, сильно не выпрыгивая из низкого уровня C. Кому то не нравится, а мне, вот, нравится. Не люблю бессмысленных абстракций.

    Не буду переходить на Swift до последнего.
  • 0
    «Сегодня он всё так же кажется мне уродливым. Но однажды окунувшись в глубины Objective-C Runtime я влюбился в него.»
    В очередной раз убеждаюсь в прописной истине: Любовь зла, полюбишь и козла.

Только зарегистрированные пользователи могут оставлять комментарии. Войдите, пожалуйста.