iOS Runtime介紹和使用

  1. Runtime 簡介
  2. Runtime 消息機制和相關函數
  3. Runtime 三次轉發流程
  4. Runtime 應用
  5. Runtime 面試題

1. Runtime 簡介

Objective-C 是一個動態語言,這意味着它不只須要一個編譯器,也須要一個運行時系統來動態得建立類和對象、進行消息傳遞和轉發。RuntimeObjective-C 面向對象和動態機制的基石,能夠從系統層面解決一些設計或技術問題。它基本是用 C 和彙編寫的,屬於1個 C 語言庫,包含了不少底層的 C 語言 API ,如跟類、成員變量、方法相關的API。它的核心是 - 消息傳遞 ( Messaging )。html

  • 動態綁定(在運行時肯定要調用的方法)
    動態綁定將調用方法的肯定推遲到運行時。在編譯時,方法的調用並不和代碼綁定在一塊兒,只有在消實發送出來以後,才肯定被調用的代碼。經過動態類型和動態綁定技術,您的代碼每次執行均可以獲得不一樣的結果。運行時所以負責肯定消息的接收者和被調用的方法。
    運行時的消息分發機制爲動態綁定提供支持。當您向一個動態類型肯定了的對象發送消息時,運行環境系統會經過接收者的isa指針定位對象的類,並以此爲起點肯定被調用的方法,方法和消息是動態綁定的。
  • Runtime 交互
    Objective-C 從三種不一樣的層級上與 Runtime 系統進行交互:
    • Objective-C 源代碼
    • Foundation 框架的 NSObject 類定義的方法
    • runtime 函數的直接調用
  • NSProxy
    Cocoa 中大多數類都繼承於 NSObject 類,也就天然繼承了它的方法。最特殊的例外是 NSProxy ,它是個抽象超類,它實現了一些消息轉發有關的方法,能夠經過繼承它來實現一個其餘類的替身類或是虛擬出一個不存在的類。

2. Runtime 消息機制和相關函數

  • Runtime 詳細消息發送步驟:
    • 檢測這個 selector 是否是要忽略的。好比 Mac OS X 開發,有了垃圾回收就不理會 retain , release 這些函數了。
    • 檢測這個 target 是否是 nil 對象。Objective-C 的特性是容許對一個 nil 對象執行任何一個方法不會 Crash ,由於會被忽略掉。
    • 若是上面兩個都過了,那就開始查找這個類的 IMP ,先從 cache 裏面找,完了找獲得就跳到對應的函數去執行。
    • 若是 cache 找不到就找一下方法分發表。
    • 若是分發表找不到就到超類的分發表去找,一直找,直到找到 NSObject 類爲止。
    • 若是還找不到就要開始進入動態方法解析了。
    • 若是仍是找不到而且消息轉發都失敗了就回執行 doesNotRecognizeSelector: 方法報 unrecognized selector 錯。
  • 舉例:
    一個對象的方法像這樣[obj eat],編譯器轉成消息發送objc_msgSend(obj, eat)Runtime時執行的流程是這樣的:
    1. 經過 objisa 指針找到它的 class
    2. classmethod listeat
    3. 若是 class 中沒找到 eat,繼續往它的 superclass 中找,一旦找到 eat 這個函數,就去執行它的實現IMP
  • 頭文件
    • <objc/runtime.h>
    • <objc/message.h>
  • 消息傳遞用到的一些概念:
    實例 objc_object
    類對象 objc_class
    元類 Meta Class
    Method objc_method
    SEL objc_selector
    類緩存 objc_cache
    Category objc_category
    IMP

objc_msg

id objc_msgSend ( id self, SEL op, ... );
複製代碼
  • id
    objc_msgSend 第一個參數類型爲id,是一個指向類實例的指針
    typedef struct objc_object *id;
    複製代碼
  • SEL(objc_selector)
    objc_msgSend 第二個參數類型爲SEL,它是 selectorObjective-C 中的表示類型( Swift 中是 Selector 類)。selector 是方法選擇器,能夠理解爲區分方法的 ID,而這個 ID 的數據結構是SEL。能夠看到selectorSEL的一個實例
    typedef struct objc_selector *SEL;
    複製代碼
    @property SEL selector;
    複製代碼
    其實它就是個映射到方法的C字符串,你能夠用 Objc 編譯器命令 @selector() 或者 Runtime 系統的 sel_registerName 函數來得到一個 SEL 類型的方法選擇器。
    注意:寫 C 代碼的時候,常常會用到函數重載,就是函數名相同,參數不一樣,可是這在Objc中是行不通的,由於selector只記了 methodname ,沒有參數,因此無法區分不一樣的 method
  • 舉例
    OC: [[Person alloc] init]
    Runtime: objc_msgSend(objc_msgSend("Person" , "alloc"), "init")

