Немного о Swift runtime или куда пропал NSObject

в 12:12, , рубрики: ios development, objective-c, runtime, swift, разработка под iOS

Привет друзья! Я из тех, кому скучно просто дергать за ниточки, торчащие из черной коробки, хочется увидеть все своими глазами, как оно работает под капотом. Мы поговорим с вами про рантайм, да-да рантайм. Для наших опытов рассмотрим старого доброго дедушку Objective C и революционного, но пока еще находящегося в стадии развития, Swift. Нам с вами понадобиться нырнуть практически на самое дно абстракций, которые заботливо придумали программисты Apple. Давайте немного разберемся, зачем вообще понадобилось разрабатывать новый язык. Я слышал много негативных отзывов в самом начале, особенно от уже матерых разработчиков Objective C. Если посмотреть повнимательнее на новый язык Swift, он на мой взгляд значительнее взрослее и серьезнее. Во-первых, он написал на языке С++ в отличии от Си, который лежит в основе Objective C. Я здесь высказываю только свои сугубо личные предубеждения, с которыми можно согласиться, можно и поспорить.

На мой взгляд С++ на текущий момент, самый серьезный язык разработки, который позволяет делать все что угодно, но уже более изящнее нежели в Си. Я думаю поэтому именно он был выбран за основу написания языка Swift, LLDB и тд. Я не буду сейчас делать обзор функциональности языка Swift, просто осветим несколько моментов, все остальное можно почитать в специализированной документации. На мой взгляд он намного функциональнее и проще для начинающего программиста, собственно это и было одной из целей его разработки, снизить порог вхождения для новых разработчиков. Вспомните как у вас вставали волосы после первого знакомства со скобочками Objective C, особенно если до этого ты писал на лаконичном C# или Java. Конечно не обошлось и без изощрений, которые придумали программисты на мой взгляд только ради того, что бы выделиться. Но это все лирика, давайте к делу. Для начала разберем базовый класс дедушки Objectice C потому сравним его со Swift.

Для тех кто знает все это, можно пока выйти покурить. Как мы знаем в Objective C любой класс или косвенно или напрямую должен наследоваться от NSObject или NSProxy. Класс NSObject реализует неформальный протокол NSObject. К нему мы еще вернемся, когда будем рассматривать SwiftObject. Забегая вперед скажу, что именно этот протокол очень поможет подружить два языка в будущем, вот она сила полиморфизма! Я предлагаю сделать так, мы по кусочками рассмотрим все методы класса NSObject. А после этого я осмелюсь сделать заключение, что не так с этим великим NSObject. Не буду долго вам мучать, погнали!

@interface NSObject <NSObject> {
    Class isa  OBJC_ISA_AVAILABILITY;
}

+ (void)load;

+ (void)initialize;
- (instancetype)init
#if NS_ENFORCE_NSOBJECT_DESIGNATED_INITIALIZER
    NS_DESIGNATED_INITIALIZER
#endif
    ;

+ (instancetype)new OBJC_SWIFT_UNAVAILABLE("use object initializers instead");
+ (instancetype)allocWithZone:(struct _NSZone *)zone OBJC_SWIFT_UNAVAILABLE("use object initializers instead");
+ (instancetype)alloc OBJC_SWIFT_UNAVAILABLE("use object initializers instead");
- (void)dealloc OBJC_SWIFT_UNAVAILABLE("use 'deinit' to define a de-initializer");

- (void)finalize;

....

С первыми двумя методами не так часто приходиться сталкиваться рядовому разработчику Objectice C. Не буду на них останавливаться, скажу лишь пару слов. Метод load вызывается по время загрузки класса или категории в исполняющую среду, не подчиняется классическим правилам наследования, блокирует работу приложения на момент выполнения. Метод initialize вызывается в отложенном режиме, перед первым использованием класса, не блокирует работу приложения, подчиняется классическим правилам наследования.

Все последующие методы отвечают за создания и инициализацию объекта. Тоже немножечко обсудим их. Метод allocWithZone отвечает за выделение памяти под наш объект. Внутри он вызывает наш любимый malloc. Он еще с бородатых времен, когда память разделялась на зоны. Сейчас все объекты создаются в одной зоне, потому появился метод alloc который внутри себя вызывает allocWithZone и передает ему зону по умолчанию — NSDefaultMallocZone.

