Objective-C 運行時以及 Swift 的動態性

前言

  • 一、Objective-C 是一門基於運行時的編程語言,

這意味着全部方法、變量、類之間的link,都會推遲到應用實際運行的最後一刻纔會創建。
這將給開發人員極高的靈活性,由於咱們能夠修改這些link。git

  • 二、而不一樣的是,Swift 絕大多數時候是一門面向編譯時的語言。

所以在 Swift 當中,靈活性受到了限制,不過您會所以獲得更多的安全性。github

咱們一般所說的 Objective-C 「動態性」,每每都是指 KVO。雖然還有其他的函數,可是這些是最多見、最經常使用的。這也就是人們所說的,Swift 缺失的部分。編程

而 KVO是Foundation框架基於運行時實現的一個特性。所以本文先從Objective-C 的運行時 開始描述。安全

Objective-C 的運行時

本質上是一個庫。它負責了 「Objective」 這個部分,app

#import <objc/runtime.h>

它主要由 C 和彙編編寫而成,其實現了諸如類、對象、方法調度、協議等等這些東西。它是徹底開源的,而且開源了很長一段時間了。框架

  • 對象在 runtime.h 當中是這樣定義的:
typedef struct objc_class *Class;

struct objc_object {
    Class isa;
};

對象只與一個類創建引用關聯,也就是這個 isa 的意思所在。編程語言

  • 類的定義
struct objc_class {
    Class isa;
    Class super_class;
    const char *name;
    long version;
    long info;
    long instance_size;
    struct objc_ivar_list *ivars;
    struct objc_method_list **methodLists;
    struct objc_cache *cache;
    struct objc_protocol_list *protocols;
};

類當中一樣有 isa 這個值。除了 NSObject 這個類以外,super_class 的值永遠不會爲 nil,由於 Objective-C 當中的其他類都是以某種方式繼承自 NSObject 的.ide

更多的應該是關注變量列表 (ivars)、方法列表 (methodLists) 和這個協議列表 (protocols)。函數

這些就是咱們能在運行時修改和讀取的。能夠看到,對象其實本質上是一個很是簡單的結構體,類一樣也是。咱們能夠藉助運行時函數,從而在運行時動態建立類。單元測試

Creates a new class and metaclass.

BJC_EXPORT Class _Nullable
objc_allocateClassPair(Class _Nullable superclass, const char * _Nonnull name, 
                       size_t extraBytes) 
    OBJC_AVAILABLE(10.5, 2.0, 9.0, 1.0, 2.0);
Class myClass =
objc_allocateClassPair([NSObject
class], "MyClass", 0);

// 在這裏添加變量、方法和協議

objc_registerClassPair(myClass);
// 當類註冊以後,變量列表將會被鎖定

[[myClass alloc] init];

所建立的這個類和其他的 Objective-C 類毫無區別.

利用 Objective-C 運行時函數:allocateClassPair 建立Class。咱們爲其提供一個 isa,在本例當中咱們提供了 NSObject,而後爲其命名。第三個參數則是額外字節的定義,一般咱們都直接賦值 0 便可。隨後咱們就能夠添加變量、方法以及協議了,以後就註冊這個 ClassPair。註冊以後,咱們就沒法修改變量列表了,其他的內容仍然能夠修改。

「內省 (introspection)」機制:判別這個類能執行何種操做

[myObject isMemberOfClass:NSObject.class];

[myObject respondsToSelector:@selector(doStuff:)];

// isa == class

class_respondsToSelector(myObject.class, @selector(doStuff:));
  • 在運行時層面,isMemberOfClass 對比二者的 isa 是否相同。
  • respondsToSelector" 則封裝了一個 Objective-C 運行時函數:

respondsToSelector,其接受 Selector 和類爲參數。

BJC_EXPORT BOOL
class_respondsToSelector(Class _Nullable cls, SEL _Nonnull sel) 
    OBJC_AVAILABLE(10.5, 2.0, 9.0, 1.0, 2.0);

XCTest 的最簡單版本:藉助 Objective-C 的運行時機制實現

在編寫 XCTestCase 的時候,須要完成 setUp 和 tearDown 的設定,隨後才能編寫相關的 test 函數。
當測試運行的時候,系統會自行遍歷全部的測試函數,並自動運行。

unsigned int count;
Method *methods = class_copyMethodList(myObject.class,
&count);
//Ivar *list = class_copyIvarList(myObject.class,&count);

for(unsigned i = 0; i < count; i++) {
        SEL selector = method_getName(methods[i]);
        NSString *selectorString =
NSStringFromSelector(selector);
    if ([selectorString containsString:@"test"]) {
        [myObject performSelector:selector];
    }
}
free(methods);

變量和方法是由什麼組成

struct objc_ivar {
    char *ivar_name;
    char *ivar_type;
    int ivar_offset;
}