實例(objc_object)

objc_msgSend 第一個參數類型爲 id 指向類實例的指針,即 objc_objectios

objc_object 結構體包含一個 isa 指針,類型爲 isa_t 聯合體。根據 isa 指向對象所屬的類。isa 這裏還涉及到 tagged pointer 等概念。由於 isa_t 使用 union 實現,因此可能表示多種形態,既能夠當成是指針,也能夠存儲標誌位置。git

struct objc_object {
private:
    isa_t isa;

public:

    // ISA() assumes this is NOT a tagged pointer object
    Class ISA();

    // getIsa() allows this to be a tagged pointer object
    Class getIsa();
    ... 此處省略其餘方法聲明
}
複製代碼

注意: isa 指針不老是指向實例對象所屬的類,不能依靠它來肯定類型,而是應該用 class 方法來肯定實例對象的類。由於 KVO 的實現機理就是將被觀察對象的 isa 指針指向一箇中間類而不是真實的類,這是一種叫作 isa-swizzling 的技術。github

objc_class

Objective-C 類是由 Class 類型來表示的,它其實是一個指向 objc_class 結構體的指針。面試

typedef struct objc_class *Class;
複製代碼

objc/runtime.hobjc_class 結構體的定義以下:objective-c

struct objc_class {
    Class _Nonnull isa  OBJC_ISA_AVAILABILITY;

#if !__OBJC2__
    Class _Nullable super_class                         OBJC2_UNAVAILABLE;
    const char * _Nonnull name                          OBJC2_UNAVAILABLE;
    long version                                        OBJC2_UNAVAILABLE;
    long info                                           OBJC2_UNAVAILABLE;
    long instance_size                                  OBJC2_UNAVAILABLE;
    struct objc_ivar_list * _Nullable ivars             OBJC2_UNAVAILABLE;
    struct objc_method_list * _Nullable * _Nullable methodLists OBJC2_UNAVAILABLE;
    struct objc_cache * _Nonnull cache                  OBJC2_UNAVAILABLE;
    struct objc_protocol_list * _Nullable protocols     OBJC2_UNAVAILABLE;
#endif

} OBJC2_UNAVAILABLE;
複製代碼

結構體裏保存了指向父類的指針、類的名字、版本、實例大小、實例變量列表、方法列表、緩存、遵照的協議列表等。json

對象在內存中的排布能夠當作一個結構體,該結構體的大小並不能動態變化,因此沒法在運行時動態給對象增長成員變量。相對的,對象的方法定義都保存在類的可變區域中。以下圖所示爲 Class 的描述信息,其中 methodList 爲可訪問類中定義的方法的指針的指針,經過修改該指針所指向的指針的值,咱們能夠實現爲類動態增長方法實現。數組

objc_class 繼承於 objc_object,也就是說一個 Objective-C 類自己同時也是一個對象,咱們稱之爲類對象,類對象就是一個結構體 struct objc_class ,這個結構體存放的數據稱爲元數據。爲了處理類和對象的關係,runtime 庫建立了一種叫作元類 (Meta Class) 的東西,類對象所屬類型就叫作元類,它用來表述類對象自己所具有的元數據。類方法就定義於此處,由於這些方法能夠理解成類對象的實例方法。每一個類僅有一個類對象,而每一個類對象僅有一個與之相關的元類。緩存

當你發出一個相似 [NSObject alloc] 的消息時,你事實上是把這個消息發給了一個類對象 (Class Object) ,這個類對象必須是一個元類的實例,而這個元類同時也是一個根元類 (root meta class) 的實例。全部的元類最終都指向根元類爲其超類。全部的元類的方法列表都有可以響應消息的類方法。因此當 [NSObject alloc] 這條消息發給類對象的時候,objc_msgSend() 會去它的元類裏面去查找可以響應消息的方法,若是找到了,而後對這個類對象執行方法調用。bash

元類(Meta Class)