Методы dealloc и finalize вызывается в момент удаления объекта. Именно в этих методах происходит зачистка всех связанных ресурсов и конечном итоге free и память уходит в пул свободной памяти, жаждущей новых выделений. Отмечу что finalize обычно не используется а dealloc вызывается в том потоке, в котором произошло последнее освобождение.

Переходим к следующей пачке методов

- (id)copy;
- (id)mutableCopy;

+ (id)copyWithZone:(struct _NSZone *)zone OBJC_ARC_UNAVAILABLE;
+ (id)mutableCopyWithZone:(struct _NSZone *)zone OBJC_ARC_UNAVAILABLE;

Ну наверно даже тому кто никогда не писал на обжектив си нетрудно догадаться, что они для копирования. С зонами все понятно, это древние методы которые уже не актуальны. На самом деле, что бы у нас что то скопировалось надо еще реализовать протокол NSCopying, а если просто вызвать эти методы, то все упадет. Но это мы еще обсудим. А пока перейдем к следующей пачке.

+ (BOOL)instancesRespondToSelector:(SEL)aSelector;
+ (BOOL)conformsToProtocol:(Protocol *)protocol;
- (IMP)methodForSelector:(SEL)aSelector;
+ (IMP)instanceMethodForSelector:(SEL)aSelector;
- (void)doesNotRecognizeSelector:(SEL)aSelector;

+ (NSMethodSignature *)instanceMethodSignatureForSelector:(SEL)aSelector OBJC_SWIFT_UNAVAILABLE("");

- (BOOL)allowsWeakReference UNAVAILABLE_ATTRIBUTE;
- (BOOL)retainWeakReference UNAVAILABLE_ATTRIBUTE;

+ (BOOL)isSubclassOfClass:(Class)aClass;

+ (BOOL)resolveClassMethod:(SEL)sel OBJC_AVAILABLE(10.5, 2.0, 9.0, 1.0);
+ (BOOL)resolveInstanceMethod:(SEL)sel OBJC_AVAILABLE(10.5, 2.0, 9.0, 1.0);

+ (Class)superclass;
+ (Class)class OBJC_SWIFT_UNAVAILABLE("use 'aClass.self' instead");

Вот она интроспекция, собственной персоной. Ключ к безопасному коду в обжектив си. Эти методы позволяют нам оценить объект или класс, какие протоколы он реализует, на какие селекторы может реагировать, и тд. Идем дальше

