iOS-Runtime從認識到深入

技 術 文 章 / 超 人


Runtime(運行時機制)概念

Object-C 是面向對象的語言,C是面向結構也就是面向過程的語言。Object-C是基于C基礎上的語言。而Runtime為Object-C添加了面向對象的特性,將靜態語言編譯期和鏈接期做的事情放到了運行時來處理。所以Runtime是每一個IOS開發人員都應該了解的。Runtime是一套純C的API。

  • Objective-C不僅需要一個編譯器,還需要一個運行時系統來執行編譯的代碼。對于Objective-C來說,這個運行時系統就像一個操作系統一樣:它讓所有的工作可以正常的運行。這個運行時系統即Objc Runtime。Objc Runtime其實是一個Runtime庫,它基本上是用C和匯編寫的,這個庫使得C語言有了面向對象的能力。

  • Runtime庫主要做下面幾件事:
    封裝:在這個庫中,對象可以用C語言中的結構體表示,而方法可以用C函數來實現,另外再加上了一些額外的特性。這些結構體和函數被runtime函數封裝后,我們就可以在程序運行時創建,檢查,修改類、對象和它們的方法了。
    找出方法的最終執行代碼:當程序執行[object doSomething]時,會向消息接收者(object)發送一條消息(doSomething),runtime會根據消息接收者是否能響應該消息而做出不同的反應。這將在后面詳細介紹。

  • 對于C語言而言函數的調用在編譯時就會決定調用那一個函數。而OC在編譯時并不能決定調用那一個函數,只有在運行時才能確定調用那一個函數,舉個簡單的例子:在日常開發時,在.h中聲明了某個方法,而.m中卻沒有實現該方法,但編譯項目卻不會報錯,因為OC編譯時并沒有確定.h中聲明但方法具體調用.m的那一個函數,當然也就不知道該函數有沒有實現了,而在運行時調用該函數時就會報錯,因為運行時才會確定具體調用那一個函數,函數沒有實現那么就會報錯。OC是動態調用,正因為動態性,所以在運行時可以修改調用方法的指針。

  • Runtime其實有兩個版本: “modern” 和 “legacy”。我們現在用的 Objective-C 2.0 采用的是現行 (Modern) 版的 Runtime 系統,只能運行在 iOS 和 macOS 10.5 之后的 64 位程序中。而 maxOS 較老的32位程序仍采用 Objective-C 1 中的(早期)Legacy 版本的 Runtime 系統。這兩個版本最大的區別在于當你更改一個類的實例變量的布局時,在早期版本中你需要重新編譯它的子類,而現行版就不需要。

  • Runtime 基本是用 C 和匯編寫的,可見蘋果為了動態系統的高效而作出的努力。你可以在這里下到蘋果維護的開源代碼。蘋果和GNU各自維護一個開源的 runtime 版本,這兩個版本之間都在努力的保持一致。

基礎知識補充

  • Class:定義Objective-C類
  • Ivar:定義對象的實例變量,包括類型和名字。
  • Protocol:定義正式協議。
  • objc_property_t:定義屬性。叫這個名字可能是為了防止和Objective-C 1.0中的用戶類型沖突,那時候還沒有屬性。
  • Method:定義對象方法或類方法。這個類型提供了方法的名字(就是選擇器)、參數數量和類型,以及返回值(這些信息合起來稱為方法的簽名),還有一個指向代碼的函數指針(也就是方法的實現)。
  • SEL:定義選擇器。選擇器是方法名的唯一標識符。
  • IMP:定義方法實現。這只是一個指向某個函數的指針,該函數接受一個對象、一個選擇器和一個可變長參數列表(varargs),返回一個對象
  • unsigned signed和unsigned不會改變類型長度,僅表示最高位是否為符號位,其中unsigned表示大于等于0的正數

受限于內存分配的機制,一個NSObject對象會分配16byte的內存空間,但在64位系統下只使用了8byte,在32位系統下只使用了4byte.

獲取Obj-C指針所指向的內存的大小

malloc_size((__brige const void *)obj);

一個NSObject實例對象成員變量所占的大小是8byte。
獲取Object對象大小方法

 Class_getInstanceSize([NSObject class])
//其本質是
size_t class_getInstanceSize(Class cls)
{
  if(!cls)return 0;
  return cls->alignedInstanceSize();
}

看到這里。你應該對Runtime有一個初步的了解了。建議先看看Runtime的方法,在看下面的內容。這樣才會容易明白實例代碼的用意。請查看Runtime方法說明


