iOS Runtime介紹和使用

orilme發表於2019-04-08
  1. Runtime 簡介
  2. Runtime 訊息機制和相關函式
  3. Runtime 三次轉發流程
  4. Runtime 應用
  5. Runtime 面試題

1. Runtime 簡介

Objective-C 是一個動態語言,這意味著它不僅需要一個編譯器,也需要一個執行時系統來動態得建立類和物件、進行訊息傳遞和轉發。RuntimeObjective-C 物件導向和動態機制的基石,可以從系統層面解決一些設計或技術問題。它基本是用 C 和彙編寫的,屬於1個 C 語言庫,包含了很多底層的 C 語言 API ,如跟類、成員變數、方法相關的API。它的核心是 - 訊息傳遞 ( Messaging )。

  • 動態繫結(在執行時確定要呼叫的方法)
    動態繫結將呼叫方法的確定推遲到執行時。在編譯時,方法的呼叫並不和程式碼繫結在一起,只有在消實傳送出來之後,才確定被呼叫的程式碼。通過動態型別和動態繫結技術,您的程式碼每次執行都可以得到不同的結果。執行時因此負責確定訊息的接收者和被呼叫的方法。
    執行時的訊息分發機制為動態繫結提供支援。當您向一個動態型別確定了的物件傳送訊息時,執行環境系統會通過接收者的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_object

objc_object 結構體包含一個 isa 指標,型別為 isa_t 聯合體。根據 isa 指向物件所屬的類。isa 這裡還涉及到 tagged pointer 等概念。因為 isa_t 使用 union 實現,所以可能表示多種形態,既可以當成是指標,也可以儲存標誌位置。

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 的技術。

objc_class

Objective-C 類是由 Class 型別來表示的,它實際上是一個指向 objc_class 結構體的指標。

typedef struct objc_class *Class;
複製程式碼

objc/runtime.hobjc_class 結構體的定義如下:

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;
複製程式碼

結構體裡儲存了指向父類的指標、類的名字、版本、例項大小、例項變數列表、方法列表、快取、遵守的協議列表等。

物件在記憶體中的排布可以看成一個結構體,該結構體的大小並不能動態變化,所以無法在執行時動態給物件增加成員變數。相對的,物件的方法定義都儲存在類的可變區域中。如下圖所示為 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() 會去它的元類裡面去查詢能夠響應訊息的方法,如果找到了,然後對這個類物件執行方法呼叫。

元類(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 ,也就是它沒有超類
    iOS Runtime介紹和使用

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));
    }
}
複製程式碼

列印結果:

iOS Runtime介紹和使用

IMP

就是指向最終實現程式的記憶體地址的指標。

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

它就是一個函式指標,這是由編譯器生成的。當你發起一個 Objective-C 訊息之後,最終它會執行的那段程式碼,就是由這個函式指標指定的。而 IMP 這個函式指標就指向了這個方法的實現。
你會發現 IMP 指向的方法與 objc_msgSend 函式型別相同,引數都包含 idSEL 型別。每個方法名都對應一個 SEL 型別的方法選擇器,而每個例項物件中的 SEL 對應的方法實現肯定是唯一的,通過一組 idSEL 引數就能確定唯一的方法實現地址;反之亦然。

3. Runtime的三次轉發流程

進行一次傳送訊息會在相關的類物件中搜尋方法列表,如果找不到則會沿著繼承樹向上一直搜尋直到繼承樹根部(通常為 NSObject ),如果還是找不到並且訊息轉發都失敗了就回執行 doesNotRecognizeSelector: 方法報 unrecognized selector 錯。

iOS Runtime介紹和使用
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的本類,且 `KVO` 為 `NSKVONotifying_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

相關文章