元類(Meta Class)是一個類對象的類。 在上面咱們提到,全部的類自身也是一個對象,咱們能夠向這個對象發送消息(即調用類方法)。 爲了調用類方法,這個類的 isa 指針必須指向一個包含這些類方法的一個 objc_class 結構體,這就引出了 meta-class 的概念。

類對象中的元數據存儲的都是如何建立一個實例的相關信息,那麼類對象和類方法應該從哪裏建立呢? 就是從 isa 指針指向的結構體建立,類對象的 isa 指針指向的咱們稱之爲元類(metaclass),元類中保存了建立類對象以及類方法所需的全部信息。

  1. 每一個 Class 都有一個 isa 指針指向一個惟一的 Meta Class
  2. 每個 Meta Classisa 指針都指向最上層的 Meta Class(圖中的 NSObjectMeta Class
  3. 最上層的 Meta Classisa 指針指向本身,造成一個迴路
  4. 每個 Meta Classsuper_class 指針指向它本來 Classsuper_classMeta Class 。可是最上層的 Meta Classsuper_class 指向 NSObject Class 自己
  5. 最上層的 NSObject Classsuper_classnil ,也就是它沒有超類

Method(objc_method)

objc/runtime.h :

typedef struct objc_method *Method;
struct objc_method {
    SEL method_name                         OBJC2_UNAVAILABLE;
    char *method_types                      OBJC2_UNAVAILABLE;
    IMP method_imp                          OBJC2_UNAVAILABLE;
}
複製代碼
  • objc_method 結構體的內容:
    SEL method_name : 方法名,相同名字的方法即便在不一樣類中定義,它們的方法選擇器也相同
    char *method_types : 方法類型,是個char指針,其實存儲着方法的參數類型和返回值類型
    IMP method_imp : 方法實現,本質上是一個函數指針

iOSRuntime 中,Method 經過 selectorIMP 兩個屬性,實現了快速查詢方法及實現,相對提升了性能,又保持了靈活性

類緩存(objc_cache)

cache 爲方法調用的性能進行優化。每一個消息都須要遍歷一次 isa 指向的類的方法列表(objc_method_list),這樣效率過低了。Runtime 系統會把被調用的方法存到 cache 中( method_name 做爲keymethod_imp 做爲value)。下次查找的時候會優先在 cache 中查找,效率更高。
objc_cache 是存在 objc_class 結構體中的。

cache_t_buckets_mask_occupied:

struct cache_t {
    struct bucket_t *_buckets;
    mask_t _mask;
    mask_t _occupied;
    ... 省略其餘方法
}
複製代碼

bucket_t 中存儲了 指針IMP 的鍵值對:

struct bucket_t {
private:
    cache_key_t _key;
    IMP _imp;

public:
    inline cache_key_t key() const { return _key; }
    inline IMP imp() const { return (IMP)_imp; }
    inline void setKey(cache_key_t newKey) { _key = newKey; }
    inline void setImp(IMP newImp) { _imp = newImp; }

    void set(cache_key_t newKey, IMP newImp);
};
複製代碼

Category(objc_category)

Category 爲現有的類提供了拓展性,它是 category_t 一個指向分類的結構體的指針。

typedef struct category_t *Category;
複製代碼
struct category_t { 
    const char *name; 
    classref_t cls; 
    struct method_list_t *instanceMethods; 
    struct method_list_t *classMethods;
    struct protocol_list_t *protocols;
    struct property_list_t *instanceProperties;
};
複製代碼
name:是指 class_name 而不是 category_name。
cls:要擴展的類對象,編譯期間是不會定義的,而是在Runtime階段經過name對 應到對應的類對象。
instanceMethods:category中全部給類添加的實例方法的列表。
classMethods:category中全部添加的類方法的列表。
protocols:category實現的全部協議的列表。
instanceProperties:表示Category裏全部的properties,這就是咱們能夠經過objc_setAssociatedObject和objc_getAssociatedObject增長實例變量的緣由,不過這個和通常的實例變量是不同的。
複製代碼

從上邊category_t的結構體中能夠看出,分類中能夠添加實例方法,類方法,甚至能夠實現協議,添加屬性,不能夠添加成員變量。

Ivar

Ivar 是一種表明類中實例變量的類型。

typedef struct ivar_t *Ivar;
複製代碼

ivar_t

struct ivar_t {
    int32_t *offset;
    const char *name;
    const char *type;
    // alignment is sometimes -1; use alignment() instead
    uint32_t alignment_raw;
    uint32_t size;

    uint32_t alignment() const {
        if (alignment_raw == ~(uint32_t)0) return 1U << WORD_SHIFT;
        return 1 << alignment_raw;
    }
};
複製代碼

class_copyIvarList 函數獲取的不只有實例變量,還有屬性。但會在本來的屬性名前加上一個下劃線。

objc_property_t

@property 標記了類中的屬性,它是一個指向objc_property結構體的指針:

typedef struct property_t *objc_property_t;
複製代碼

能夠經過 class_copyPropertyListprotocol_copyPropertyList 方法來獲取類和協議中的屬性:

objc_property_t *class_copyPropertyList(Class cls, unsigned int *outCount)
objc_property_t *protocol_copyPropertyList(Protocol *proto, unsigned int *outCount)
複製代碼

返回類型爲指向指針的指針,由於屬性列表是個數組,每一個元素內容都是一個 objc_property_t 指針,而這兩個函數返回的值是指向這個數組的指針。

class_copyIvarListclass_copyPropertyList 對比:

- (void)runtimeGetPropertyList {
    id RuntimeExploreInfo = objc_getClass("RuntimeExploreInfo");
    unsigned int outCount, i;
    objc_property_t *properties = class_copyPropertyList(RuntimeExploreInfo, &outCount);
    for (i = 0; i < outCount; i++) {
        objc_property_t property = properties[i];
        fprintf(stdout, "runtimeGetPropertyList---%s %s\n", property_getName(property), property_getAttributes(property));
    }
}

- (void)runtimeGetIvarList {
    id RuntimeExploreInfo = objc_getClass("RuntimeExploreInfo");
    unsigned int numIvars = 0;
    Ivar *ivars = class_copyIvarList(RuntimeExploreInfo, &numIvars);
    for(int i = 0; i < numIvars; i++) {
        Ivar thisIvar = ivars[i];
        const char *type = ivar_getTypeEncoding(thisIvar);
        NSString *stringType =  [NSString stringWithCString:type encoding:NSUTF8StringEncoding];
        if (![stringType hasPrefix:@"@"]) {
            continue;
        }
        fprintf(stdout, "runtimeGetIvarList---%s\n", ivar_getName(thisIvar));
    }
}
複製代碼

打印結果:

IMP

就是指向最終實現程序的內存地址的指針。

typedef void (*IMP)(void /* id, SEL, ... */ );
複製代碼

它就是一個函數指針,這是由編譯器生成的。當你發起一個 Objective-C 消息以後,最終它會執行的那段代碼,就是由這個函數指針指定的。而 IMP 這個函數指針就指向了這個方法的實現。
你會發現 IMP 指向的方法與 objc_msgSend 函數類型相同,參數都包含 idSEL 類型。每一個方法名都對應一個 SEL 類型的方法選擇器,而每一個實例對象中的 SEL 對應的方法實現確定是惟一的,經過一組 idSEL 參數就能肯定惟一的方法實現地址;反之亦然。

3. Runtime的三次轉發流程

進行一次發送消息會在相關的類對象中搜索方法列表,若是找不到則會沿着繼承樹向上一直搜索直到繼承樹根部(一般爲 NSObject ),若是仍是找不到而且消息轉發都失敗了就回執行 doesNotRecognizeSelector: 方法報 unrecognized selector 錯。

Runtime的三次轉發流程:

  1. 動態方法解析: +resolveInstanceMethod:, +resolveClassMethod:
  2. 消息轉發: forwardingTargetForSelector
  3. 重定向: methodSignatureForSelector:, forwardInvocation:

動態方法解析

Objective-C 運行時會調用 +resolveInstanceMethod:或者 +resolveClassMethod:,讓你有機會提供一個函數實現。若是你添加了函數並返回YES, 那運行時系統就會從新啓動一次消息發送的過程。

- (void)viewDidLoad {
        [super viewDidLoad];
        // Do any additional setup after loading the view, typically from a nib.
        //執行foo函數
        [self performSelector:@selector(foo:)];
    }
    
    + (BOOL)resolveInstanceMethod:(SEL)sel {
        if (sel == @selector(foo:)) {//若是是執行foo函數,就動態解析,指定新的IMP
            class_addMethod([self class], sel, (IMP)fooMethod, "v@:");
            return YES;
        }
        return [super resolveInstanceMethod:sel];
    }
    
    void fooMethod(id obj, SEL _cmd) {
        NSLog(@"Doing foo");//新的foo函數
    }
複製代碼

若是resolve方法返回 NO ,運行時就會移到下一步 :forwardingTargetForSelector

消息轉發

若是目標對象實現了 -forwardingTargetForSelector:Runtime 這時就會調用這個方法,給你把這個消息轉發給其餘對象的機會。
Controller :

- (void)viewDidLoad {
    [super viewDidLoad];
    // Do any additional setup after loading the view, typically from a nib.
    [self performSelector:@selector(runtimeMessageTest)];
}

+ (BOOL)resolveInstanceMethod:(SEL)sel {
    return YES; // 返回YES,進入下一步轉發
}

- (id)forwardingTargetForSelector:(SEL)aSelector {
    if (aSelector == @selector(runtimeMessageTest)) {
        return [RuntimeExploreInfo new]; // 返回RuntimeExploreInfo對象,讓RuntimeExploreInfon對象接收這個消息
    }
    
    return [super forwardingTargetForSelector:aSelector];
}
複製代碼

RuntimeExploreInfo :

#import "RuntimeExploreInfo.h"
    
    @implementation RuntimeExploreInfo
    
    - (void)runtimeMessageTest {
        NSLog(@"runtimeMessageTest---");
    }
    
    @end
複製代碼

經過 forwardingTargetForSelector 把當前 Controller 的方法轉發給了 RuntimeExploreInfo 去執行。

重定向

若是在上一步還不能處理未知消息,則惟一能作的就是啓用完整的消息轉發機制了。 首先它會發送 -methodSignatureForSelector: 消息得到函數的參數和返回值類型。若是 -methodSignatureForSelector: 返回 nilRuntime 則會發出 -doesNotRecognizeSelector: 消息,程序這時也就掛掉了。若是返回了一個函數簽名,Runtime 就會建立一個 NSInvocation 對象併發送 -forwardInvocation: 消息給目標對象。

- (void)viewDidLoad {
        [super viewDidLoad];
        // Do any additional setup after loading the view, typically from a nib.
    
        [self performSelector:@selector(runtimeMessageTest)];
    }
    
    + (BOOL)resolveInstanceMethod:(SEL)sel {
        return YES; // 返回YES,進入下一步轉發
    }
    
    - (id)forwardingTargetForSelector:(SEL)aSelector {
        return nil; // 返回nil,進入下一步轉發
    }
    
    - (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector {
        if ([NSStringFromSelector(aSelector) isEqualToString:@"runtimeMessageTest"]) {
            return [NSMethodSignature signatureWithObjCTypes:"v@:"]; // 簽名,進入forwardInvocation
        }
        
        return [super methodSignatureForSelector:aSelector];
    }
    
    - (void)forwardInvocation:(NSInvocation *)anInvocation {
        SEL sel = anInvocation.selector;
        
        RuntimeExploreInfo *p = [RuntimeExploreInfo new];
        if([p respondsToSelector:sel]) {
            [anInvocation invokeWithTarget:p];
        }else {
            [self doesNotRecognizeSelector:sel];
        }
    }
複製代碼

咱們實現了完整的轉發。經過簽名,Runtime 生成了一個對象 anInvocation ,發送給了 forwardInvocation ,咱們在 forwardInvocation 方法裏面讓 RuntimeExploreInfo 對象去執行了 runtimeMessageTest 函數。

4. Runtime 應用

  1. 關聯對象( Objective-C Associated Objects )給分類增長屬性
  2. 方法魔法( Method Swizzling )方法添加和替換
  3. KVO 實現
  4. 實現 NSCoding 的自動歸檔和自動解檔
  5. 實現字典和模型的自動轉換( MJExtensionYYModel )
  6. 用於封裝框架(想怎麼改就怎麼改)

關聯對象( Objective-C Associated Objects )給分類增長屬性

RuntimeExploreInfo+RuntimeAddProperty.h 添加了 phoneNum 屬性

#import "RuntimeExploreInfo+RuntimeAddProperty.h"
    #import "objc/runtime.h"
    
    @implementation RuntimeExploreInfo (RuntimeAddProperty)
    
    static char kPhoneNumKey;
    
    - (void)setPhoneNum:(NSString *)phoneNum {
        objc_setAssociatedObject(self, &kPhoneNumKey, phoneNum, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
    }
    
    - (id)phoneNum {
        return objc_getAssociatedObject(self, &kPhoneNumKey);
    }
    
    @end
複製代碼
- (void)runtimeAddProperty {
        RuntimeExploreInfo *test = [RuntimeExploreInfo new];
        test.phoneNum = @"12342424242";
        NSLog(@"RuntimeAddProperty---%@", test.phoneNum);
    }
複製代碼

方法魔法( Method Swizzling )方法添加和替換和 KVO 實現

  • 添加方法
    /**
     class_addMethod(Class  _Nullable __unsafe_unretained cls, SEL  _Nonnull name, IMP  _Nonnull imp, const char * _Nullable types)
     cls 被添加方法的類
     name 添加的方法的名稱的SEL
     imp 方法的實現。該函數必須至少要有兩個參數,self,_cmd
     類型編碼
     */
    class_addMethod([self class], sel, (IMP)fooMethod, "v@:");
    複製代碼
  • 替換方法
    class_replaceMethod 替換類方法的定義
    method_exchangeImplementations 交換兩個方法的實現
    method_setImplementation 設置一個方法的實現
    注意:class_replaceMethod 試圖替換一個不存在的方法時候,會調用 class_addMethod 爲該類增長一個新方法
    + (void)load {
        static dispatch_once_t onceToken;
        dispatch_once(&onceToken, ^{
            Class class = [self class];
            SEL originalSelector = @selector(viewDidLoad);
            SEL swizzledSelector = @selector(runtimeReplaceViewDidLoad);
            
            Method originalMethod = class_getInstanceMethod(class, originalSelector);
            Method swizzledMethod = class_getInstanceMethod(class, swizzledSelector);
            
            //judge the method named  swizzledMethod is already existed.
            BOOL didAddMethod = class_addMethod(class, originalSelector, method_getImplementation(swizzledMethod), method_getTypeEncoding(swizzledMethod));
            // if swizzledMethod is already existed.
            if (didAddMethod) {
                class_replaceMethod(class, swizzledSelector, method_getImplementation(originalMethod), method_getTypeEncoding(originalMethod));
            }else {
                method_exchangeImplementations(originalMethod, swizzledMethod);
            }
        });
    }
    
    - (void)runtimeReplaceViewDidLoad {
        NSLog(@"替換的方法");
        //[self runtimeReplaceViewDidLoad];
    }
    複製代碼
    swizzling應該只在 +load 中執行一次( dispatch_once )完成。在 Objective-C 的運行時中,每一個類有兩個方法都會自動調用。+load 是在一個類被初始裝載時調用,+initialize 是在應用第一次調用該類的類方法或實例方法前調用的。兩個方法都是可選的,而且只有在方法被實現的狀況下才會被調用。

KVO實現

Apple 使用了 isa-swizzling 來實現 KVO 。當觀察對象A時,KVO機制動態建立一個新的名爲:NSKVONotifying_A的新類,該類繼承自對象A的本類,且 KVONSKVONotifying_A 重寫觀察屬性的 setter 方法,setter 方法會負責在調用原 setter 方法以前和以後,通知全部觀察對象屬性值的更改狀況。

NSKVONotifying_A 類剖析

NSLog(@"self->isa:%@",self->isa);  
    NSLog(@"self class:%@",[self class]);  
複製代碼

在創建KVO監聽前,打印結果爲:

self->isa:A
    self class:A
複製代碼

在創建KVO監聽以後,打印結果爲:

self->isa:NSKVONotifying_A
    self class:A
複製代碼

子類setter方法剖析:
KVO 的鍵值觀察通知依賴於 NSObject 的兩個方法: willChangeValueForKey:didChangeValueForKey: ,在存取數值的先後分別調用 2 個方法: 被觀察屬性發生改變以前,willChangeValueForKey:被調用,通知系統該 keyPath 的屬性值即將變動;當改變發生後, didChangeValueForKey: 被調用,通知系統該keyPath 的屬性值已經變動;以後, observeValueForKey:ofObject:change:context:也會被調用。且重寫觀察屬性的 setter 方法這種繼承方式的注入是在運行時而不是編譯時實現的。
KVO 爲子類的觀察者屬性重寫調用存取方法的工做原理在代碼中至關於: - (void)setName:(NSString *)newName { [self willChangeValueForKey:@"name"]; //KVO 在調用存取方法以前總調用 [super setValue:newName forKey:@"name"]; //調用父類的存取方法 [self didChangeValueForKey:@"name"]; //KVO 在調用存取方法以後總調用 }

實現NSCoding的自動歸檔和自動解檔

原理描述:用 runtime 提供的函數遍歷 Model 自身全部屬性,並對屬性進行 encodedecode 操做。

核心方法:在Model的基類中重寫方法:

- (id)initWithCoder:(NSCoder *)aDecoder {
        if (self = [super init]) {
            unsigned int outCount;
            Ivar * ivars = class_copyIvarList([self class], &outCount);
            for (int i = 0; i < outCount; i ++) {
                Ivar ivar = ivars[i];
                NSString * key = [NSString stringWithUTF8String:ivar_getName(ivar)];
                [self setValue:[aDecoder decodeObjectForKey:key] forKey:key];
            }
        }
        return self;
    }
    
    - (void)encodeWithCoder:(NSCoder *)aCoder {
        unsigned int outCount;
        Ivar * ivars = class_copyIvarList([self class], &outCount);
        for (int i = 0; i < outCount; i ++) {
            Ivar ivar = ivars[i];
            NSString * key = [NSString stringWithUTF8String:ivar_getName(ivar)];
            [aCoder encodeObject:[self valueForKey:key] forKey:key];
        }
    }
複製代碼

實現字典和模型的自動轉換

原理描述:用runtime提供的函數遍歷Model自身全部屬性,若是屬性在json中有對應的值,則將其賦值。
核心方法:在NSObject的分類中添加方法

- (instancetype)initWithDict:(NSDictionary *)dict {
    
        if (self = [self init]) {
            //(1)獲取類的屬性及屬性對應的類型
            NSMutableArray * keys = [NSMutableArray array];
            NSMutableArray * attributes = [NSMutableArray array];
    
            unsigned int outCount;
            objc_property_t * properties = class_copyPropertyList([self class], &outCount);
            for (int i = 0; i < outCount; i ++) {
                objc_property_t property = properties[i];
                //經過property_getName函數得到屬性的名字
                NSString * propertyName = [NSString stringWithCString:property_getName(property) encoding:NSUTF8StringEncoding];
                [keys addObject:propertyName];
                //經過property_getAttributes函數能夠得到屬性的名字和@encode編碼
                NSString * propertyAttribute = [NSString stringWithCString:property_getAttributes(property) encoding:NSUTF8StringEncoding];
                [attributes addObject:propertyAttribute];
            }
            //當即釋放properties指向的內存
            free(properties);
    
            //(2)根據類型給屬性賦值
            for (NSString * key in keys) {
                if ([dict valueForKey:key] == nil) continue;
                [self setValue:[dict valueForKey:key] forKey:key];
            }
        }
        return self;
    
    }
複製代碼

5. Runtime 面試題

  • Self & Super

    @implementation Son : Father
    - (id)init
    {
        self = [super init];
        if (self)
        {
            NSLog(@"%@", NSStringFromClass([self class]));
            NSLog(@"%@", NSStringFromClass([super class]));
        }
        return self;
    }
    @end
    複製代碼

    答案:都輸出 Son
    解惑:這個題目主要是考察關於 objc 中對 selfsuper 的理解。

    self 是類的隱藏參數,指向當前調用方法的這個類的實例。而 super 是一個 Magic Keyword, 它本質是一個編譯器標示符,和 self 是指向的同一個消息接受者。上面的例子無論調用 [self class] 仍是 [super class] ,接受消息的對象都是當前 Son *xxx 這個對象。而不一樣的是,super 是告訴編譯器,調用 class 這個方法時,要去父類的方法,而不是本類裏的。

    當使用 self 調用方法時,會從當前類的方法列表中開始找,若是沒有,就從父類中再找;
    而當使用 super 時,則從父類的方法列表中開始找。而後調用父類的這個方法。

    當調用 [self class] 時,實際先調用的是 objc_msgSend 函數,第一個參數是 Son 當前的這個實例,而後在 Son 這個類裏面去找 - (Class)class 這個方法,沒有,去父類 Father 裏找,也沒有,最後在 NSObject 類中發現這個方法。而 - (Class)class的實現就是返回 self 的類別,故上述輸出結果爲 Son

    當調用 [super class] 時,會轉換成 objc_msgSendSuper 函數。第一步先構造 objc_super 結構體,結構體第一個成員就是 self 。第二個成員是 (id)class_getSuperclass(objc_getClass(「Son」)) , 實際該函數輸出結果爲 Father。第二步是去 Father 這個類裏去找 - (Class)class ,沒有,而後去 NSObject 類去找,找到了。最後內部是使用 objc_msgSend(objc_super->receiver, @selector(class)) 去調用,此時已經和 [self class] 調用相同了,故上述輸出結果仍然返回 Son

  • Object & Class & Meta Clas

    @interface Sark : NSObject
    @end
    @implementation Sark
    @end
    int main(int argc, const char * argv[]) {
        @autoreleasepool {
            BOOL res1 = [(id)[NSObject class] isKindOfClass:[NSObject class]];
            BOOL res2 = [(id)[NSObject class] isMemberOfClass:[NSObject class]];
            BOOL res3 = [(id)[Sark class] isKindOfClass:[Sark class]];
            BOOL res4 = [(id)[Sark class] isMemberOfClass:[Sark class]];
            NSLog(@"%d %d %d %d", res1, res2, res3, res4);
        }
        return 0;
    }
    複製代碼

    答案: 1 0 0 0
    咱們看到在 Objective-C 的設計哲學中,一切都是對象。Class在設計中自己也是一個對象。而這個 Class 對象的對應的類,咱們叫它 Meta Class 。即 Class 結構體中的 isa 指向的就是它的 Meta Class
    Meta Class 理解爲 一個 Class 對象的 Class 。簡單的說:
    當咱們發送一個消息給一個 NSObject 對象時,這條消息會在對象的類的方法列表裏查找;
    當咱們發送一個消息給一個類時,這條消息會在類的 Meta Class 的方法列表裏查找

  • 消息 和 Category

    @interface NSObject (Sark)
    + (void)foo;
    @end
    @implementation NSObject (Sark)
    - (void)foo
    {
        NSLog(@"IMP: -[NSObject(Sark) foo]");
    }
    @end
    int main(int argc, const char * argv[]) {
        @autoreleasepool {
            [NSObject foo];
            [[NSObject new] foo];
        }
        return 0;
    }
    複製代碼

    答案:
    IMP: -[NSObject(Sark) foo]
    IMP: -[NSObject(Sark) foo]
    解釋:

    1. objc runtime 加載完後,NSObjectSark Category 被加載。而 NSObjectSark Category 的頭文件 + (void)foo 並無實質參與到工做中,只是給編譯器進行靜態檢查,全部咱們編譯上述代碼會出現警告,提示咱們沒有實現 + (void)foo 方法。而在代碼編譯中,它已經被註釋掉了。
    2. 實際被加入到 Classmethod list 的方法是 - (void)foo ,它是一個實例方法,因此加入到當前類對象 NSObject 的方法列表中,而不是 NSObject Meta class 的方法列表中。
    3. 當執行 [NSObject foo] 時,咱們看下整個 objc_msgSend 的過程:
    • objc_msgSend 第一個參數是 (id)objc_getClass("NSObject") ,得到 NSObject Class 的對象。
    • 類方法在 Meta Class 的方法列表中找,咱們在 load Category 方法時加入的是 - (void)foo 實例方法,因此並不在 NSOBject Meta Class 的方法列表中
    • 繼續往 super class 中找,NSObject Meta Classsuper classNSObject 自己。因此,這個時候咱們可以找到 - (void)foo 這個方法。
      因此正常輸出結果。
    1. 當執行 [[NSObject new] foo] ,咱們看下整個 objc_msgSend 的過程:
      [NSObject new] 生成一個 NSObject 對象。直接在該對象的類( NSObject )的方法列表裏找。可以找到,因此正常輸出結果。
  • 成員變量與屬性

    @interface Sark : NSObject
    @property (nonatomic, copy) NSString *name;
    @end
    @implementation Sark
    - (void)speak
    {
        NSLog(@"my name is %@", self.name);
    }
    @end
    @interface Test : NSObject
    @end
    @implementation Test
    - (instancetype)init
    {
        self = [super init];
        if (self) {
            id cls = [Sark class];
            void *obj = &cls;
            [(__bridge id)obj speak];
        }
        return self;
    }
    @end
    int main(int argc, const char * argv[]) {
        @autoreleasepool {
            [[Test alloc] init];
        }
        return 0;
    }
    複製代碼

    答案: my name is

 

更多實用詳見 Demo Runtime文件夾下

 

參考文章

Objective-C Runtime
刨根問底Objective-C Runtime

相關文章
相關標籤/搜索