Runtime學習筆記整理

[toc]php

1、基本概念

Runtime是一套比較底層的純C語言API,包含了不少底層的C語言API。在咱們平時編寫的OC代碼中,程序運行時,其實最終都是轉成了Runtime的C語言代碼。Runtime是開源的,你能夠去這裏下載Runtime的源碼。html

實例方法被調用的過程分析

實例方法被調用的時候,會經過其持有的isa指針找到對應的類,而後在其中的class_data_bits_t中查找對應的方法。ios

*執行NSArray array = [[NSArray alloc] init];的流程:git

  • 一、[NSArray alloc]先被執行,因爲NSArray沒有+alloc方法,因此去分類NSObject中查找
  • 二、檢查NSArray是否能響應alloc方法,發現響應後,檢查NSArray類,開闢NSArray所需的內存空間,而後把isa指向NSArray。同時+alloc方法被添加到cache列表中。
  • 三、接着執行-init方法,若是NSArray不響應,則繼續去父類NSObject中查找。找到後同時加入到cache列表中。
  • 四、之後再使用[[NSArray alloc] init]初始化數組,直接從cache中獲取方法執行。

realizeClass方法的主要做用是對類進行第一次初始化(分配可讀寫數據空間、返回真正的類結構) 類在內存中的位置是編譯期肯定的,只要代碼不改變,類在內存中的位置就會不變github

  • ObjC 類中的屬性、方法還有遵循的協議等信息都保存在 class_rw_t
  • 當前類在編譯期就已經肯定的屬性、方法以及遵循的協議都保存在 class_ro_t
  • 類的方法、屬性以及協議在編譯期間存放到了「錯誤」的位置,直到 realizeClass 執行以後,才放到了 class_rw_t 指向的只讀區域 class_ro_t,這樣咱們便可以在運行時爲 class_rw_t 添加方法,也不會影響類的只讀結構。
  • class_ro_t 中的屬性在運行期間就不能改變了,再添加方法時,會修改 class_rw_t 中的 methods 列表,而不是 class_ro_t 中的 baseMethods

1、相關術語

一、id:表示Objective-C的任意對象類型

struct objc_object {
    Class isa;
} *id;
複製代碼

二、isa:實例的一個屬性,用來指向實例所屬的類

typedef struct objc_class *Class;
struct objc_class { 
    Class isa                                 OBJC_ISA_AVAILABILITY; 
}
複製代碼
  • 實例方法調用時,經過對象的 isa 在類中獲取方法的實現
  • 類方法調用時,經過類的 isa 在元類中獲取方法的實現

三、SEL:SEL又叫選擇器,是表示一個方法的selector的指針

typedef struct objc_selector *SEL;
複製代碼

四、Method:方法(方法名+方法類型+方法實現)

typedef struct objc_method *Method;
struct objc_method {
    SEL method_name         OBJC2_UNAVAILABLE;  // 方法名
    char *method_types      OBJC2_UNAVAILABLE;  // 方法類型,主要存儲着方法的參數類型和返回值類型
    IMP method_imp          OBJC2_UNAVAILABLE;  // 方法的實現,函數指針
}
複製代碼

class_copyMethodList(Class cls, unsigned int *outCount)可使用這個方法獲取某個類的成員方法列表。objective-c

五、IMP:函數指針,指向方法的實現,由編譯器生成,決定代碼最終在何處執行。

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

六、Ivar:實例變量

typedef struct objc_ivar *Ivar;
struct objc_ivar {
    char *ivar_name                   OBJC2_UNAVAILABLE; 
    char *ivar_type                   OBJC2_UNAVAILABLE; 
    int ivar_offset                   OBJC2_UNAVAILABLE; 
#ifdef __LP64__
    int space                         OBJC2_UNAVAILABLE;
#endif
}
複製代碼

class_copyIvarList(Class cls, unsigned int *outCount) 可使用這個方法獲取某個類的成員變量列表。sql

// ivar 的修飾信息存放在了 Class 的 Ivar Layout 中
struct class_ro_t {
    uint32_t flags;
    uint32_t instanceStart;
    uint32_t instanceSize;
#ifdef __LP64__
    uint32_t reserved;
#endif
    const uint8_t * ivarLayout;           // <- 記錄了哪些是 strong 的 ivar

    const char * name;
    const method_list_t * baseMethods;
    const protocol_list_t * baseProtocols;
    const ivar_list_t * ivars;

    const uint8_t * weakIvarLayout;     // <- 記錄了哪些是 weak 的 ivar
    const property_list_t *baseProperties;
};
複製代碼

七、objc_property_t:實例屬性 = Ivar + setter + getter

typedef struct objc_property *objc_property_t;
複製代碼

class_copyPropertyList(Class cls, unsigned int *outCount) 可使用這個方法獲取某個類的屬性列表。編程

八、objc_category

typedef struct objc_category *Category;

typedef struct objc_category {
    const char *name;                            // 類的名字
    classref_t cls;                              // 類
    struct method_list_t *instanceMethods;       // category中全部給類添加的實例方法的列表
    struct method_list_t *classMethods;          // category中全部添加的類方法的列表
    struct protocol_list_t *protocols;           // category實現的全部協議的列表
    struct property_list_t *instanceProperties;  // category中添加的全部屬性
};
複製代碼

九、Cache:緩存提升查找效率

typedef struct objc_cache *Cache
struct objc_cache {
    unsigned int mask                   OBJC2_UNAVAILABLE;
    unsigned int occupied               OBJC2_UNAVAILABLE;
    Method buckets[1]                   OBJC2_UNAVAILABLE;
};
複製代碼

每調用一次方法後,不會直接在isa指向的類的方法列表(methodLists)中遍歷查找可以響應消息的方法,由於這樣效率過低。它會把該方法緩存到cache列表中,下次的時候,就直接優先從cache列表中尋找,若是cache沒有,才從isa指向的類的方法列表(methodLists)中查找方法。提升效率。json

十、metaClass(元類):類對象所屬的類,類對象的isa指針指向元類

十一、根元類:全部的元類的基類,根元類的isa指針指向本身