struct objc_method {
    SEL method_name;
    char *method_types;
    IMP method_imp;
}
  • 變量的組成包含了變量類型和變量名稱。偏移量 (offset) 則是內存管理方面的內容。
  • 方法還用編碼字符串來表示其類型。以後即是方法的實現,它使用了一種特定的表示方式,
  • 在運行時向對象當中添加方法
Method doStuff = class_getInstanceMethod(self.class, @selector(doStuff));

IMP doStuffImplementation = method_getImplementation(doStuff);

const char *types = method_getTypeEncoding(doStuff); //「v@:@"

class_addMethod(myClass.class, @selector(doStuff:), doStuffImplementation, types);

這個例子具體的方法實現部分咱們取了個巧,由於咱們使用了既有的 doStuff 方法,
所以可以很簡單地獲取其方法實現和方法類型。
不過咱們還能夠用其餘方法來完成:交換方法的實現。可使用運行時當中最著名的動態特性:方法混淆 (swizzling)。

  • objc_msgSend

咱們可使用 [self doStuff] 或者 [self performSelector:@selector(doStuff)] 來進行調用,實際上在運行時級別,它們都是藉助 objc_msgSend 向對象發送了一個消息。

[self doStuff];
[self performSelector:@selector(doStuff)];

objc_msgSend(self, @selector(message));

與類別Category相比運行時的setAssociatedObject、getAssociatedObject好處:向既有的類當中添加存儲屬性

擴展一個不是本身建立的類,想要向其中添加函數。Swift 的擴展與之很是類似。
類別的一個問題便在於,它沒法添加存儲屬性。您能夠添加一個計算屬性,可是存儲屬性是沒法添加的。

運行時的另外一個特性即是:
咱們能夠藉助 setAssociatedObject 和 getAssociatedObject 這兩個函數,向既有的類當中添加存儲屬性。

@implementation NSObject (AssociatedObject)
@dynamic associatedObject;

- (void)setAssociatedObject:(id)object {
    objc_setAssociatedObject(self,
@selector(associatedObject), object,
OBJC_ASSOCIATION_RETAIN_NONATOMIC);
}

- (id)associatedObject {
    return objc_getAssociatedObject(self,
@selector(associatedObject));
}

方法轉發:崩潰以前會預留幾個步驟

可是若是調用方法所在的對象爲 nil 的時候,咱們就會獲得一個異常,應用便會崩潰。但事實證實,在崩潰以前會預留幾個步驟,從而容許咱們對某個不存在的函數進行一些操做。

// 1
+(BOOL)resolveInstanceMethod:(SEL)sel{
    // 添加實例方法並返回 YES 的一次機會,它隨後會再次嘗試發送消息
}

// 2
- (id)forwardingTargetForSelector:(SEL)aSelector{
    // 返回能夠處理 Selector 的對象
}

