Трохи про Swift runtime або куди зник NSObject

Привіт друзі! Я з тих, кому нудно просто смикати за ниточки, що стирчать з чорної коробки, хочеться побачити все своїми очима, як воно працює під капотом. Ми поговоримо з вами про рантайм, так-так рантайм. Для наших дослідів розглянемо старого доброго дідуся 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. Не буду довго мучити, погнали!
<habracut/>
@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 {
NO return;
}

- (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 {
NO return;
}
+ (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;

NO return;
}

+ (BOOL)isSubclassOfClass:(Class)someClass {
for (auto isa = (const ClassMetadata*) самоврядування; isa != nullptr;
isa = _swift_getSuperclass(isa))
if (isa == (const ClassMetadata*) someClass)
return YES;

NO return;
}

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

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

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

- (BOOL)conformsToProtocol:(Protocol*)proto {
if (!proto) NO return;
auto selfClass = (Class) _swift_getClassOfAllocated(self);

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

NO return;
}

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

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

NO return;
}

- (NSUInteger)hash {
return (NSUInteger)самоврядування;
}

- (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 if things 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__ { NO return; }
- (BOOL)isNSDictionary__ { NO return; }
- (BOOL)isNSSet__ { NO return; }
- (BOOL)isNSOrderedSet__ { NO return; }
- (BOOL)isNSNumber__ { NO return; }
- (BOOL)isNSData__ { NO return; }
- (BOOL)isNSDate__ { NO return; }
- (BOOL)isNSString__ { NO return; }
- (BOOL)isNSValue__ { NO return; }

@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. Ось як то так! Всім спасибі! Здоров'я і гарного настрою!
Джерело: Хабрахабр

0 коментарів

Тільки зареєстровані та авторизовані користувачі можуть залишати коментарі.