2、全部實例、類以及元類(meta class)都繼承自一個基類,關係以下圖所示:

上圖中:superclass指針表明繼承關係,isa指針表明實例所屬的類。 類也是一個對象,它是另一個類的實例,這個就是「元類」,元類裏面保存了類方法的列表,類裏面保存了實例方法的列表。實例對象的isa指向類,類對象的isa指向元類,元類對象的isa指針指向一個「根元類」(root metaclass)。全部子類的元類都繼承父類的元類,換而言之,類對象和元類對象有着一樣的繼承關係。 注意:

  • 一、Class是一個指向objc_class結構體的指針,而id是一個指向objc_object結構體的指針,其中的isa是一個指向objc_class結構體的指針。其中的id就是咱們所說的對象,Class就是咱們所說的類。
  • 二、isa指針不老是指向實例對象所屬的類,不能依靠它來肯定類型,而是應該用isKindOfClass:方法來肯定實例對象的類。由於KVO的實現機制就是將被觀察對象的isa指針指向一箇中間類而不是真實的類。

3、類對象在runtime中的數據結構

typedef struct objc_class *Class;
struct objc_class {
    Class isa  OBJC_ISA_AVAILABILITY;
#if !__OBJC2__
    Class super_class                       OBJC2_UNAVAILABLE;  // 父類
    const char *name                        OBJC2_UNAVAILABLE;  // 類名
    long version                            OBJC2_UNAVAILABLE;  // 類的版本信息,默認爲0
    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;
複製代碼
  • 一、isa: 結構體的首個變量也是isa指針,這說明Class自己也是Objective-C中的對象。
  • 二、super_class: 結構體裏還有個變量是super_class,它定義了本類的超類。類對象所屬類型(isa指針所指向的類型)是另一個類,叫作「元類」。
  • 三、name:類名稱。
  • 四、version:類的版本信息。
  • 五、info:運行期使用的標誌位,好比0x1(CLS_CLASS)表示該類爲普通class,0x2(CLS_META)表示該類爲 metaclass。
  • 六、instance_size:實例大小,即內存所佔空間。
  • 七、ivars: 成員變量列表,類的成員變量都在ivars裏面。
  • 八、methodLists: 方法列表,根據標誌位的不一樣可能指向不一樣,好比可能指向實例方法列表,或者指向類方法列表。類的實例方法都在methodLists裏,類方法在元類的methodLists裏面。methodLists是一個指針的指針,經過修改該指針指向指針的值,就能夠動態的爲某一個類添加成員方法。這也就是Category實現的原理,同時也說明了Category只能夠爲對象添加成員方法,不能添加成員變量。
  • 九、cache: 方法緩存列表,objc_msgSend(下文詳解)每調用一次方法後,就會把該方法緩存到cache列表中,下次調用的時候,會優先從cache列表中尋找,若是cache沒有,才從methodLists中查找方法。提升效率。
  • 十、protocols:類須要遵照的協議。

4、運行時建立類,只須要三步:

  • 一、爲類和類的元類分配內存空間objc_allocateClassPair
  • 二、爲類添加方法和成員屬性class_addMethodclass_addIvarclass_addProperty
  • 三、註冊新建立的類objc_registerClassPair

注意:運行時只能添加屬性,不能添加成員變量,不然會打亂類的內存結構。segmentfault

參考博客

Objective-C中的Runtime

深刻解析 ObjC 中方法的結構

2、動態特性

[TOC]

@(runtime)[runTime, 溫故而知新]

轉自:iOS動態特性初研究(利用JSON動態建立類型和對象)

1.什麼是動態特性?

程序能夠訪問,檢測和修改它自己狀態或行爲的能力。用我本身的理解,這裏的狀態和行爲,理解成變量,屬性和方法,會更加形象一點。

2.與動態特性相關的概念,selector,IMP,Class

Class: 從語法形式上看,和UIButton,NSString同樣,是一種類型。

Class被定義爲一個指向objc_class的結構體指針。

它是指向對象的類結構體的指針,該類結構體含有一個指向其父類類結構的指針,訪類方法的鏈表,該類方法的緩存以及其餘必要信息。見下圖

除了靜態方法來建立對象,還可使用string來建立,NSClassFromString。

SEL:定義成一個指向objc_selector指針

運行時,會在方法鏈表中根據SEL查找具體的實現方法IMP。爲何不用函數指針直接調用,而加了一層SEL?個人理解,首先Object-C的類不能直接應用函數指針,這樣只能作一個@selector語法來取(本人在OC中寫過狀態機,用函數指針形式寫action,但一直報錯,只能用selector代替);其次,SEL還能夠配合動態方法來使用,例如NSSelectorFromString,performSelector,動態添加方法,並執行。

IMP:就是定義一個函數指針的形式

它包含一個接受消息的對象(self指針),調用方法SEL,以及若干參數,並返回一個id。

3. 舉例子,如何將JSON直接映射成對象,如何將對象直接映射成DB(coreData原理)

3.1定義該類的屬性,方法,生成對象。

用動態方法,得到該對象的屬性/變量列表(class_copyPropertyList/class_copyIvarList),遍歷得到每一個屬性的名稱(property_getName),而後將JSON轉換Dic,用key-value(setvalueForkey,valueForKey)方法,對對象進行賦值,取值操做。

此種方法,抽象出了公用的setter方法(用dictionary給對象賦值),可是缺點是,類型要事先定義。沒法動態生成類型。這種例子,網上不少,並且不明白爲何例子中都把property name和attribute值打印出來,至於怎麼用,半個字都沒提?

(上面是最長見的使用方式,有人問我可否不事先定義類型,而後利用JSON來建立類型呢?這個還把我問住了)後來查閱OC runtime guide,發現有動態添加變量的方法(class_addIvar),因而思路由此打開:

3.二、首先定義一個空的類

(沒有屬性,變量,方法),只有一個類名,而後運行時,給該類添加變量(當時沒有查到能夠動態添加屬性的方法,後來發現有,可是要到iOS4.3之後才行),隨後用給變量賦值。可是結果讓人失望,沒法動態添加變量。緣由是class_addIvar只能在動態建立類型的時候,添加變量,也就是「class_addIvar"This function may only be called after objc_allocateClassPair and beforeobjc_registerClassPair.Adding an instance variable to an existing class is notsupported」,而事先定義類是靜態建立的類,故沒法在runtime時添加變量objective-c-add-property-in-runtime

因而,只能放棄事先定義類的方式,轉而利用在動態建立類時(objc_allocateClassPair),添加變量 。而後用給變量賦值和取值的方式(object_setInstanceVariable,object_getIvar,注意,沒法用key-value的方式操做,這種方法只有靜態定義屬性後才行),但這種方式,就只能用純C的方式封裝,賦值,取值都要傳進obj參數,比較繁瑣,沒有面向對象那麼方便。

結論:3.2中的結論,若是編譯前定義類,那麼沒法用runtime添加變量,這種方法行不通;只有在runtime時,在objc_allocateClassPairobjc_registerClassPair之間用class_addIvar添加變量

3.三、後來查到有動態添加property的方法

(class_addProperty),在4.3以後。因而想到一種動態建立類型,而且能夠用OC語法的方式訪問變量。

首先,動態建立類型,添加變量(這個很重要,由於當咱們訪問property時,其實是要對變量操做,若是沒有添加變量,那麼就是null),註冊類型,而後往裏動態添加屬性,隨後就能夠象OC同樣方便訪問屬性了 (由於靜態類中屬性會默認有一個和它同名的變量,對屬性操做,其實是對該變量操做)。

但實際上對該屬性賦值後,取值倒是null。由於只有在編譯前定義的屬性纔會默認一個變量,property實際上只是提供了setter和getter的方法,至於你要把值存貯在哪裏,須要本身設定,因此還須要在class_addProperty方法後,添加property的setter,getter,並在其中肯定須要把值保存到哪裏,從哪裏取值。

getter

setter

這樣咱們就能用ClassA objA; [objAsetxxx:xxx]; [objA xxx]的方法來訪問屬性了(本人寫了一個簡單的實現,但暫時沒法上傳github,稍後會上傳,請各位上傳)

3.四、使用動態建立類,對象,以及ORM的優勢,缺點

這個例子有以下幾個特色:1.能夠動態生成類型 2.能夠用OC的方式訪問屬性。純粹的「動態」。

固然也有美中不足的地方,首先動態建立對象的類型都是id類型(由於是動態建立,事先沒有定義具體類型),視覺上不直觀。其次編譯過程當中,會報warning,由於property是動態添加的,不是編譯以前肯定的,因此編譯器不知道setter,getter方法哪裏來的。(固然能夠用performSelector來調用就沒有warning問題,可是調用方式太繁瑣)

可是不影響使用。

結果

結論:3.3的方法比3.2,3.1的方法牛逼,直接動態建立類型和對象,可是犧牲的是code的可讀性和可維護性,研究的意義大於實用意義。

注意:這裏須要你們研究的是,如何經過JSON的值,肯定動態添加的變量和property的類型,個人思路是,能夠容易區分NSString和NSNumber,可是若是肯定int,long,float, long long等類型?應該能夠經過值的大小範圍來肯定,例如int -256~255

3.五、如何將對象映射進DB中,其實原理是同樣的,能夠運行時,得到類名,屬性名,屬性類型,值,而後用sqlite3的接口建立表,列,值,類型等等。其實Coredata也是運用了這個動態的原理來實現的。

3、 動態添加屬性

@property 和 Ivar 的區別@property = Ivar + setter + getter

第一種:經過runtime動態關聯對象

相關函數objc_setAssociatedObjectobjc_getAssociatedObjectobjc_removeAssociatedObjects,下面的代碼經過給UIButton添加一個分類的方式關聯兩個屬性clickIntervalclickTime,來實現按鈕的防連點操做。


// .h文件
#import <UIKit/UIKit.h>
@interface UIButton (FixMultiClick)
@property (nonatomic, assign) NSTimeInterval clickInterval;
@end
複製代碼
// .m文件
#import "UIButton+FixMultiClick.h"
#import <objc/runtime.h>
#import <Aspects/Aspects.h>

@interface UIButton ()
@property (nonatomic, assign) NSTimeInterval clickTime;
@end

@implementation UIButton (FixMultiClick)
-(NSTimeInterval)clickTime {
    return [objc_getAssociatedObject(self, _cmd) doubleValue];
}
-(void)setClickTime:(NSTimeInterval)clickTime {
    objc_setAssociatedObject(self, @selector(clickTime), @(clickTime), OBJC_ASSOCIATION_RETAIN_NONATOMIC);
}
-(NSTimeInterval)clickInterval {
    return [objc_getAssociatedObject(self, _cmd) doubleValue];
}
-(void)setClickInterval:(NSTimeInterval)clickInterval {
    objc_setAssociatedObject(self, @selector(clickInterval), @(clickInterval), OBJC_ASSOCIATION_RETAIN_NONATOMIC);
}
+(void)load {
    [UIButton aspect_hookSelector:@selector(sendAction:to:forEvent:)
                      withOptions:AspectPositionInstead
                       usingBlock:^(id<AspectInfo> info){
        UIButton *obj = info.instance;
        if(obj.clickInterval <= 0){
            [info.originalInvocation invoke];
        }
        else{
            if ([NSDate date].timeIntervalSince1970 - obj.clickTime < obj.clickInterval) {
                return;
            }
            obj.clickTime = [NSDate date].timeIntervalSince1970;
            [info.originalInvocation invoke];
        }
    } error:nil];
}
@end
複製代碼

優勢:

能夠快速爲一個已有的class添加一個動態屬性或者block塊

缺點:

不能遍歷全部的關聯對象列表,不能移除指定的關聯對象,只能經過objc_removeAssociatedObjects一次移除全部的關聯對象。

第二種:經過runtime動態建立類的時候添加Ivar

相關函數objc_alloctateClassPairclass_addIvarobjc_registerClassPaire

// 一:爲Class分配內存空間
Class myClass = objc_allocateClassPair([NSObject class], "myClass", 0);
// 二:添加方法
class_addMethod(myClass, @selector(method), (IMP)myMethod, "v@:");
// 三:註冊Class
objc_registerClassPair(myClass);
// 建立對象調用方法
id obj = [[myClass alloc] init];
[obj performSelector:@selector(method)];
複製代碼

優勢:

動態添加Ivar咱們可以經過遍歷Ivar獲得咱們所添加的屬性

缺點:

必須經過class_allocatePair動態建立一個class,才能調用class_addIvar建立Ivar,最後經過class_registClassPair註冊class。不能爲已存在的類添加Ivar,不然會涉及到OC中類的成員變量的偏移量問題,若是在類註冊以後class_adddIvar的話會破壞原來類成員變量的正確偏移量,這樣的話會致使你訪問的成員變量並非你想訪問的成員變量(用KVC賦值和取值直接報錯, 用getIvar的話取值爲null),如圖:

第三種:經過runtime動態添加property

相關函數class_addPropertyclass_addMethodobjc_getAssociatedObjectobjc_getAssociatedObject

僅僅添加屬性是沒什麼用的,由於還須要添加屬性對應的實例變量。雖然runtime提供了class_addIvar方法來給類添加實例變量,可是注意,該方法只能在建立新的類的時候才能使用;對於已經存在的類,是不容許添加實例變量的。鑑於上述緣由,因此能夠採用動態添加關聯對象來存儲屬性對應的實例變量。實現策略以下:

  • 一、因爲咱們確定會在interface 中提供生的property(因爲沒有合成實現與ivar,在此稱爲生的),因此這樣對於在外部訪問時和普通property相同。
  • 二、因爲缺少的是實現以及能夠存取的數據量,這裏咱們能夠直接實現這些set與get。
  • 三、set與get的實現能夠經過 associatedObject 進行對對象的存取操做。
#import "RuntimeTest.h"
#import <objc/runtime.h>

@interface RuntimeTest()
{
    NSString* _address;
}
@end

@implementation RuntimeTest

+(void)load {
    [self runtimeTest];
}

void myMethod(id self, SEL _cmd) {
    NSLog(@"self = %@", self);
    NSLog(@"self.name = %@", [self valueForKey:NSStringFromSelector(@selector(name))]);
    NSLog(@"self.addres = %@", [self valueForKey:NSStringFromSelector(@selector(addres))]);
}

NSString *nameGetter(id self, SEL _cmd) {
    NSString* result = objc_getAssociatedObject(self, _cmd);
    return result;
}

void nameSetter(id self, SEL _cmd, NSString *value) {
    NSString *propertyStr = NSStringFromSelector(_cmd);
    // 去掉 set
    NSString *realProperty = [propertyStr substringFromIndex:3];
    // 去掉 :
    realProperty = [realProperty substringToIndex:realProperty.length - 1];
    // 首字母小寫
    realProperty = [realProperty lowercaseString];
    // 關聯對象
    objc_setAssociatedObject(self, NSSelectorFromString(realProperty), value, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
}
    
+ (void) runtimeTest {
    // 一、Class分配內存空間
    Class myClass = objc_allocateClassPair([NSObject class], "myClass", 0);
    // 2.一、添加方法
    class_addMethod(myClass, @selector(method), (IMP)myMethod, "v@:");
    // 2.二、添加變量(ivar)
    class_addIvar(myClass, "_name", sizeof(NSString*), log2(sizeof(NSString*)), @encode(NSString*));
    // 三:註冊Class
    objc_registerClassPair(myClass);
    
    // 2.三、添加屬性(property)(能夠在類的註冊完成以後)
    NSString* propertyName = @"addres";
    objc_property_attribute_t type = { "T", [[NSString stringWithFormat:@"@\"%@\"",NSStringFromClass([NSString class])] UTF8String] }; //type
    objc_property_attribute_t ownership0 = { "C", "" }; // C = copy
    objc_property_attribute_t ownership = { "N", "" };  // N = nonatomic
    objc_property_attribute_t backingivar  = { "V", [[NSString stringWithFormat:@"_%@", propertyName] UTF8String] };  //variable name
    objc_property_attribute_t attrs[] = { type, ownership0, ownership, backingivar };
    if (class_addProperty(myClass, [propertyName UTF8String], attrs, sizeof(attrs)/sizeof(objc_property_attribute_t))) {
        //添加get和set方法
        NSString *setFunc = [NSString stringWithFormat:@"set%@:",[propertyName capitalizedString]];
        class_addMethod(myClass, NSSelectorFromString(propertyName), (IMP)nameGetter, "@@:");
        class_addMethod(myClass, NSSelectorFromString(setFunc), (IMP)nameSetter, "v@:@");
    }
    
    // 建立對象調用方法
    id obj = [[myClass alloc] init];
    [obj setValue:@"xiaoMing" forKey:NSStringFromSelector(@selector(name))];
    [obj setValue:@"宇宙1" forKey:NSStringFromSelector(@selector(addres))];
    NSLog(@"addres1 = : %@", [obj valueForKey:NSStringFromSelector(@selector(addres))]);
    [obj setValue:@"宇宙2" forKey:@"addres"];
    NSLog(@"addres1 = : %@", [obj valueForKey:@"addres"]);

    [obj performSelector:@selector(method)];
}
@end
複製代碼

優勢:

能都在已有的類中添加property,而且能可以遍歷到動態添加的屬性。這種操做因爲提供了生的property,因此在第三方的json轉model庫遍歷property時能夠直接遍歷到,因爲手動實現了set和get方法,因此在遍歷後的KVC賦值時也能起到做用,保證了和普通成員變量操做的一致性。

缺點:

比較麻煩class_addProperty只是聲明瞭get和set方法(缺乏實現和Ivar),get和set方法須要本身實現,值也須要本身存儲(可使用關聯對象或者存儲到已存在的ivar上)。

第四種:經過setValue:forUndefinedKey:動態添加鍵值

這種方法相似於property,須要重寫setValue:forUndefinedKeyvalueForUndefinedKey:,存值方式也同樣,須要藉助一個其餘對象。因爲這種方式沒有藉助於runtime,因此也比較容易理解。

參考資料:

ios動態添加屬性的幾種方法

iOS-Runtime-實踐篇

老生常談category增長屬性的幾種操做

Objective-C runtime - 應用和示例

4、 KVC分析

Key-Value Coding(KVC)實現分析

[obj setValue:@"張三" forKey:@"name"];
// =======================================
// 就會被編譯器處理成:
// =======================================
SEL sel = sel_get_uid ("setValue:forKey:");
IMP method = objc_msg_lookup (obj->isa, sel);
method(obj, sel, @"張三", @"name");
複製代碼

KVC運用了isa_swizzling(類型混合指針機制)技術,來實現其內部查找定位的。

- (void)setValue:(id)value forKey:(NSString *)key;
複製代碼
  • ① 首先搜索 setter 方法,有就直接賦值。
  • ② 若是上面的 setter 方法沒有找到,再檢查類方法+ (BOOL)accessInstanceVariablesDirectly
    • 返回 NO,則執行setValue:forUNdefinedKey:
    • 返回 YES,則按_key,_isKey,key,isKey的順序搜索成員名。
  • ③ 尚未找到的話,就調用setValue:forUndefinedKey。這些方法的默認實現都是拋出異常,咱們能夠根據須要重寫它們。
- (id)valueForKey:(NSString *)key;
複製代碼
  • ① 首先查找 getter 方法,找到直接調用。若是是 bool、int、float 等基本數據類型,會作 NSNumber 的轉換。
  • ② 若是沒查到,再檢查類方法+ (BOOL)accessInstanceVariablesDirectly
    • 返回 NO,則執行valueForUNdefinedKey:
    • 返回 YES,則按_key,_isKey,key,isKey的順序搜索成員名。
  • ③ 尚未找到的話,調用valueForUndefinedKey:

KVC 主要方法

設置值

// value的值爲OC對象,若是是基本數據類型要包裝成NSNumber
- (void)setValue:(id)value forKey:(NSString *)key;
// keyPath鍵路徑,類型爲xx.xx
- (void)setValue:(id)value forKeyPath:(NSString *)keyPath;
// 它的默認實現是拋出異常,能夠重寫這個函數作錯誤處理。
- (void)setValue:(id)value forUndefinedKey:(NSString *)key;
複製代碼

獲取值

- (id)valueForKey:(NSString *)key;
- (id)valueForKeyPath:(NSString *)keyPath;
// 若是Key不存在,且沒有KVC沒法搜索到任何和Key有關的字段或者屬性,則會調用這個方法,默認是拋出異常
- (id)valueForUndefinedKey:(NSString *)key;
複製代碼

NSKeyValueCoding 類別中還有其餘的一些方法

// 容許直接訪問實例變量,默認返回YES。若是某個類重寫了這個方法,且返回NO,則KVC不能夠訪問該類。
+ (BOOL)accessInstanceVariablesDirectly;
// 這是集合操做的API,裏面還有一系列這樣的API,若是屬性是一個NSMutableArray,那麼能夠用這個方法來返回
- (NSMutableArray *)mutableArrayValueForKey:(NSString *)key;
// 若是你在setValue方法時面給Value傳nil,則會調用這個方法
- (void)setNilValueForKey:(NSString *)key;
// 輸入一組key,返回該組key對應的Value,再轉成字典返回,用於將Model轉到字典。
- (NSDictionary *)dictionaryWithValuesForKeys:(NSArray *)keys;
// KVC提供屬性值確認的API,它能夠用來檢查set的值是否正確、爲不正確的值作一個替換值或者拒絕設置新值並返回錯誤緣由。
- (BOOL)validateValue:(id)ioValue forKey:(NSString *)inKey error:(NSError)outError;
複製代碼

實用技巧

//JSON數據:
//{
//    "username": "lxz",
//    "age": 25,
//    "id": 100
//}

@interface User : NSObject
@property (nonatomic, copy) NSString *name;
@property (nonatomic, assign) NSString age;
@property (nonatomic, assign) NSInteger userId;
@end

@implementation User
- (void)setValue:(id)value forUndefinedKey:(NSString *)key {
    if ([key isEqualToString:@"id"]) {
        self.userId = [value integerValue];
    }
}
@end
複製代碼

賦值時會遇到一些問題,例如服務器會返回一個id字段,可是對於客戶端來講id是系統保留字段,能夠重寫setValue:forUndefinedKey:方法並在內部處理id參數的賦值。

轉換時須要服務器數據和類定義匹配,字段數量和字段名都應該匹配。若是User比服務器數據多,則服務器沒傳的字段爲空。若是服務端傳遞的數據User中沒有定義,則會致使崩潰。

在KVC進行屬性賦值時,內部會對基礎數據類型作處理,不須要手動作NSNumber的轉換。須要注意的是,NSArray和NSDictionary等集合對象,value都不能是nil,不然會致使Crash。

異常處理

- (void)setNilValueForKey:(NSString *)key {
    if ([key isEqualToString:@"name"]) {
        [self setValue:@"" forKey:@」age」];
    } else {
        [super setNilValueForKey:key];
    }
}
複製代碼

當經過KVC給某個非對象的屬性賦值爲nil時,此時KVC會調用屬性所屬對象的setNilValueForKey:方法,並拋出NSInvalidArgumentException的異常,並使應用程序Crash。

咱們能夠經過重寫下面方法,在發生這種異常時進行處理。例如給name賦值爲nil的時候,就能夠重寫setNilValueForKey:方法並表示name是空的。

應用場景:

一、訪問修改私有變量(替換系統自帶的導航欄,tabBar,替換UIPageControl的image等等)

KVC修改readonly的系統隱藏變量。將UIPageControl的圓形替換爲長條形。

[pageControler setValue:[UIImage imageNamed:@"line"] forKeyPath:@"pageImage"];
[pageControler setValue:[UIImage imageNamed:@"current"] forKeyPath:@"currentPageImage"];
複製代碼
二、valueForKeyPath的使用更加普遍,功能也更增強大

一、對數組求和、平均值、最大值、最小值。

NSArray *array = @[@1, @3, @5, @7, @9,@11, @13];
NSInteger sumPath = [[array valueForKeyPath:@"@sum.floatValue"] integerValue];
NSInteger avgPath = [[array valueForKeyPath:@"@avg.floatValue"] integerValue];
NSInteger maxPath = [[array valueForKeyPath:@"@max.floatValue"] integerValue];
NSInteger minPath = [[array valueForKeyPath:@"@min.floatValue"] integerValue];
NSLog(@"sum = %ld, avg = %ld, max = %ld, min = %ld",(long)sumPath, (long)avgPath, (long)maxPath, (long)minPath);
// 上述例子經驗證是可取的,但下面的寫法不可取(將引發崩潰)
NSInteger sum = [[array valueForKey:@"@sum.floatValue"] integerValue];
NSInteger avg = [[array valueForKey:@"@avg.floatValue"] integerValue];
NSInteger max = [[array valueForKey:@"@max.floatValue"] integerValue];
NSInteger min = [[array valueForKey:@"@min.floatValue"] integerValue];
NSLog(@"sum = %ld, avg = %ld, max = %ld, min = %ld",(long)sum, (long)avg, (long)max, (long)min);
複製代碼

二、刪除數組中重複的數據

NSArray *array = @[@1, @3, @5, @7, @9, @11, @13, @7, @9,@11];
NSLog(@"deleteKeyPath = %@",[array valueForKeyPath:@"@distinctUnionOfObjects.self"]);
// 下述寫法不可取,會引發崩潰
NSLog(@"deleteKey = %@",[array valueForKey:@"@distinctUnionOfObjects.self"]);
複製代碼

三、深層次取出字典中的屬性

NSDictionary *dic = @{@"dic1":@{@"dic2":@{@"name":@"zhangsanfeng",@"info":@{@"age":@"13"}}}};
NSLog(@"KeyPath = %@",[dic valueForKeyPath:@"dic1.dic2.info.age"]); // 能夠深層次的取到子層級屬性
NSLog(@"Key = %@",[dic valueForKey:@"dic1.dic2.info.age"]);         // 沒法深層次取到子層級屬性
複製代碼

參考資料:

KVC/KVO原理詳解及編程指南

ios動態添加屬性的幾種方法

KVC, KVO實現原理剖析

KVC 與 KVO 使用姿式和原理解析

KVC原理剖析

KVC 中的 valueForKeyPath 高級用法

iOS 關於KVC的一些總結

5、IMP

method_t的結構

struct method_t {
    SEL name;           // 方法名(一個類裏面可能有多個name相同的method,好比分類中重寫的方法)
    const char *types;  // 存儲着方法的參數類型和返回值類型的描述字串 
    IMP imp;            // 方法的函數指針(方法實現,相同的name可能對應不一樣的實現)
};
複製代碼

IMP與SEL的區別?

  • IMP 它是一個指向方法實現的指針,每個方法都一個對應的IMP指針
  • SEL 是方法的名字,不一樣的方法可能名字(SEL)相同,實現(IMP)不一樣。好比 category中重寫了類方法,則可能出現不一樣的method對應相同的名字(SEL),可是實現(IMP)不一樣

怎麼獲取IMP

一、根據method獲取IMP(惟一)

// Method method = class_getInstanceMethod([self class], sel);
Method method = someMethod;
IMP imp = method_getImplementation(method);
複製代碼

二、根據SEL獲取IMP(可能不是想要的mehtod的IMP)

// 第一種:methodForSelector(SEL) (內部是用 class_getMethodImplementation 實現)
SEL sel = @selector(myFunc);
IMP imp = [self methodForSelector:sel];

// 第二種:class_getMethodImplementation(Class, SEL)
SEL sel = @selector(myFunc);
IMP imp = class_getMethodImplementation(self, sel);
複製代碼

執行一個selector的幾種方法

SEL sel = @selector(someFunc:);
複製代碼

一、使用objc_msgSend(接受者+選擇器+參數)

#if !OBJC_OLD_DISPATCH_PROTOTYPES
OBJC_EXPORT void objc_msgSend(void /* id self, SEL op, ... */ )
#else
OBJC_EXPORT id _Nullable objc_msgSend(id _Nullable self, SEL _Nonnull op, ...)
#endif
複製代碼

二、使用performSelector(儘可能不使用)

performSelector系列方法在內存管理上容易有缺失,它沒法肯定將要執行的選擇子是什麼,於是ARC編譯器也沒法插入適當的內存管理方法,這是一個大坑,使用GCD則不存在這個問題。

// 沒有參數
- (id)performSelector:(SEL)aSelector;
// 傳遞一個參數
- (id)performSelector:(SEL)aSelector withObject:(id)object;
// 傳遞兩個參數
- (id)performSelector:(SEL)aSelector withObject:(id)object1 withObject:(id)object2;
複製代碼

實現performSelector 傳遞多個參數

三、直接調用IMP(至關於C語言函數指針)

// 不一樣的返回值使用不一樣的宏,不然會報EXC_BAD_ACCESS錯誤
typedef id (*_IMP) (id, SEL, ...);
typedef int (*_INT_IMP) (id, SEL, ...);
typedef bool (*_BOOL_IMP) (id, SEL, ...);
typedef void (*_VOID_IMP) (id, SEL, ...);

Method mthod = class_getInstanceMethod([Obj class], sel);
_IMP imp = (_IMP)method_getImplementation(mthod);
imp(Obj, sel, 參數列表)
複製代碼

四、經過NSInvocation調用

NSMethodSignature * methodSignature  = [[myObj class] instanceMethodSignatureForSelector:@selector(myFunc)];
NSInvocation * invocation = [NSInvocation invocationWithMethodSignature:methodSignature];
[invocation setTarget:myObj];
[invocation setSelector:@selector(myFunc)];
NSString *a=@"111";
int b=2;
[invocation setArgument:&a atIndex:2];
[invocation setArgument:&b atIndex:3];
[invocation retainArguments];
[invocation invoke];
複製代碼

objc_msgSend的方法實現的僞代碼

id objc_msgSend(id self, SEL op, ...) {
   if (!self) return nil;
   // 關鍵代碼(a)
   IMP imp = class_getMethodImplementation(self->isa, SEL op);
   imp(self, op, ...); // 調用這個函數,僞代碼...
}
// 查找IMP
IMP class_getMethodImplementation(Class cls, SEL sel) {
    if (!cls || !sel) return nil;
    IMP imp = lookUpImpOrNil(cls, sel);
    if (!imp) {
      ... // 執行動態綁定
    }
    IMP imp = lookUpImpOrNil(cls, sel);
    if (!imp) return _objc_msgForward; // 這個是用於消息轉發的
    return imp;
}
// 遍歷繼承鏈,查找IMP
IMP lookUpImpOrNil(Class cls, SEL sel) {
    if (!cls->initialize()) {
        _class_initialize(cls);
    }
    Class curClass = cls;
    IMP imp = nil;
    do { // 先查緩存,緩存沒有時重建,仍舊沒有則向父類查詢
        if (!curClass) break;
        if (!curClass->cache) fill_cache(cls, curClass);
        imp = cache_getImp(curClass, sel);
        if (imp) break;
    } while (curClass = curClass->superclass); // 關鍵代碼(b)
    return imp;
}
複製代碼

IMP實戰

一:若是分類中重寫了類的方法,找到原有方法,而且執行獲取結果

/**
 若是分類中重寫了類的方法,找到原有方法,而且執行獲取結果

 @param aString 須要比較的NSString
 @return YES or NO
 */
-(BOOL)excuteoRiginalIsEqualToString:(NSString*)aString {
    unsigned int count;
    Method originalMethod = {0};
    // 獲取類的全部方法列表,根據SEL匹配,可能找到多個method,最後一個即原有method
    Method *methods = class_copyMethodList([self class], &count);
    for (int i = 0; i < count; i++) {
        const char* funcName = sel_getName(method_getName(methods[i]));
        if ( 0 == strcmp(funcName, "isEqualToString:") ) {
            // category中的方法在方法列表中的下標小,最後一個爲原來的方法
            originalMethod = methods[i];
        }
    }
    _BOOL_IMP imp = (_BOOL_IMP)method_getImplementation(originalMethod);
    BOOL res = NO;
    if (imp) {
        res = imp(self, method_getName(originalMethod), aString);
    }
    free(methods);
    return res;
}
複製代碼

二:當每一個Controller執行完ViewDidLoad之後就在控制檯把本身的名字打印出來,方便去作調試或者瞭解項目結構

#import "UIViewController+viewDidLoad.h"
#import <objc/runtime.h>

@implementation UIViewController (viewDidLoad)

+ (void)load
{
    //保證交換方法只執行一次
    static dispatch_once_t onceToken;
    dispatch_once(&onceToken, ^{
        //獲取原始方法
        Method viewDidLoad = class_getInstanceMethod(self, @selector(viewDidLoad));
        //獲取方法實現
        _VIMP viewDidLoad_IMP = (_VIMP)method_getImplementation(viewDidLoad);
        //從新設置方法實現
        method_setImplementation(viewDidLoad,imp_implementationWithBlock(^(id target,SEL action){
            viewDidLoad_IMP(target,@selector(viewDidLoad));
            //自定義代碼
            NSLog(@"%@ did load",target);
        }));
    });
}
複製代碼

參考博客

GCD實踐(二)少用performSelector系列方法

刨根究底iOS—調戲Category

6、KVO分析

一次簡單的KVO操做

@interface Man : NSObject
@property (nonatomic, assign) NSInteger p_mustacheLength;
// 直接修改爲員變量
- (void)set_P_mustacheLength:(NSInteger)p_mustacheLength;
// 手動觸發
- (void)set_P_mustacheLength_manual:(NSInteger)p_mustacheLength;
@end
@implementation Man
// 直接修改爲員變量
- (void)set_P_mustacheLength:(NSInteger)p_mustacheLength {
    _p_mustacheLength = p_mustacheLength;
}
// 手動觸發
- (void)set_P_mustacheLength_manual:(NSInteger)p_mustacheLength {
    [self willChangeValueForKey:@"p_mustacheLength"];
    _p_mustacheLength = p_mustacheLength;
    [self didChangeValueForKey:@"p_mustacheLength"];
}
// 重寫set方法
- (void)setP_mustacheLength:(NSInteger)p_mustacheLength {
    _p_mustacheLength = p_mustacheLength;
}
// 是否自動對屬性p_mustacheLength觸發KVO
+(BOOL)automaticallyNotifiesObserversOfP_mustacheLength {
    // 默認返回YES
    return YES;
}
@end
@interface KVOVC : UIViewController
@end

// 同一個屬性觀察了屢次,用來區分是哪一次觀察操做
// const*:不能改變內容
// const:不能改變地址
char const* const context_p_man_p_mustacheLength_1 = "context_p_man_p_mustacheLength_1";
char const* const context_p_man_p_mustacheLength_2 = "context_p_man_p_mustacheLength_2";
@interface KVOVC ()
@property (nonatomic, strong) Man* p_man;
@end
@implementation KVOVC
- (void)observeValueForKeyPath:(NSString *)keyPath
                      ofObject:(id)object
                        change:(NSDictionary<NSKeyValueChangeKey,id> *)change
                       context:(void *)context {
    NSLog(@"-----------------------------------------");
    NSLog(@"keyPath = %@", keyPath);
    NSLog(@"object = %@", object);
    NSLog(@"change = %@", change);
    NSLog(@"context = %s", context);
}
- (void)viewDidLoad {
    [super viewDidLoad];
	self.p_man = [[Man alloc] init];
	
    [self.p_man addObserver:self
                 forKeyPath:NSStringFromSelector(@selector(p_mustacheLength))
                    options:NSKeyValueObservingOptionNew
                    context:(void*)context_p_man_p_mustacheLength_1];
    
    [self.p_man addObserver:self
                 forKeyPath:NSStringFromSelector(@selector(p_mustacheLength))
                    options:NSKeyValueObservingOptionNew
                    context:(void*)context_p_man_p_mustacheLength_2];

    // (觸發)set方法能夠觸發(不管是否重寫Man的p_mustacheLengthset方法。由於此時p_man的isa = NSKVONotifying_Man而不是Man 查看連接)
    self.p_man.p_mustacheLength = 10;
    // (觸發)kvc能夠觸發(kvc首先查找調用的也是set方法 查看連接)
    [self.p_man setValue:@20 forKey:NSStringFromSelector(@selector(p_mustacheLength))];
    // (不能觸發)直接修改爲員變量不能觸發(沒有走set方法)
    [self.p_man set_P_mustacheLength:30];
    // (觸發)手動觸發
    [self.p_man set_P_mustacheLength_manual:40];
}
- (void)dealloc {
    NSLog(@"%@-%s-%d", NSStringFromClass([self class]), __func__, __LINE__);
        [self.p_man removeObserver:self
                    forKeyPath:NSStringFromSelector(@selector(p_mustacheLength))
                       context:(void*)context_p_man_p_mustacheLength_1];
    [self.p_man removeObserver:self
                    forKeyPath:NSStringFromSelector(@selector(p_mustacheLength))
                       context:(void*)context_p_man_p_mustacheLength_2];
}
@end
複製代碼

官方KVO文檔:Key-Value Observing Implementation Details

自動鍵值觀察是使用isa- swizzle技術實現的。顧名思義,isa指針指向維護分派表的對象的類。這個分派表本質上包含指向類實現的方法以及其餘數據的指針。當觀察者爲一個對象的屬性註冊時,被觀察對象的isa指針被修改,指向一箇中間類而不是真正的類。所以,isa指針的值不必定反映實例的實際類。 您永遠不該該依賴isa指針來肯定類的繼承關係。相反,您應該使用類方法來肯定對象實例的類。

KVO原理

一:在self.p_man添加KVO以前,查看其繼承關係。結果:isa = Man,superClass = NSObject

二:在self.p_man添加KVO以後,查看其繼承關係。結果:isa = NSKVONotifying_Man,superClass = NSObject

官方文檔中說起作多的關鍵字就是isa,Objective-C的消息機制就是經過isa查找方法的。其實在添加KVO以後,isa已經替換成了NSKVONotifying_Man。所以調用屬性的set方法的時候,根據isa找到的方法實際上是NSKVONotifying_Man中的set方法。

KVO是基於runtime機制實現的,當某個實例的屬性第一次被觀察的時候,系統會在運行時期動態的建立一個該類的子類(類名=NSKVONotifying_XXX)並將isa指針指向新建立的子類。在這個派生類中重寫全部被觀察屬性的set方法,在成員變量被改變前調用NSObject的willChangeValueForKey:,被改變後調用didChangeValueForKey:。從而致使observeValueForKey:ofObject:change:context被調用。

KVO的這套實現機制中蘋果還偷偷重寫了class方法,讓咱們誤認爲仍是使用的當前類,從而達到隱藏生成的派生類

-(void) addObserver: forKeyPath: options: context: 這個部分就是觀察者的註冊了。經過如下類圖能夠很方便得看到,全部的類的KVO觀察都是經過infoTable管理的。以被觀察對象實例做key,GSKVOInfo對象爲value的形式保存在infoTable表裏,每一個被觀察者實例會對應多個keypath,每一個keypath會對應多個observer對象。順帶提一下,關於Notification的實現也相似,也是全局表維護通知的註冊監聽者和通知名。 GSKVOInfo的結構能夠看出來,一個keyPath能夠對應有多個觀察者。其中觀察對象的實例和option打包成GSKVOObservation對象保存在一塊兒。

如何手動觸發一個的KVO

// 手動觸發
- (void)set_P_mustacheLength_manual:(NSInteger)p_mustacheLength {
    [self willChangeValueForKey:@"p_mustacheLength"];
    _p_mustacheLength = p_mustacheLength;
    [self didChangeValueForKey:@"p_mustacheLength"];
}
複製代碼

若是要禁止KVO對某個屬性自動觸發,返回NO就能夠

// 是否自動對屬性p_mustacheLength觸發KVO
+(BOOL)automaticallyNotifiesObserversOfP_mustacheLength {
    // 默認返回YES
    return YES;
}
複製代碼

KVO容易掉進去的坑

  • 一、沒有removeObserver (使用KVOController或者不要忘記)
  • 二、同一個對象的同一個屬性被重複removeObserver了屢次(使用context來removeObserver)
  • 三、keyPath嚴重依賴於string(@selector彌補)

KVOController實現探索

本身實現一個KVO:ImplementKVO

參考博客

KVO進階——KVO實現探究

探究KVO的底層實現原理

從本身實現isa-swizzling到說一些Runtime的內容

iOS探索KVO實現原理,重寫KVO

如何優雅地使用 KVO

7、消息轉發

爲啥能夠對nil對象發送消息?

NilTest 宏,判斷被髮送消息的對象是否爲 nil 的。若是爲 nil,那就直接返回 nil

參考資料:

Objective-C 消息發送與轉發機制原理

8、weak的原理

weak不管是用做property修飾符仍是用來修飾一個變量的聲明其做用是同樣的,就是不增長新對象的引用計數,被釋放時也不會減小新對象的引用計數,同時在新對象被銷燬時,weak修飾的屬性或變量均會被設置爲nil,這樣能夠防止野指針錯誤,本文要講解的也正是這個特性,runtime如何將weak修飾的變量的對象在銷燬時自動置爲nil。

那麼runtime是如何實如今weak修飾的變量的對象在被銷燬時自動置爲nil的呢?

一個廣泛的解釋是:runtime對註冊的類會進行佈局,對於weak修飾的對象會放入一個hash表中。用weak指向的對象內存地址做爲key,當此對象的引用計數爲0的時候會dealloc,假如weak指向的對象內存地址是a,那麼就會以a爲鍵在這個weak表中搜索,找到全部以a爲鍵的weak對象,從而設置爲nil

相關文章
相關標籤/搜索