// 3
- (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector{
    // 您須要實現它來建立 NSInvocation
}

- (void)forwardInvocation:(NSInvocation *)invocation {
    // 在您所選擇的目標上調用 Selector
    [invocation invokeWithTarget:target];
}

ps:試圖橋接兩個不一樣的框架的時候,這個功能便很是有用

  • 第一步

當調用了某個不存在的方法時,運行時首先會調用一個名爲 resolveInstanceMethod 的類方法,
若是所調用的方法是類方法的話,則爲調用 resolveClassMethod。

這時候咱們便有機會來添加方法了,即上面提到的利用運行時動態添加方法

  • 第2步

若是不想建立新方法的話,第一步返回了NO,
還有 forwardingTargetForSelector。能夠直接返回須要調用方法的目標對象便可,以後這個對象就會調用 Selector。

第三步驟:forwardInvocation

略爲複雜的 forwardInvocation。
若是您須要這麼作,那麼還須要實現 methodSignatureForSelector。
全部的調用過程都被封裝到 NSInvocation 對象當中,以後你即可以使用特定的對象進行調用了。

swizzling

交換方法的實現

+ (void)load {
    static dispatch_once_t onceToken;
    dispatch_once(&onceToken, ^{
        Class class = [self class];

        SEL originalSelector = @selector(doSomething);
        SEL swizzledSelector = @selector(mo_doSomething);

        Method originalMethod = class_getInstanceMethod(class,
originalSelector);
        Method swizzledMethod = class_getInstanceMethod(class,
swizzledSelector);

        BOOL didAddMethod = class_addMethod(class, originalSelector,
method_getImplementation(swizzledMethod),
method_getTypeEncoding(swizzledMethod));

        if (didAddMethod) {
            class_replaceMethod(class,
                                swizzledSelector,
                                method_getImplementation(originalMethod),
                                method_getTypeEncoding(originalMethod));
        } else {
            method_exchangeImplementations(originalMethod, swizzledMethod);
        }
    });
}

2種實現方式:
一、class_addMethod originalSelector 的時候使用swizzledMethod的IMP、TYP
以後進行class_replaceMethod,來達到交換的目的。
即先換掉originalSelector的IMP,再換掉swizzledMethod的IMP,達到exchange 的目的

二、直接使用exchange 方法

  • 小結

當類加載以後,會調用一個名爲 load 的類函數。因爲咱們只打算混淆一次,所以咱們須要使用 dispatch_once。接着咱們即可以獲得該方法,而後使用 class_replaceMethod 或者 method_exchangeImplementations 來替換方法。
之因此想要混淆,是由於它能夠用於日誌記錄和 Mock 測試。例如上報用戶打開的界面所在VC的名稱,就可使用swizzling 統一處理

運行時層面的上一層,Foundation 框架

Foundation 框架實現了基於運行時的一個特性:
鍵值編碼 (key-value-coding, KVC) 以及鍵值觀察 (key-value observing, KVO)。

KVC 和 KVO 容許咱們將 UI 和數據進行綁定。這也是 Rx 以及其餘響應式框架實現的基礎。

ps:並且MVVM的實現又能夠藉助「V-VM」第三方綁定框架進行實現

  • KVC 的工做方式
@property (nonatomic, strong) NSNumber *number;

[myClass valueForKey:@"number"];
[myClass setValue:@(4) forKey:@"number"];

能夠將屬性名稱做爲鍵,來獲取屬性值或者設置屬性值.

  • KVO,能夠對狀態的變化進行註冊
[myClass addObserver:self
    forKeyPath:@"number"
    options:NSKeyValueObservingOptionInitial | NSKeyValueObservingOptionNew
    context:nil];

- (void)observeValueForKeyPath:(NSString *)keyPath
                      ofObject:(id)object
                        change:(NSDictionary<NSKeyValueChangeKey,id> *)change
                       context:(void *)context{
    // Respond to observation.
}

在觀察的值發生變動以後,KVO 會調用此方法當即通知觀察者。經過這個方法,咱們即可以按需更新 UI。

咱們一般所說的 Objective-C 「動態性」,每每都是指 KVO。雖然還有其他的函數,可是這些是最多見、最經常使用的。這也就是人們所說的,Swift 缺失的部分。

第二大內容:Swift

Swift 是一種強類型語言。類型靜態,也就是說 Swift 的默認類型是很是安全的。
若是須要的話,不安全類型也是存在的,Swift 中的動態性能夠經過 Objective-C 運行時來得到。

可是 Swift 開源並遷移到 Linux 以後,因爲 Linux 上的 Swift 並不提供 Objective-C 運行時,事情就。。

  • @objc 和 @dynamic

Swift 當中存在有這兩個修飾符 @objc 和 @dynamic,此外咱們一樣還能夠訪問 NSObject。@objc 將您的 Swift API 暴露給 Objective-C 運行時,可是它仍然不能保證編譯器會嘗試對其進行優化。

若是您真的想使用動態功能的話,就須要使用 @dynamic。(一旦您使用了 @dynamic 修飾符以後,就不須要添加 @objc 了,由於它已經隱含在其中。)

Swift 當中的動態特性

  • 方法轉發
// 1
override class func resolveInstanceMethod(_ sel: Selector!)
-> Bool {
    // 添加實例方法並返回 true 的一次機會,它隨後會再次嘗試發送消息
}

// 2
override func forwardingTarget(for aSelector: Selector!) ->
Any? {
    // 返回能夠處理 Selector 的對象
}

// 3 - Swift 不支持 NSInvocation
  • 方法混淆

load 在 Swift 再也不會被調用,所以咱們須要在 initialize 中進行混淆。
在 Objective-C 當中,咱們使用 dispatch_once,可是自 Swift 3 以後,dispatch_once 便不復存在於 Swift 當中了。事情變得略爲複雜。

  • 內省
if self is MyClass {
    // YAY
}

let myString = "myString";
let mirror = Mirror(reflecting: myString)
print(mirror.subjectType) // 「String"
let string = String(reflecting: type(of:
myString)) // Swift.String

// No native method introspection

is 替代了 isMemberOfClass

  • 小結
  • 沒法自動遍歷全部的函數

若是打算爲 Linux 編寫單元測試的時候,就沒法自動遍歷全部的函數。您必須實現 static var allTests,而後手動列出全部的測試函數。這很。。。。

  • KVO 和 KVC 在 Swift 被極大地削弱了。

KVO 的魅力在於,您能夠在不是本身所建立的類當中使用它,也能夠只對您想要監聽變化的類使用。所觀察的對象必需要繼承自 NSObject,而且使用一個 Objective-C 類型。所觀察的變量必需要生命爲 dynamic。致使你必需要對想要觀察的事務瞭如指掌。
只能使用基於協議來觀察對象,語言自身是沒有原生的解決方案的。或者使用一些符合 Swift 風格的方法來暴露一些運行時函數的 ObjectiveKit 的開源庫

相關文章
相關標籤/搜索