Objective-C Runtime изнутри

    (оригинал — Mike Ash, взято отсюда)

    Многие Cocoa разработчики имеют довольно смутное представление об Objective-C Runtime API. Они знают, что он существует где-то там (некоторые не знают даже этого!), что он важен, и Objective-C без него неработоспособен, но обычно этим все знания и ограничиваются.

    Сегодня я расскажу о том, как устроен Objective-C на уровне Runtime и о том, как конекретно вы можете это использовать.

    Объекты


    В Objective-C мы постоянно имеем дело с объектами, но что же такое объект на самом деле? Давайте попробуем соорудить что-то, что поможет пролить нам свет на этот вопрос.

    Во-первых, всем нам известно, что мы ссылаемся на объекты с помощью указателей, например, NSObject *. Также мы знаем, что создаём мы их с помощью +alloc. Всё, что мы може узнать об этом из документации, так это то, что это происходит путём вызова +allocWithZone:. Продолжая нашу цепочку исследований, мы обнаруживаем NSDefaultMallocZone, который создаётся с помощью обыкновенного malloc. И всё!

    Но что из себя представляют созданные объекты? Что ж, посмотрим:
     #import <Foundation/Foundation.h>
        
        @interface A : NSObject { @public int a; } @end
        @implementation A @end
        @interface B : A { @public int b; } @end
        @implementation B @end
        @interface C : B { @public int c; } @end
        @implementation C @end
        
        int main(int argc, char **argv)
        {
            [NSAutoreleasePool new];
            
            C *obj = [[C alloc] init];
            obj->a = 0xaaaaaaaa;
            obj->b = 0xbbbbbbbb;
            obj->c = 0xcccccccc;
            
            NSData *objData = [NSData dataWithBytes:obj length:malloc_size(obj)];
            NSLog(@"Object contains %@", objData);
            
            return 0;
        }
    

    Мы соорудили иерархию классов, каждый из которых содержет в себе переменные, и заполнили их вполне очевидными значениями. Затем мы извлекли данные в удобоваримый вид, исползьуя malloc_size, дабы получить правильную длину и воспользовались NSData, чтобы распечатать всё в hex. Вот, что у нас получилось на выходе:
     2009-01-27 15:58:04.904 a.out[22090:10b] Object contains <20300000 aaaaaaaa bbbbbbbb cccccccc>
    

    Мы видим, что класс последовательно заполнил ячейки памяти—сначала переменную A, потом его наследника B, а потом C. Всё просто!
    Но что за 20300000 в самом начале? Они идут перед A, и потому, скорее всего, принадлежат NSObject. Посмотрим-ка на определение NSObject.
        /***********	Base class		***********/
        
        @interface NSObject  {
            Class	isa;
        }
    

    Как видим, вновь какая-то пременная. Но что это за Class такой? Переходим по определению, которое нам предлагает Xcode и попадаем в usr/include/objc/objc.h, в котором находим следущее:
     typedef struct objc_class *Class;
    

    Идём дальше в /usr/include/objc/runtime.h и видим:
    struct objc_class {
            Class isa;
        
        #if !__OBJC2__
            Class super_class                                        OBJC2_UNAVAILABLE;
            const char *name                                         OBJC2_UNAVAILABLE;
            long version                                             OBJC2_UNAVAILABLE;
            long info                                                OBJC2_UNAVAILABLE;
            long instance_size                                       OBJC2_UNAVAILABLE;
            struct objc_ivar_list *ivars                             OBJC2_UNAVAILABLE;
            struct objc_method_list **methodLists                    OBJC2_UNAVAILABLE;
            struct objc_cache *cache                                 OBJC2_UNAVAILABLE;
            struct objc_protocol_list *protocols                     OBJC2_UNAVAILABLE;
        #endif
        
        } OBJC2_UNAVAILABLE;
    

    Таким образом, Class это указатель на структуру, которая… Начинается с ещё одного Class
    Посмотрим ещё один класс, NSProxy
    @interface NSProxy  {
            Class	isa;
        }
    

    И тут он есть. Ещё один, id, за которым может скрываться любой объект в Objective-C
    typedef struct objc_object {
            Class isa;
        } *id;
    
    

    И снова он. Очевидно, что каждый объект в Objective-C должен начинаться с Class isa, даже объекты классов. Так что же это такое?
    Как следует из названия и типа, переменная isa указывает, какому классу принадлежит тот или иной объект. Каждый объект в Objective-C должен начинаться с isa, иначе runtime не будет знать, что же с ним делать. Вся информация о типе каждого конкретного объекта скрывается за этим крохотным указателем. Оставшийся кусок объекта, с точки зрения runtime, представляет из себя просто огромный BLOB, не дающий никакой информации. Только лишь классы могут придать этому куску какой-то смысл.

    Классы


    Что же тогда на самом деле содержится в классах? Ответ на этот вопрос нам поможет найти «недоступные » поля структуры (те, что после #if !__OBJC2__, они оставлены здесь для совместимости с пре-Леопардом, и вы не должны пользоваться ими, если занимаеться разработкой пол Леопард и выше, однако, они помогут понять нам, что же за информация там скрывается). Сначала идет isa, позволяющий работать с классом, как с объектом. Потом идет ссылка на Class — предок, дабы не нарушалась иерархия классов. Далее идет некоторая базовая информация о классе. Но самое интересное — в конце. Это список переменных, список методов и список протоколов. Все это доступно во время исполнения, и может быть изменено там же!

    Я пропустил кэш, так как он не слишком интересен с точки зрения манипуляции во время исполнения, но стоит рассказать о том, какую роль он играет в принципе. Каждый раз, когда вы посылаете сообщение ([foo bar]), Runtime ищет его в списке методов класса объекта. Но так как это просто линейный список, этот процесс достаточно продолжительный. Кэш же — это хэш таблица, в которой содержатся уже вызывавшиеся до этого методы. Именно поэтому первый вызов метода может быть значительно дольше, чем все последующие.

    Исследуя runtime.h, вы можете обнаружить множество функций для доступа и изменению этих элементов. Каждая функция начинается с префикса, который показывает, с чем она имеет дело. Базовые начинаются на objc_, функции для работы с классами на class_, и так далее. Например, вы можете вызвать class_getInstanceMethod, чтобы узнать информацию о конкретном методе, такую как список аргументов/тип возвращаемого значения. Или же можете добавить новый метод с помощью class_addMethod. Вы даже можете создавать целые классы с помощью objc_allocateClassPair прямо во время исполнения!

    Практическое применеие


    Есть множество вариантов применеия этой Runtime мета-информации, вот только некоторые из них
    1. Автоматический поиск переменных/методов. Apple уже реализовало это в виде Key-Value Coding: вы задаете имя, в соответсвии с этим именем получаете переменную или метод. Вы можете делать это и сами, если вас чем-то не устраивает реализация Apple.(прим. переводчика — например, вот так Better key-value observing for Cocoa)
    2. Автоматическая регистрация/вызов подклассов. Используя objc_getClassList вы можете получить список классов, уже известных Runtime и, проследив иерархию классов, выяснить, какие подклассы наследуются из данного класса. Это дает вам возможность писать подклассы, которые будут отвечать за специфичиские форматы данных, или что-то в этом роде, и потом дать суперклассу возможность находить их самому, избавив себя от утомительной необходимости регистрировать их все руками.
    3. Автоматически вызвывать метод для каждого класса. Это может быть полезно для unit-testing фреймворков и подобного рода вещей. Очень похоже на #2, но с акцентом на поиск возможных методов, а не на иерархию классов
    4. Перегрузка методов во время исполнения. Runtime предоставляет вам полный набор инструментов для изменения реализации методов классов, без необходимости изменять что-либо в их исходном коде
    5. Bridging Имеея возможность динамически создавать классы и просматривать необходимые поля, вы можете создать мост между Objective-C и другим (достаточно динамичным) языком.
    6. И многое, многое другое! Не ограничивайте себя списком, представленным выше.

    Заключение


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

    (прим. переводчика — для тех, кому уже не терпится поиграться со всей этой бесконечной динамичностью Objective-C, но не хочется разбираться с runtime.h, Mike Ash выложил на GitHub проект — обертку над runtime.h, предоставляющую полный доступ ко всем вкусностям, описаным выше, но в привычном Objective-C синтаксисе.)
    Поделиться публикацией

    Комментарии 13

      –1
      lass_getInstanceMethod => class_getInstanceMethod
        +7
        Спасибо, исправил, но есть же хабрапочта)
        0
        > проследив иерархию классов, выяснить, какие подклассы наследуются из данного класса. Это дает вам возможность писать подклассы, которые будут отвечать за специфичиские форматы данных, или что-то в этом роде, и потом дать суперклассу возможность находить их самому, избавив себя от утомительной необходимости регистрировать их все руками.

        Всегда надеялся, что линкер или оптимизирующий компилятор выбрасывают неиспользуемый код, и в исполняемый файл попадает код только используемых функций и классов, т.е. если импортировать какую-нибудь библиотеку с тысячей функций, и использовать только одну из них, то только эта одна функция и будет включена в итоговый исполняемый файл.
        Но возможно, это экономия на спичках в наше время терабайтовых жёстких дисков и гигабайтов оперативной памяти.
          0
          Я тоже не так давно задался этим вопросом, оказалось, что компилятор не производит такого уровня оптимизации.
            0
            У iPhone опертаивная память имеет ограничения, поэтому не сказал бы, что это экономия на спичках.
              +1
              такая оптимизация в ObjC нерелевантна и невозможна.
              0
              Я уже не представляю как можно кодить без Runtime API, он действительно открывает огромнейшие возможности, так что если кто еще не начал его юзать — очень советую.
                0
                Прям классы на лету создаёте? Или больше интроспекцией занимаетесь?
                  +3
                  Да всего по чуть-чуть, вот в этом проекте все построено на мощи Runtime.
                  Классы на лету только инстанциировал, но новый класс не добавлял.
                  Самый банальный пример удобного использования ObjC Runtime это замена switch'ей.
                  Допустим нам надо основываясь на каком-то выборе отображать конкретный контроллер, в случае без гибкого рантайма пришлось бы использовать switch, а в случае с ObjC можно делать все просто и красиво

                  const char * const controllers[] = {"Foo", "Bar", "Bazz"};
                  ...
                  //  index - индекс ячейки в таблице, или тэг на кнопке
                  NSString *controllerName = [NSString stringWithFormat:@"%sViewController", controllers[index]];
                  UIViewController *viewController = [NSClassFormString(controllerName) new];
                  


                  и все, никаких свитчей, минимум кода.

                  А вообще там есть гораздо больше плюх и удобств.
                    0
                    Да, красиво, особенно, если возможных вариантов больше десятка)
                      0
                      *конечно не православно, но всё же*
                      Методика очень удобная, постоянно использую, когда работаю над ActionScript проектами:

                      const classes : Vector.<Class> = new <Class>[ FooController, BarController, BazzController ];
                      var controller : IViewController = new ( classes[ index ] )( );
                      


                      это когда список классов заранее известен. Так же есть прекрасные вещи в стиле:

                      function addParticle(particleClassName : String = "DefaultParticle") : void
                      {
                          addChild( new ( ApplicationDomain.currentDomain.getDefinition( particleClassName ) )() );
                      }
                      


                      честно признать, не знал, что в ObjC оно тоже есть, буду пользоваться:) Спасибо!
                0
                Офигительное пятничное чтиво, серьёзно. Спасибо. :)
                  0
                  Спасибо, сам испытываю наслаждение от статей Mike Ash)
                  На самом деле этот мужик просто космический! Он, по-моему, единственный на земном шаре понимает как компьютеры работают на всех уровнях: от микросхем до высокоуровневых API.

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

                Самое читаемое