使用Runtime
1.導入頭文件 #import <objc/message.h>
2.OC 解決消息機制方法提示步驟[查找build setting-> 搜索msg->objc_msgSend(YES --> NO]

Runtime最主要是消息機制,OC在編譯時會轉化為C,轉化為: objc_msgSend(receiver, selector),何以證明?新建一個類 MyClass,其.m文件如下:

#import "MyClass.h"
@implementation MyClass
-(instancetype)init{
    if (self = [super init]) {
        [self showUserName];
    }
    return self;
}
-(void)showUserName{
    NSLog(@"Dave Ping");
}

然后使用 clang 重寫命令:

$ clang -rewrite-objc MyClass.m

然后在同一目錄下會多出一個 MyClass.cpp 文件,雙擊打開,可以看到 init 方法已經被編譯器轉化為下面這樣:

static instancetype _I_MyClass_init(MyClass * self, SEL _cmd) {
    if (self = ((MyClass *(*)(__rw_objc_super *, SEL))(void *)objc_msgSendSuper)((__rw_objc_super){(id)self, (id)class_getSuperclass(objc_getClass("MyClass"))}, sel_registerName("init"))) {
        ((void (*)(id, SEL))(void *)objc_msgSend)((id)self, sel_registerName("showUserName"));
    }
    return self;
}

我們要找的就是它:

((void (*)(id, SEL))(void *)objc_msgSend)((id)self, sel_registerName("showUserName"))

objc_msgSend 函數被定義在 objc/message.h 目錄下,其函數原型是醬紫滴:

OBJC_EXPORT void objc_msgSend(void /* id self, SEL op, ... */ )

該函數有兩個參數,一個 id 類型,一個 SEL 類型。

SEL

SEL 被定義在 objc/objc.h 目錄下:

typedef struct objc_selector *SEL;

其實它就是個映射到方法的C字符串,你可以用 Objective-C 編譯器命令 @selector() 或者 Runtime 系統的 sel_registerName 函數來獲得一個 SEL 類型的方法選擇器。簡單來說就是方法名稱

id

與 SEL 一樣,id 也被定義在 objc/objc.h 目錄下:

typedef struct objc_object *id;

id 是一個結構體指針類型,它可以指向 Objective-C 中的任何對象。objc_object 結構體定義如下:

struct objc_object { Class isa OBJC_ISA_AVAILABILITY;};

我們通常所說的對象,就長這個樣子,這個結構體只有一個成員變量 isa,對象可以通過 isa 指針找到其所屬的類。isa 是一個 Class 類型的成員變量,那么 Class 又是什么呢?

isa 等價于 isKindOf方法

  • 實例對象 isa 指向類對象
  • 類對象指 isa 向元類對象
  • 元類對象的 isa 指向元類的基類

isa 有兩種類型

  • 純指針:指向內存地址
  • NON_POINTER_ISA:除了內存地址,還存有一些其他信息

Class

Class 也是一個結構體指針類型:

typedef struct objc_class *Class;

objc_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;
    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;

我們通常說的類就長這樣子:

  • Class : Objective-C類是由Class類型來表示的,它實際上是一個指向objc_class結構體的指針
  • super_class:指向該類的父類,如果該類已經是最頂層的根類(如NSObject或NSProxy),則super_class為NULL。
  • name:是類名。
  • version:我們可以使用這個字段來提供類的版本信息。這對于對象的序列化非常有用,它可以讓我們識別出不同類定義版本中實例變量布局的改變。
  • info:是類的詳情。
  • instance_size:是該類的實例對象的大小。
  • ivars:指向該類的成員變量列表。
  • methodLists:指向該類的實例方法列表,它將方法選擇器和方法實現地址聯系起來。methodLists 是指向 ·objc_method_list 指針的指針,也就是說可以動態修改 *methodLists 的值來添加成員方法,這也是 Category 實現的原理,同樣解釋了 Category 不能添加屬性的原因。
  • cache:用于緩存最近使用的方法。一個接收者對象接收到一個消息時,它會根據isa指針去查找能夠響應這個消息的對象。在實際使用中,這個對象只有一部分方法是常用的,很多方法其實很少用或者根本用不上。這種情況下,如果每次消息來時,我們都是methodLists中遍歷一遍,性能勢必很差。這時,cache就派上用場了。在我們每次調用過一個方法后,這個方法就會被緩存到cache列表中,下次調用的時候runtime就會優先去cache中查找,如果cache沒有,才去methodLists中查找方法。這樣,對于那些經常用到的方法的調用,但提高了調用的效率。
  • protocols:指向該類的協議列表。

objc_object與id

struct objc_object {
    Class isa  OBJC_ISA_AVAILABILITY;
};
 
typedef struct objc_object *id;

可以看到,這個結構體只有一個字體,即指向其類的isa指針。這樣,當我們向一個Objective-C對象發送消息時,運行時庫會根據實例對象的isa指針找到這個實例對象所屬的類。Runtime庫會在類的方法列表及父類的方法列表中去尋找與消息對應的selector指向的方法。找到后即運行這個方法。

當創建一個特定類的實例對象時,分配的內存包含一個objc_object數據結構,然后是類的實例變量的數據。NSObject類的alloc和allocWithZone:方法使用函數class_createInstance來創建objc_object數據結構。

另外還有我們常見的id,它是一個objc_object結構類型的指針。它的存在可以讓我們實現類似于C++中泛型的一些操作。該類型的對象可以轉換為任何一種對象,有點類似于C語言中void *指針類型的作用。

objc_cache
上面提到了objc_class結構體中的cache字段,它用于緩存調用過的方法。這個字段是一個指向objc_cache結構體的指針,其定義如下:

struct objc_cache {
    unsigned int mask /* total = mask + 1 */                 OBJC2_UNAVAILABLE;
    unsigned int occupied                                    OBJC2_UNAVAILABLE;
    Method buckets[1]                                        OBJC2_UNAVAILABLE;
};
  • mask:一個整數,指定分配的緩存bucket的總數。在方法查找過程中,Objective-C runtime使用這個字段來確定開始線性查找數組的索引位置。指向方法selector的指針與該字段做一個AND位操作(index = (mask & selector))。這可以作為一個簡單的hash散列算法。

  • occupied:一個整數,指定實際占用的緩存bucket的總數。

  • buckets:指向Method數據結構指針的數組。這個數組可能包含不超過mask+1個元素。需要注意的是,指針可能是NULL,表示這個緩存bucket沒有被占用,另外被占用的bucket可能是不連續的。這個數組可能會隨著時間而增長

元類(Meta Class)

例:

NSArray *array = [NSArray array];
  • 這個例子中,+array消息發送給了NSArray類,而這個NSArray也是一個對象。既然是對象,那么它也是一個objc_object指針,它包含一個指向其類的一個isa指針。那么這些就有一個問題了,這個isa指針指向什么呢?為了調用+array方法,這個類的isa指針必須指向一個包含這些類方法的一個objc_class結構體。這就引出了meta-class的概念:

  • meta-class是一個類對象的類。

  • 當我們向一個對象發送消息時,runtime會在這個對象所屬的這個類的方法列表中查找方法;而向一個類發送消息時,會在這個類的meta-class的方法列表中查找。

  • meta-class之所以重要,是因為它存儲著一個類的所有類方法。每個類都會有一個單獨的meta-class,因為每個類的類方法基本不可能完全相同。

  • 再深入一下,meta-class也是一個類,也可以向它發送一個消息,那么它的isa又是指向什么呢?為了不讓這種結構無限延伸下去,Objective-C的設計者讓所有的meta-class的isa指向基類的meta-class,以此作為它們的所屬類。即,任何NSObject繼承體系下的meta-class都使用NSObject的meta-class作為自己的所屬類,而基類的meta-class的isa指針是指向它自己。這樣就形成了一個完美的閉環。

IMP
IMP就是Implementation的縮寫,顧名思義,它是指向一個方法實現的指針,每一個方法都有一個對應的IMP,所以,我們可以直接調用方法的IMP指針,來避免方法調用死循環的問題。

IMP的定義如下:

if !OBJC_OLD_DISPATCH_PROTOTYPES
typedef void (*IMP)(void /* id, SEL, ... */ ); 
else
typedef id (*IMP)(id, SEL, ...); 
endif

實際上直接調用一個方法的IMP指針的效率是高于調用方法本身的,所以,如果你有一個合適的時機獲取到方法的IMP的話,你可以試著調用它。

objc_msgSend說明
OBJC_EXPORT void objc_msgSend(void /* id self, SEL op, ... */ )

首先,Runtime 系統會把方法調用轉化為消息發送,即 objc_msgSend,并且把方法的調用者,和方法選擇器,當做參數傳遞過去.

此時,方法的調用者會通過 isa 指針來找到其所屬的類,然后在 cache 或者 methodLists 中查找該方法,找得到就跳到對應的方法去執行。

如果在類中沒有找到該方法,則通過 super_class 往上一級超類查找(如果一直找到 NSObject 都沒有找到該方法的話,這種情況,我們放到后面消息轉發的時候再說)。

前面我們說 methodLists 指向該類的實例方法列表,實例方法即-方法,那么類方法(+方法)存儲在哪兒呢?類方法被存儲在元類中,Class 通過 isa 指針即可找到其所屬的元類。


指針

上圖實線是 super_class 指針,虛線是 isa 指針。根元類的超類是NSObject,而 isa 指向了自己。NSObject 的超類為 nil,也就是它沒有超類。

前面我們使用 clang 重寫命令,看到 Runtime 是如何將方法調用轉化為消息發送的。我們也可以依樣畫葫蘆,來學習使用一下 objc_msgSend。新建一個類 TestClass,添加如下方法:

-(void)showAge{
    NSLog(@"24");
}
-(void)showName:(NSString *)aName{
    NSLog(@"name is %@",aName);
}
-(void)showSizeWithWidth:(float)aWidth andHeight:(float)aHeight{
    NSLog(@"size is %.2f * %.2f",aWidth, aHeight);
}
-(float)getHeight{
    return 187.5f;
}
-(NSString *)getInfo{
    return @"Hi, my name is Dave Ping, I'm twenty-four years old in the year, I like apple, nice to meet you.";
}

我們可以像下面這樣,使用 objc_msgSend 依次調用這些方法:

Class *objct = NSClassFromStrin[@"TestClass"];
((void (*) (id, SEL)) objc_msgSend) (objct, sel_registerName("showAge"));
((void (*) (id, SEL, NSString *)) objc_msgSend) (objct, sel_registerName("showName:"), @"Dave Ping");
((void (*) (id, SEL, float, float)) objc_msgSend) (objct, sel_registerName("showSizeWithWidth:andHeight:"), 110.5f, 200.0f);
float f = ((float (*) (id, SEL)) objc_msgSend_fpret) (objct, sel_registerName("getHeight"));
NSLog(@"height is %.2f",f);
NSString *info = ((NSString* (*) (id, SEL)) objc_msgSend) (objct, sel_registerName("getInfo"));
NSLog(@"%@",info);

也許你已經注意到,objc_msgSend 在使用時都被強制轉換了一下,這是因為 objc_msgSend 函數可以hold住各種不同的返回值以及多個參數,但默認情況下是沒有參數和返回值的。如果我們把調用 showAge 方法改成這樣:

objc_msgSend(objct, sel_registerName("showAge"));

Xcode 就會報錯:

Too many arguments to function call, expected 0, have 2.

objc_msgSendSuper
編譯器會根據情況在 objc_msgSend,objc_msgSend_stret,objc_msgSendSuper,objc_msgSendSuper_stret 或 objc_msgSend_fpret 五個方法中選擇一個來調用。如果消息是傳遞給超類,那么會調用 objc_msgSendSuper 方法,如果消息返回值是數據結構,就會調用 objc_msgSendSuper_stret 方法,如果返回值是浮點數,則調用 objc_msgSend_fpret 方法。

這里我們重點說一下 objc_msgSendSuper,objc_msgSendSuper 函數原型如下:

OBJC_EXPORT void objc_msgSendSuper(void /* struct objc_super *super, SEL op, ... */ )

當我們調用 [super selector] 時,Runtime 會調用 objc_msgSendSuper 方法,objc_msgSendSuper 方法有兩個參數,super 和 op,Runtime 會把 selector 方法選擇器賦值給 op。而 super 是一個 objc_super 結構體指針,objc_super 結構體定義如下:

struct objc_super {
    /// Specifies an instance of a class.
    __unsafe_unretained id receiver;
    /// Specifies the particular superclass of the instance to message.
#if !defined(__cplusplus)  &&  !__OBJC2__
    /* For compatibility with old objc-runtime.h header */
    __unsafe_unretained Class class;
#else
    __unsafe_unretained Class super_class;
#endif
    /* super_class is the first class to search */
};

Runtime 會創建一個 objc_spuer 結構體變量,將其地址作為參數(super)傳遞給 objc_msgSendSuper,并且將 self 賦值給 receiver:super—>receiver=self.

舉個栗子,問下面的代碼輸出什么:

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

答案是全部輸出 Son。

使用 clang 重寫命令,發現上述代碼被轉化為:

NSLog((NSString *)&__NSConstantStringImpl__var_folders_gm_0jk35cwn1d3326x0061qym280000gn_T_main_a5cecc_mi_0, NSStringFromClass(((Class (*)(id, SEL))(void *)objc_msgSend)((id)self, sel_registerName("class"))));
NSLog((NSString *)&__NSConstantStringImpl__var_folders_gm_0jk35cwn1d3326x0061qym280000gn_T_main_a5cecc_mi_1, NSStringFromClass(((Class (*)(__rw_objc_super *, SEL))(void *)objc_msgSendSuper)((__rw_objc_super){ (id)self, (id)class_getSuperclass(objc_getClass("Son")) }, sel_registerName("class"))));

當調用 [super class] 時,會轉換成 objc_msgSendSuper 函數:

  • 第一步先構造 objc_super 結構體,結構體第一個成員就是 self。第二個成員是 (id)class_getSuperclass(objc_getClass(“Son”)).

第二步是去 Father 這個類里去找 - (Class)class,沒有,然后去 NSObject 類去找,找到了。最后內部是使用 objc_msgSend(objc_super->receiver, @selector(class)) 去調用,此時已經和 [self class] 調用相同了,所以兩個輸出結果都是 Son。

Associated對象關聯
對象關聯允許開發者對已經存在的類在 Category 中添加自定義的屬性:

OBJC_EXPORT void objc_setAssociatedObject(id object, const void *key, id value, objc_AssociationPolicy policy) __OSX_AVAILABLE_STARTING(__MAC_10_6, __IPHONE_3_1);
  • object 是源對象

  • value 是被關聯的對象

  • key 是關聯的鍵,objc_getAssociatedObject 方法通過不同的 key 即可取出對應的被關聯對象

  • policy 是一個枚舉值,表示關聯對象的行為,從命名就能看出各個枚舉值的含義:

typedef OBJC_ENUM(uintptr_t, objc_AssociationPolicy) {
   OBJC_ASSOCIATION_ASSIGN = 0,           /** 指定對關聯對象的弱引用. */
   OBJC_ASSOCIATION_RETAIN_NONATOMIC = 1, /** 指定對關聯對象的強引.不是atomically */
   OBJC_ASSOCIATION_COPY_NONATOMIC = 3,   /** 指定復制的關聯對象.*   不是 atomically. */
   OBJC_ASSOCIATION_RETAIN = 01401,       /** 指定對關聯對象的強引用.*   是 atomically. */
   OBJC_ASSOCIATION_COPY = 01403          /** 指定復制的關聯對象. *   是 atomically. */
};

要取出被關聯的對象使用 objc_getAssociatedObject 方法即可,要刪除一個被關聯的對象,使用 objc_setAssociatedObject 方法將對應的 key 設置成 nil 即可:

objc_setAssociatedObject(self, associatedKey, nil, OBJC_ASSOCIATION_COPY_NONATOMIC);

objc_removeAssociatedObjects 方法將會移除源對象中所有的關聯對象.

舉個栗子,假如我們要給 UIButton 添加一個監聽單擊事件的 block 屬性,新建 UIButton 的 Category,其.m文件如下:

#import "UIButton+ClickBlock.h"
static const void *associatedKey = "associatedKey";
@implementation UIButton (ClickBlock)
//Category中的屬性,只會生成setter和getter方法,不會生成成員變量
-(void)setClick:(clickBlock)click{
    /* 建立click的關聯,暗號為associatedKey */
    objc_setAssociatedObject(self, associatedKey, click, OBJC_ASSOCIATION_COPY_NONATOMIC);
    /* 先移除監聽,防止重復監聽 */
    [self removeTarget:self action:@selector(buttonClick) forControlEvents:UIControlEventTouchUpInside];
    /* 判斷是否實現了click,沒有實現就不用注冊 */
    if (click) {
    /* 注冊監聽 */
        [self addTarget:self action:@selector(buttonClick) forControlEvents:UIControlEventTouchUpInside];
    }
}
-(clickBlock)click{
    /* 當使用click屬性時,直接獲取關聯對象當click,相當于button.click使用時獲取當對象就關聯對象 */
    return objc_getAssociatedObject(self, associatedKey);
}
-(void)buttonClick{
    /* 收到監聽消息后回調到click對象中 */
    if (self.click) {
        self.click();
    }
}
@end

然后在代碼中,就可以使用 UIButton 的屬性來監聽單擊事件了:

UIButton *button = [UIButton buttonWithType:UIButtonTypeCustom];
button.frame = self.view.bounds;
[self.view addSubview:button];
button.click = ^{
    NSLog(@"buttonClicked");
};

Runtime案例使用

自動歸檔

一般到歸檔寫法時這樣的:

- (void)encodeWithCoder:(NSCoder *)aCoder{
    [aCoder encodeObject:self.name forKey:@"name"];
    [aCoder encodeObject:self.ID forKey:@"ID"];
}
- (id)initWithCoder:(NSCoder *)aDecoder{
    if (self = [super init]) {
        self.ID = [aDecoder decodeObjectForKey:@"ID"];
        self.name = [aDecoder decodeObjectForKey:@"name"];
    }
    return self;
}

那么問題來了,如果當前 Model 有100個屬性的話,就需要寫100行這種代碼:

[aCoder encodeObject:self.name forKey:@"name"];

想想都頭疼,通過 Runtime 我們就可以輕松解決這個問題:

1.使用 class_copyIvarList 方法獲取當前 Model 的所有成員變量.

2.使用 ivar_getName 方法獲取成員變量的名稱.

3.通過 KVC 來讀取 Model 的屬性值(encodeWithCoder:),以及給 Model 的屬性賦值(initWithCoder:).

舉個栗子,新建一個 Model 類,其.m文件如下

#import "TestModel.h"
#import #import @implementation TestModel
- (void)encodeWithCoder:(NSCoder *)aCoder{
   unsigned int outCount = 0;
   Ivar *vars = class_copyIvarList([self class], &outCount);
   for (int i = 0; i < outCount; i ++) {
       Ivar var = vars[i];
       const char *name = ivar_getName(var);
       NSString *key = [NSString stringWithUTF8String:name];
       // 注意kvc的特性是,如果能找到key這個屬性的setter方法,則調用setter方法
       // 如果找不到setter方法,則查找成員變量key或者成員變量_key,并且為其賦值
       // 所以這里不需要再另外處理成員變量名稱的“_”前綴
       id value = [self valueForKey:key];
       [aCoder encodeObject:value forKey:key];
   }
}
- (nullable instancetype)initWithCoder:(NSCoder *)aDecoder{
   if (self = [super init]) {
       unsigned int outCount = 0;
       Ivar *vars = class_copyIvarList([self class], &outCount);
       for (int i = 0; i < outCount; i ++) {
           Ivar var = vars[i];
           const char *name = ivar_getName(var);
           NSString *key = [NSString stringWithUTF8String:name];
           id value = [aDecoder decodeObjectForKey:key];
           [self setValue:value forKey:key];
       }
   }
   return self;
}
@end

Ivar 定義對象的實例變量,包括類型和名字
是objc_ivar的指針,包含變量名,變量類型等成員。ivars是一個數組,數組中每個元素是指向Ivar(變量信息)的指針

  • Ivar定義:
typedef objc_ivar * Ivar;
  • Ivar結構體:
struct objc_ivar {
     char *ivar_name;
     char *ivar_type;
     int ivar_offset;
  #ifdef __LP64__
     int space;
  #endif
  }
  • Ivar的相關操作
//獲取Ivar的名稱
  const char *ivar_getName(Ivar v);
  //獲取Ivar的類型編碼,
  const char *ivar_getTypeEncoding(Ivar v)
  //通過變量名稱獲取類中的實例成員變量
  Ivar class_getInstanceVariable(Class cls, const char *name)
  //通過變量名稱獲取類中的類成員變量
  Ivar class_getClassVariable(Class cls, const char *name)
  //獲取指定類的Ivar列表及Ivar個數
  Ivar *class_copyIvarList(Class cls, unsigned int *outCount)
  //獲取實例對象中Ivar的值
  id object_getIvar(id obj, Ivar ivar) 
  //設置實例對象中Ivar的值
  void object_setIvar(id obj, Ivar ivar, id value)
字典轉模型

一般的字典轉模型是這樣滴:

-(instancetype)initWithDictionary:(NSDictionary *)dict{
    if (self = [super init]) {
        self.age = dict[@"age"];
        self.name = dict[@"name"];
    }
    return self;
}

可想而知,遇到的問題跟歸檔時候一樣(后來使用MJExtension),這里我們稍微來學習一下其中原理,字典轉模型的時候:

1.根據字典的 key 生成 setter 方法

2.使用 objc_msgSend 調用 setter 方法為 Model 的屬性賦值(或者 KVC)

模型轉字典的時候:

1.調用 class_copyPropertyList 方法獲取當前 Model 的所有屬性

2.調用 property_getName 獲取屬性名稱

3.根據屬性名稱生成 getter 方法

4.使用 objc_msgSend 調用 getter 方法獲取屬性值(或者 KVC)

代碼如下:

#import "NSObject+KeyValues.h"
#import @implementation NSObject (KeyValues)

//字典轉模型
+(id)objectWithKeyValues:(NSDictionary *)aDictionary{
    /* 創建model */
    id objc = [[self alloc] init];
    /* 遍歷字典里所有的key */
    for (NSString *key in aDictionary.allKeys) {
        /* 取出key里的值 */
        id value = aDictionary[key];
        /*根據key在self中獲取該key的屬性指針property,判斷當前屬性是不是Model的*/
        objc_property_t property = class_getProperty(self, key.UTF8String);
        /* outCount存放你定義屬性的個數 */
        unsigned int outCount = 0;
        /* 根據指針動態取出對象所有屬性 */
        objc_property_attribute_t *attributeList = property_copyAttributeList(property, &outCount);
        /* 取出對象首個屬性 */
        objc_property_attribute_t attribute = attributeList[0];
        /* 取出屬性值 */
        NSString *typeString = [NSString stringWithUTF8String:attribute.value];
        /* 判斷取出的屬性類型是否是當前這個model類 */
        if ([typeString isEqualToString:[NSString stringWithFormat:@"@\"%@\"",self.class]]) {
          /* 如果model中有一個屬性也是model,即嵌套model。就遍歷一次屬性model  */
            value = [self objectWithKeyValues:value];
        }
        /**********************/
        //生成setter方法,并用objc_msgSend調用
        NSString *methodName = [NSString stringWithFormat:@"set%@%@:",[key substringToIndex:1].uppercaseString,[key substringFromIndex:1]];
        /* 獲取方法名 */
        SEL setter = sel_registerName(methodName.UTF8String);
        /* 判斷是否有該方法名 */
        if ([objc respondsToSelector:setter]) {
            /* 如果有就發送消息到該方法 */
            ((void (*) (id,SEL,id)) objc_msgSend) (objc,setter,value);
        }
    }
    return objc;
}
//模型轉字典
-(NSDictionary *)keyValuesWithObject{
    /* outCount存放你定義屬性的個數 */
    unsigned int outCount = 0;
    /* 取出模型中所有到屬性指針,并獲取屬性個數 */
    objc_property_t *propertyList = class_copyPropertyList([self class], &outCount);
    /* 創建一個字典 */
    NSMutableDictionary *dict = [NSMutableDictionary dictionary];
    /* 根據屬性數量遍歷 */
    for (int i = 0; i < outCount; i ++) {
        /* 取出每個屬性指針 */
        objc_property_t property = propertyList[i];
        //生成getter方法,并用objc_msgSend調用
        const char *propertyName = property_getName(property);
        /* 獲取方法名 */
        SEL getter = sel_registerName(propertyName);
        /* 判斷是否存在該方法 */
        if ([self respondsToSelector:getter]) {
            /* 如果存在就從該方法獲取值 */
            id value = ((id (*) (id,SEL)) objc_msgSend) (self,getter);
            /*判斷當前屬性是不是Model*/
            if ([value isKindOfClass:[self class]] && value) {
                /* 如果是就在遍歷一次屬性model */
                value = [value keyValuesWithObject];
            }
            /**********************/
            /* 判斷是否有值 */
            if (value) {
                /* 獲取屬性名稱到字符串 */
                NSString *key = [NSString stringWithUTF8String:propertyName];
              /* 存值 */
                [dict setObject:value forKey:key];
            }
        }
    }
    return dict;
}
@end

字典轉模型中的知識點補充

  • objc_property_t :屬性指針
    類型:
typedef struct property_t *objc_property_t;

@property 標記了類中的屬性,這個不必多說大家都很熟悉,它是一個指向objc_property 結構體的指針.可以通過 class_copyPropertyList 和 protocol_copyPropertyList 方法來獲取類和協議中的屬性

objc_property_t *class_copyPropertyList(Class cls, unsigned int *outCount)
objc_property_t *protocol_copyPropertyList(Protocol *proto, unsigned int *outCount)

返回類型為指向指針的指針,哈哈,因為屬性列表是個數組,每個元素內容都是一個 objc_property_t 指針,而這兩個函數返回的值是指向這個數組的指針。
可以用 property_getName 函數來查找屬性名稱:

const char *property_getName(objc_property_t property)

可以用class_getProperty 和 protocol_getProperty通過給出的名稱來在類和協議中獲取屬性的引用:

objc_property_t class_getProperty(Class cls, const char *name)
objc_property_t protocol_getProperty(Protocol *proto, const char *name, BOOL isRequiredProperty, BOOL isInstanceProperty)

可以用property_getAttributes函數來發掘屬性的名稱和@encode類型字符串:

const char *property_getAttributes(objc_property_t property)

對比下 class_copyIvarList 函數,使用 class_copyPropertyList 函數只能獲取類的屬性,而不包含成員變量。但此時獲取的屬性名是不帶下劃線的。

  • objc_property_attribute_t:屬性指針對象的列表
    objc_property_attribute_t的value和name
常用attribute name value
nonatomic "N" ""
strong/retain "&" ""
weak "W" ""
屬性的類型type "T" "@TypeName", eg"@"NSString""
屬性對應的實例變量Ivar "V" "Ivar_name", eg "_name"
readonly "R" ""
getter "G" "GetterName", eg"isRight"
setter "S" "SetterName", eg"setName"
assign/atomic 默認即為assign和retain
動態方法解析

如果某個對象調用了不存在的方法時會怎么樣,一般情況下程序會crash,錯誤信息類似下面這樣:

unrecognized selector sent to instance 0x7fd0a141afd0

但是在程序crash之前,Runtime 會給我們動態方法解析的機會,消息發送的步驟大致如下:

  • 1.檢測這個 selector 是不是要忽略的。比如 Mac OS X 開發,有了垃圾回收就不理會 retain,release 這些函數了

  • 2.檢測這個 target 是不是 nil 對象。ObjC 的特性是允許對一個 nil 對象執行任何一個方法不會 Crash,因為會被忽略掉

  • 3.如果上面兩個都過了,那就開始查找這個類的 IMP,先從 cache 里面找,完了找得到就跳到對應的函數去執行,如果 cache 找不到就找一下方法分發表

  • 4.如果分發表找不到就到超類的分發表去找,一直找,直到找到NSObject類為止
    如果還找不到就要開始進入消息轉發了,消息轉發的大致過程如圖:


    消息轉發
  • 1.進入 resolveInstanceMethod:方法,指定是否動態添加方法。若返回NO,則進入下一步,若返回YES,則通過 class_addMethod 函數動態地添加方法,消息得到處理,此流程完畢。
  • 2.resolveInstanceMethod: 方法返回 NO 時,就會進入 forwardingTargetForSelector:方法,這是 Runtime 給我們的第二次機會,用于指定哪個對象響應這個 selector。返回nil,進入下一步,返回某個對象,則會調用該對象的方法。
  • 3.若 forwardingTargetForSelector: 返回的是nil,則我們首先要通過 methodSignatureForSelector:來指定方法簽名,返回nil,表示不處理,若返回方法簽名,則會進入下一步。
  • 4.當第 methodSignatureForSelector: 方法返回方法簽名后,就會調用 forwardInvocation: 方法,我們可以通過 anInvocation 對象做很多處理,比如修改實現方法,修改響應對象等。如果到最后,消息還是沒有得到響應,程序就會crash

未完待續...

文章出處

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 228,546評論 6 533
  • 序言:濱河連續發生了三起死亡事件,死亡現場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發現死者居然都...
    沈念sama閱讀 98,570評論 3 418
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 176,505評論 0 376
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 63,017評論 1 313
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 71,786評論 6 410
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發上,一...
    開封第一講書人閱讀 55,219評論 1 324
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,287評論 3 441
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 42,438評論 0 288
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當地人在樹林里發現了一具尸體,經...
    沈念sama閱讀 48,971評論 1 335
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 40,796評論 3 354
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發現自己被綠了。 大學時的朋友給我發了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 42,995評論 1 369
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,540評論 5 359
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發生泄漏。R本人自食惡果不足惜,卻給世界環境...
    茶點故事閱讀 44,230評論 3 347
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 34,662評論 0 26
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 35,918評論 1 286
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 51,697評論 3 392
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 47,991評論 2 374

推薦閱讀更多精彩內容