- (id)forwardingTargetForSelector:(SEL)aSelector OBJC_AVAILABLE(10.5, 2.0, 9.0, 1.0);
- (void)forwardInvocation:(NSInvocation *)anInvocation OBJC_SWIFT_UNAVAILABLE("");
- (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector OBJC_SWIFT_UNAVAILABLE("");

Два первых метода вызовутся до того момента как у вас все грохнется если в них вы ничего не реализуете. Это происходит тогда, когда наш объект не понимает, что вы от него хотите и вызываете метод, которого у него нет. Если вы это делаете осмысленно то будьте любезны определить у себя один из этих методов и решить, что делать с вызовом селектора, куда его послать так сказать. На самом деле это называется механизм перенаправления сообщений. Об этом тоже можно почитать, куча статей, и методов там три. Я не буду на этом останавливаться, иначе мы потеряем основную суть этой статьи.

Давайте посмотрим на последние методы

+ (NSUInteger)hash;
+ (NSString *)description;
+ (NSString *)debugDescription;

Я думаю метод хеш всем известен, используется для сравнения объектов, для распределения объектов по гнездам в коллекциях и тд. Методы description и debugDescription необходимо переопределять если вы хотите видеть в логах не просто адреса по которым располагается ваш объект, а какую то осмысленную для него информацию.

Фух, что то я уже утомился описывать эти методы. Давайте теперь повнимательнее посмотрим на этот класс, вот он стоит во главе всех классов в обжектив си, в основном. Что с ним не так? Чем он мне так не нравиться? Да все с ним не так!

Как там гласят наши принципы ООП: наследование, инкапсуляция, полиморфизм! Но там почему то не сказано что наследуй все от одного класса, да простят меня разработчики Java и С#. Я думаю программисты с опытом построения архитектур, наталкивались на различные подводные камни архитектурных решений, которые изначально казались очень продуманными. Я бы сказал что наследование это как бомба замедленного действия, которая в конечном итоге сведет всю вашу продуманную архитектуру в ноль или уже свяжет вам руки для развития. Во многих книгах по архитектуре написано, что выбирайте композицию вместо наследования. Это более гибкий подход, но наследование тоже очень мощная штука, с которой надо уметь обращаться. А именно, правильно разделять абстракции и закладывать на дальнейшее расширение. Забегая вперед скажу, что у Swift пропал этот базовый класс, доступный для программиста.

На самом деле он есть, и называется он SwiftObject от которого наследуются все классы в IOS, написанные на свифте. Мы еще поговорим о нем. Многие наверно скажут: что несет этот парень! Наследование это же крутая вещь, переиспользование кода, что в нем плохого. Я думаю эту тему я вынесу в отдельную статью, пока поговорим о другом. Вот например зачем мне метод copy, если я не хочу ничего копировать? Тем не мене я могу его вызывать, и само собой все упадет, если я не реализую прокол NSCopying. Давайте еще поговорим про наследование. Есть метод init который я должен вызывать, если хочу проинициализовать объект, а есть метод dealloc, который вызывается сам! Ибо это метод жизненного цикла объекта и не надо вызывать его руками, никогда! Но никто ведь не мешает мне это сделать, не правда ли здорово? Да вот совсем не здорово. Выходит сам по себе класс NSObject позволяет нам делать то чего делать не надо, или знать то, о чем нам знать не обязательно. Я могу развивать тему дальше и дальше, но я думаю уже понятно, что NSObject это уродство, которого быть не должно и поэтому он пропал в языке Swift для программиста. Формально конечно базовый класс остался и только для IOS платформы, но уже для того что бы можно было подружить между собой, эти два языка: старичка Objective C и амбициозного Swift. Давайте наверно посмотрим на него одним глазком

@implementation SwiftObject
+ (void)initialize {}

+ (instancetype)allocWithZone:(struct _NSZone *)zone {
  assert(zone == nullptr);
  return _allocHelper(self);
}

+ (instancetype)alloc {
  // we do not support "placement new" or zones,
  // so there is no need to call allocWithZone
  return _allocHelper(self);
}

+ (Class)class {
  return self;
}
- (Class)class {
  return (Class) _swift_getClassOfAllocated(self);
}
+ (Class)superclass {
  return (Class) _swift_getSuperclass((const ClassMetadata*) self);
}
- (Class)superclass {
  return (Class) _swift_getSuperclass(_swift_getClassOfAllocated(self));
}

+ (BOOL)isMemberOfClass:(Class)cls {
  return cls == (Class) _swift_getClassOfAllocated(self);
}

- (BOOL)isMemberOfClass:(Class)cls {
  return cls == (Class) _swift_getClassOfAllocated(self);
}

- (instancetype)self {
  return self;
}
- (BOOL)isProxy {
  return NO;
}

- (struct _NSZone *)zone {
  auto zone = malloc_zone_from_ptr(self);
  return (struct _NSZone *)(zone ? zone : malloc_default_zone());
}

- (void)doesNotRecognizeSelector: (SEL) sel {
  Class cls = (Class) _swift_getClassOfAllocated(self);
  fatalError(/* flags = */ 0,
             "Unrecognized selector %c[%s %s]n",
             class_isMetaClass(cls) ? '+' : '-', 
             class_getName(cls), sel_getName(sel));
}

- (id)retain {
  auto SELF = reinterpret_cast<HeapObject *>(self);
  swift_retain(SELF);
  return self;
}
- (void)release {
  auto SELF = reinterpret_cast<HeapObject *>(self);
  swift_release(SELF);
}
- (id)autorelease {
  return _objc_rootAutorelease(self);
}
- (NSUInteger)retainCount {
  return swift::swift_retainCount(reinterpret_cast<HeapObject *>(self));
}
- (BOOL)_isDeallocating {
  return swift_isDeallocating(reinterpret_cast<HeapObject *>(self));
}
- (BOOL)_tryRetain {
  return swift_tryRetain(reinterpret_cast<HeapObject*>(self)) != nullptr;
}
- (BOOL)allowsWeakReference {
  return !swift_isDeallocating(reinterpret_cast<HeapObject *>(self));
}
- (BOOL)retainWeakReference {
  return swift_tryRetain(reinterpret_cast<HeapObject*>(self)) != nullptr;
}

// Retaining the class object itself is a no-op.
+ (id)retain {
  return self;
}
+ (void)release {
  /* empty */
}
+ (id)autorelease {
  return self;
}
+ (NSUInteger)retainCount {
  return ULONG_MAX;
}
+ (BOOL)_isDeallocating {
  return NO;
}
+ (BOOL)_tryRetain {
  return YES;
}
+ (BOOL)allowsWeakReference {
  return YES;
}
+ (BOOL)retainWeakReference {
  return YES;
}

- (void)dealloc {
  swift_rootObjCDealloc(reinterpret_cast<HeapObject *>(self));
}

- (BOOL)isKindOfClass:(Class)someClass {
  for (auto isa = _swift_getClassOfAllocated(self); isa != nullptr;
       isa = _swift_getSuperclass(isa))
    if (isa == (const ClassMetadata*) someClass)
      return YES;

  return NO;
}

+ (BOOL)isSubclassOfClass:(Class)someClass {
  for (auto isa = (const ClassMetadata*) self; isa != nullptr;
       isa = _swift_getSuperclass(isa))
    if (isa == (const ClassMetadata*) someClass)
      return YES;

  return NO;
}

+ (BOOL)respondsToSelector:(SEL)sel {
  if (!sel) return NO;
  return class_respondsToSelector((Class) _swift_getClassOfAllocated(self), sel);
}

- (BOOL)respondsToSelector:(SEL)sel {
  if (!sel) return NO;
  return class_respondsToSelector((Class) _swift_getClassOfAllocated(self), sel);
}

+ (BOOL)instancesRespondToSelector:(SEL)sel {
  if (!sel) return NO;
  return class_respondsToSelector(self, sel);
}

- (BOOL)conformsToProtocol:(Protocol*)proto {
  if (!proto) return NO;
  auto selfClass = (Class) _swift_getClassOfAllocated(self);
  
  // Walk the superclass chain.
  while (selfClass) {
    if (class_conformsToProtocol(selfClass, proto))
      return YES;
    selfClass = class_getSuperclass(selfClass);
  }

  return NO;
}

+ (BOOL)conformsToProtocol:(Protocol*)proto {
  if (!proto) return NO;

  // Walk the superclass chain.
  Class selfClass = self;
  while (selfClass) {
    if (class_conformsToProtocol(selfClass, proto))
      return YES;
    selfClass = class_getSuperclass(selfClass);
  }
  
  return NO;
}

- (NSUInteger)hash {
  return (NSUInteger)self;
}

- (BOOL)isEqual:(id)object {
  return self == object;
}

- (id)performSelector:(SEL)aSelector {
  return ((id(*)(id, SEL))objc_msgSend)(self, aSelector);
}

- (id)performSelector:(SEL)aSelector withObject:(id)object {
  return ((id(*)(id, SEL, id))objc_msgSend)(self, aSelector, object);
}

- (id)performSelector:(SEL)aSelector withObject:(id)object1
                                     withObject:(id)object2 {
  return ((id(*)(id, SEL, id, id))objc_msgSend)(self, aSelector, object1,
                                                                 object2);
}

- (NSString *)description {
  return _getDescription(self);
}
- (NSString *)debugDescription {
  return _getDescription(self);
}

+ (NSString *)description {
  return _getClassDescription(self);
}
+ (NSString *)debugDescription {
  return _getClassDescription(self);
}

- (NSString *)_copyDescription {
  // The NSObject version of this pushes an autoreleasepool in case -description
  // autoreleases, but we're OK with leaking things if we're at the top level
  // of the main thread with no autorelease pool.
  return [[self description] retain];
}

- (CFTypeID)_cfTypeID {
  // Adopt the same CFTypeID as NSObject.
  static CFTypeID result;
  static dispatch_once_t predicate;
  dispatch_once_f(&predicate, &result, [](void *resultAddr) {
    id obj = [[NSObject alloc] init];
    *(CFTypeID*)resultAddr = [obj _cfTypeID];
    [obj release];
  });
  return result;
}

// Foundation collections expect these to be implemented.
- (BOOL)isNSArray__      { return NO; }
- (BOOL)isNSDictionary__ { return NO; }
- (BOOL)isNSSet__        { return NO; }
- (BOOL)isNSOrderedSet__ { return NO; }
- (BOOL)isNSNumber__     { return NO; }
- (BOOL)isNSData__       { return NO; }
- (BOOL)isNSDate__       { return NO; }
- (BOOL)isNSString__     { return NO; }
- (BOOL)isNSValue__      { return NO; }

@end

Та дамм! Что мы видим, а видим мы что класс SwiftObject имплементирует неформальный протокол NSObject, но реализация методов уже совсем иная. Вот теперь мы знаем врага в лицо, все классы Swift которые явно не наследуются от NSObject теперь неявно наследуются от SwiftObject класса. Сразу сделаю поправку, что это имеет место только для IOS платформы. На non-IOS платформах (Linux например) такого нет так как нет необходимости.

Как мы это узнали это уже другая история. Я думаю можно тоже немного рассказать. Как известно язык Swift лежит в открытом доступе на репозитории Apple. Никто не мешает скачать его и собрать самому из исходников, что собственно мы и сделали. Но мы пошли немножечко дальше. Всеми известный Xcode, начиная с версии 8 позволяет подсовывать свой toolchain. Чувствуете да, что это значит? Это значит что можно собрать Swift с дебаг информацией и подложить его в Xcode.

image

Мой коллега так и сделал, что позволило нам дебажить прямо из Xcode исходники Swift.

image

Мы немного отвлеклись, продолжим наши рассуждения. Уже очевидно можно сделать вывод, что метаданные которые генерировались в Objective C и которые генерируются в Swift имеют разную природу. Любой программист который долгое время писал на Objectice C и хоть немного ковырял рантайм, знает вот эту структуру

struct objc_class {
    Class isa  OBJC_ISA_AVAILABILITY;

#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;

Все мы знаем, что все объекты в конечном итоге в той или иной степени выглядят в виде этой структуры. Наш любимый с вами NSObject представляет собой абстракцию, которая уберегает программиста от прямого взаимодействия с этой структурой. Подробнее про нее можно почитать, есть куча статей написанных за время существования языка, даже на русском. Давайте вернемся к нашему Swift. Теперь для хранения метаданных появился специальный класс Metadata, который достаточно объемный и представляет собой основу для всех метаданных в Swift. Более подробное описание его структуры вынесу в отдельную статью. Еще момент, несмотря на то, что все объекты Swift умеют свою структуру метаданных, они все равно генерируют еще метаданные Objective C для совместимости. То есть каждый обьект Swift имеет два набора метаданных.

Давайте немного подведем итоги. Мы разобрались что NSObject уродлив, и не место ему в новом языке. Поэтому в Swift можно создавать классы не наследуя их ни от чего, но на самом деле для совместимости, они все равно наследуются от SwiftObject. Подружить класс SwiftObject и класс NSObject позволил неформальный протокол NSObject. Который позволяет кастить объект Swift в id и передавать в Objective C. Но было бы неплохо, что бы он там работал, поэтому каждый объект Swift генерирует помимо своих метаданных, еще метаданные Objective C. Вот как то так! Всем спасибо! Здоровья и хорошего настроения!

Автор: CocaineMaster

Источник


* - обязательные к заполнению поля


https://ajax.googleapis.com/ajax/libs/jquery/3.4.1/jquery.min.js