iOS-Runtime從認(rèn)識(shí)到深入

技 術(shù) 文 章 / 超 人


Runtime(運(yùn)行時(shí)機(jī)制)概念

Object-C 是面向?qū)ο蟮恼Z(yǔ)言,C是面向結(jié)構(gòu)也就是面向過(guò)程的語(yǔ)言。Object-C是基于C基礎(chǔ)上的語(yǔ)言。而Runtime為Object-C添加了面向?qū)ο蟮奶匦裕瑢㈧o態(tài)語(yǔ)言編譯期和鏈接期做的事情放到了運(yùn)行時(shí)來(lái)處理。所以Runtime是每一個(gè)IOS開(kāi)發(fā)人員都應(yīng)該了解的。Runtime是一套純C的API。

  • Objective-C不僅需要一個(gè)編譯器,還需要一個(gè)運(yùn)行時(shí)系統(tǒng)來(lái)執(zhí)行編譯的代碼。對(duì)于Objective-C來(lái)說(shuō),這個(gè)運(yùn)行時(shí)系統(tǒng)就像一個(gè)操作系統(tǒng)一樣:它讓所有的工作可以正常的運(yùn)行。這個(gè)運(yùn)行時(shí)系統(tǒng)即Objc Runtime。Objc Runtime其實(shí)是一個(gè)Runtime庫(kù),它基本上是用C和匯編寫的,這個(gè)庫(kù)使得C語(yǔ)言有了面向?qū)ο蟮哪芰Α?/p>

  • Runtime庫(kù)主要做下面幾件事:
    封裝:在這個(gè)庫(kù)中,對(duì)象可以用C語(yǔ)言中的結(jié)構(gòu)體表示,而方法可以用C函數(shù)來(lái)實(shí)現(xiàn),另外再加上了一些額外的特性。這些結(jié)構(gòu)體和函數(shù)被runtime函數(shù)封裝后,我們就可以在程序運(yùn)行時(shí)創(chuàng)建,檢查,修改類、對(duì)象和它們的方法了。
    找出方法的最終執(zhí)行代碼:當(dāng)程序執(zhí)行[object doSomething]時(shí),會(huì)向消息接收者(object)發(fā)送一條消息(doSomething),runtime會(huì)根據(jù)消息接收者是否能響應(yīng)該消息而做出不同的反應(yīng)。這將在后面詳細(xì)介紹。

  • 對(duì)于C語(yǔ)言而言函數(shù)的調(diào)用在編譯時(shí)就會(huì)決定調(diào)用那一個(gè)函數(shù)。而OC在編譯時(shí)并不能決定調(diào)用那一個(gè)函數(shù),只有在運(yùn)行時(shí)才能確定調(diào)用那一個(gè)函數(shù),舉個(gè)簡(jiǎn)單的例子:在日常開(kāi)發(fā)時(shí),在.h中聲明了某個(gè)方法,而.m中卻沒(méi)有實(shí)現(xiàn)該方法,但編譯項(xiàng)目卻不會(huì)報(bào)錯(cuò),因?yàn)镺C編譯時(shí)并沒(méi)有確定.h中聲明但方法具體調(diào)用.m的那一個(gè)函數(shù),當(dāng)然也就不知道該函數(shù)有沒(méi)有實(shí)現(xiàn)了,而在運(yùn)行時(shí)調(diào)用該函數(shù)時(shí)就會(huì)報(bào)錯(cuò),因?yàn)檫\(yùn)行時(shí)才會(huì)確定具體調(diào)用那一個(gè)函數(shù),函數(shù)沒(méi)有實(shí)現(xiàn)那么就會(huì)報(bào)錯(cuò)。OC是動(dòng)態(tài)調(diào)用,正因?yàn)閯?dòng)態(tài)性,所以在運(yùn)行時(shí)可以修改調(diào)用方法的指針。

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

  • Runtime 基本是用 C 和匯編寫的,可見(jiàn)蘋果為了動(dòng)態(tài)系統(tǒng)的高效而作出的努力。你可以在這里下到蘋果維護(hù)的開(kāi)源代碼。蘋果和GNU各自維護(hù)一個(gè)開(kāi)源的 runtime 版本,這兩個(gè)版本之間都在努力的保持一致。

基礎(chǔ)知識(shí)補(bǔ)充

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

受限于內(nèi)存分配的機(jī)制,一個(gè)NSObject對(duì)象會(huì)分配16byte的內(nèi)存空間,但在64位系統(tǒng)下只使用了8byte,在32位系統(tǒng)下只使用了4byte.

獲取Obj-C指針?biāo)赶虻膬?nèi)存的大小

malloc_size((__brige const void *)obj);

一個(gè)NSObject實(shí)例對(duì)象成員變量所占的大小是8byte。
獲取Object對(duì)象大小方法

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

看到這里。你應(yīng)該對(duì)Runtime有一個(gè)初步的了解了。建議先看看Runtime的方法,在看下面的內(nèi)容。這樣才會(huì)容易明白實(shí)例代碼的用意。請(qǐng)查看Runtime方法說(shuō)明


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

Runtime最主要是消息機(jī)制,OC在編譯時(shí)會(huì)轉(zhuǎn)化為C,轉(zhuǎn)化為: objc_msgSend(receiver, selector),何以證明?新建一個(gè)類 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

然后在同一目錄下會(huì)多出一個(gè) MyClass.cpp 文件,雙擊打開(kāi),可以看到 init 方法已經(jīng)被編譯器轉(zhuǎn)化為下面這樣:

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 函數(shù)被定義在 objc/message.h 目錄下,其函數(shù)原型是醬紫滴:

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

該函數(shù)有兩個(gè)參數(shù),一個(gè) id 類型,一個(gè) SEL 類型。

SEL

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

typedef struct objc_selector *SEL;

其實(shí)它就是個(gè)映射到方法的C字符串,你可以用 Objective-C 編譯器命令 @selector() 或者 Runtime 系統(tǒng)的 sel_registerName 函數(shù)來(lái)獲得一個(gè) SEL 類型的方法選擇器。簡(jiǎn)單來(lái)說(shuō)就是方法名稱

id

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

typedef struct objc_object *id;

id 是一個(gè)結(jié)構(gòu)體指針類型,它可以指向 Objective-C 中的任何對(duì)象。objc_object 結(jié)構(gòu)體定義如下:

struct objc_object { Class isa OBJC_ISA_AVAILABILITY;};

我們通常所說(shuō)的對(duì)象,就長(zhǎng)這個(gè)樣子,這個(gè)結(jié)構(gòu)體只有一個(gè)成員變量 isa,對(duì)象可以通過(guò) isa 指針找到其所屬的類。isa 是一個(gè) Class 類型的成員變量,那么 Class 又是什么呢?

isa 等價(jià)于 isKindOf方法

  • 實(shí)例對(duì)象 isa 指向類對(duì)象
  • 類對(duì)象指 isa 向元類對(duì)象
  • 元類對(duì)象的 isa 指向元類的基類

isa 有兩種類型

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

Class

Class 也是一個(gè)結(jié)構(gòu)體指針類型:

typedef struct objc_class *Class;

objc_class 結(jié)構(gòu)體是醬紫滴:

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;

我們通常說(shuō)的類就長(zhǎng)這樣子:

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

objc_object與id

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

可以看到,這個(gè)結(jié)構(gòu)體只有一個(gè)字體,即指向其類的isa指針。這樣,當(dāng)我們向一個(gè)Objective-C對(duì)象發(fā)送消息時(shí),運(yùn)行時(shí)庫(kù)會(huì)根據(jù)實(shí)例對(duì)象的isa指針找到這個(gè)實(shí)例對(duì)象所屬的類。Runtime庫(kù)會(huì)在類的方法列表及父類的方法列表中去尋找與消息對(duì)應(yīng)的selector指向的方法。找到后即運(yùn)行這個(gè)方法。

當(dāng)創(chuàng)建一個(gè)特定類的實(shí)例對(duì)象時(shí),分配的內(nèi)存包含一個(gè)objc_object數(shù)據(jù)結(jié)構(gòu),然后是類的實(shí)例變量的數(shù)據(jù)。NSObject類的alloc和allocWithZone:方法使用函數(shù)class_createInstance來(lái)創(chuàng)建objc_object數(shù)據(jù)結(jié)構(gòu)。

另外還有我們常見(jiàn)的id,它是一個(gè)objc_object結(jié)構(gòu)類型的指針。它的存在可以讓我們實(shí)現(xiàn)類似于C++中泛型的一些操作。該類型的對(duì)象可以轉(zhuǎn)換為任何一種對(duì)象,有點(diǎn)類似于C語(yǔ)言中void *指針類型的作用。

objc_cache
上面提到了objc_class結(jié)構(gòu)體中的cache字段,它用于緩存調(diào)用過(guò)的方法。這個(gè)字段是一個(gè)指向objc_cache結(jié)構(gòu)體的指針,其定義如下:

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

  • occupied:一個(gè)整數(shù),指定實(shí)際占用的緩存bucket的總數(shù)。

  • buckets:指向Method數(shù)據(jù)結(jié)構(gòu)指針的數(shù)組。這個(gè)數(shù)組可能包含不超過(guò)mask+1個(gè)元素。需要注意的是,指針可能是NULL,表示這個(gè)緩存bucket沒(méi)有被占用,另外被占用的bucket可能是不連續(xù)的。這個(gè)數(shù)組可能會(huì)隨著時(shí)間而增長(zhǎng)

元類(Meta Class)

例:

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

  • meta-class是一個(gè)類對(duì)象的類。

  • 當(dāng)我們向一個(gè)對(duì)象發(fā)送消息時(shí),runtime會(huì)在這個(gè)對(duì)象所屬的這個(gè)類的方法列表中查找方法;而向一個(gè)類發(fā)送消息時(shí),會(huì)在這個(gè)類的meta-class的方法列表中查找。

  • meta-class之所以重要,是因?yàn)樗鎯?chǔ)著一個(gè)類的所有類方法。每個(gè)類都會(huì)有一個(gè)單獨(dú)的meta-class,因?yàn)槊總€(gè)類的類方法基本不可能完全相同。

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

IMP
IMP就是Implementation的縮寫,顧名思義,它是指向一個(gè)方法實(shí)現(xiàn)的指針,每一個(gè)方法都有一個(gè)對(duì)應(yīng)的IMP,所以,我們可以直接調(diào)用方法的IMP指針,來(lái)避免方法調(diào)用死循環(huán)的問(wèn)題。

IMP的定義如下:

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

實(shí)際上直接調(diào)用一個(gè)方法的IMP指針的效率是高于調(diào)用方法本身的,所以,如果你有一個(gè)合適的時(shí)機(jī)獲取到方法的IMP的話,你可以試著調(diào)用它。

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

首先,Runtime 系統(tǒng)會(huì)把方法調(diào)用轉(zhuǎn)化為消息發(fā)送,即 objc_msgSend,并且把方法的調(diào)用者,和方法選擇器,當(dāng)做參數(shù)傳遞過(guò)去.

此時(shí),方法的調(diào)用者會(huì)通過(guò) isa 指針來(lái)找到其所屬的類,然后在 cache 或者 methodLists 中查找該方法,找得到就跳到對(duì)應(yīng)的方法去執(zhí)行。

如果在類中沒(méi)有找到該方法,則通過(guò) super_class 往上一級(jí)超類查找(如果一直找到 NSObject 都沒(méi)有找到該方法的話,這種情況,我們放到后面消息轉(zhuǎn)發(fā)的時(shí)候再說(shuō))。

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


指針

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

前面我們使用 clang 重寫命令,看到 Runtime 是如何將方法調(diào)用轉(zhuǎn)化為消息發(fā)送的。我們也可以依樣畫葫蘆,來(lái)學(xué)習(xí)使用一下 objc_msgSend。新建一個(gè)類 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 依次調(diào)用這些方法:

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

也許你已經(jīng)注意到,objc_msgSend 在使用時(shí)都被強(qiáng)制轉(zhuǎn)換了一下,這是因?yàn)?objc_msgSend 函數(shù)可以hold住各種不同的返回值以及多個(gè)參數(shù),但默認(rèn)情況下是沒(méi)有參數(shù)和返回值的。如果我們把調(diào)用 showAge 方法改成這樣:

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

Xcode 就會(huì)報(bào)錯(cuò):

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

objc_msgSendSuper
編譯器會(huì)根據(jù)情況在 objc_msgSend,objc_msgSend_stret,objc_msgSendSuper,objc_msgSendSuper_stret 或 objc_msgSend_fpret 五個(gè)方法中選擇一個(gè)來(lái)調(diào)用。如果消息是傳遞給超類,那么會(huì)調(diào)用 objc_msgSendSuper 方法,如果消息返回值是數(shù)據(jù)結(jié)構(gòu),就會(huì)調(diào)用 objc_msgSendSuper_stret 方法,如果返回值是浮點(diǎn)數(shù),則調(diào)用 objc_msgSend_fpret 方法。

這里我們重點(diǎn)說(shuō)一下 objc_msgSendSuper,objc_msgSendSuper 函數(shù)原型如下:

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

當(dāng)我們調(diào)用 [super selector] 時(shí),Runtime 會(huì)調(diào)用 objc_msgSendSuper 方法,objc_msgSendSuper 方法有兩個(gè)參數(shù),super 和 op,Runtime 會(huì)把 selector 方法選擇器賦值給 op。而 super 是一個(gè) objc_super 結(jié)構(gòu)體指針,objc_super 結(jié)構(gòu)體定義如下:

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 會(huì)創(chuàng)建一個(gè) objc_spuer 結(jié)構(gòu)體變量,將其地址作為參數(shù)(super)傳遞給 objc_msgSendSuper,并且將 self 賦值給 receiver:super—>receiver=self.

舉個(gè)栗子,問(wèn)下面的代碼輸出什么:

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

答案是全部輸出 Son。

使用 clang 重寫命令,發(fā)現(xiàn)上述代碼被轉(zhuǎn)化為:

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"))));

當(dāng)調(diào)用 [super class] 時(shí),會(huì)轉(zhuǎn)換成 objc_msgSendSuper 函數(shù):

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

第二步是去 Father 這個(gè)類里去找 - (Class)class,沒(méi)有,然后去 NSObject 類去找,找到了。最后內(nèi)部是使用 objc_msgSend(objc_super->receiver, @selector(class)) 去調(diào)用,此時(shí)已經(jīng)和 [self class] 調(diào)用相同了,所以兩個(gè)輸出結(jié)果都是 Son。

Associated對(duì)象關(guān)聯(lián)
對(duì)象關(guān)聯(lián)允許開(kāi)發(fā)者對(duì)已經(jīng)存在的類在 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 是源對(duì)象

  • value 是被關(guān)聯(lián)的對(duì)象

  • key 是關(guān)聯(lián)的鍵,objc_getAssociatedObject 方法通過(guò)不同的 key 即可取出對(duì)應(yīng)的被關(guān)聯(lián)對(duì)象

  • policy 是一個(gè)枚舉值,表示關(guān)聯(lián)對(duì)象的行為,從命名就能看出各個(gè)枚舉值的含義:

typedef OBJC_ENUM(uintptr_t, objc_AssociationPolicy) {
   OBJC_ASSOCIATION_ASSIGN = 0,           /** 指定對(duì)關(guān)聯(lián)對(duì)象的弱引用. */
   OBJC_ASSOCIATION_RETAIN_NONATOMIC = 1, /** 指定對(duì)關(guān)聯(lián)對(duì)象的強(qiáng)引.不是atomically */
   OBJC_ASSOCIATION_COPY_NONATOMIC = 3,   /** 指定復(fù)制的關(guān)聯(lián)對(duì)象.*   不是 atomically. */
   OBJC_ASSOCIATION_RETAIN = 01401,       /** 指定對(duì)關(guān)聯(lián)對(duì)象的強(qiáng)引用.*   是 atomically. */
   OBJC_ASSOCIATION_COPY = 01403          /** 指定復(fù)制的關(guān)聯(lián)對(duì)象. *   是 atomically. */
};

要取出被關(guān)聯(lián)的對(duì)象使用 objc_getAssociatedObject 方法即可,要?jiǎng)h除一個(gè)被關(guān)聯(lián)的對(duì)象,使用 objc_setAssociatedObject 方法將對(duì)應(yīng)的 key 設(shè)置成 nil 即可:

objc_setAssociatedObject(self, associatedKey, nil, OBJC_ASSOCIATION_COPY_NONATOMIC);

objc_removeAssociatedObjects 方法將會(huì)移除源對(duì)象中所有的關(guān)聯(lián)對(duì)象.

舉個(gè)栗子,假如我們要給 UIButton 添加一個(gè)監(jiān)聽(tīng)單擊事件的 block 屬性,新建 UIButton 的 Category,其.m文件如下:

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

然后在代碼中,就可以使用 UIButton 的屬性來(lái)監(jiān)聽(tīng)單擊事件了:

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

Runtime案例使用

自動(dòng)歸檔

一般到歸檔寫法時(shí)這樣的:

- (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;
}

那么問(wèn)題來(lái)了,如果當(dāng)前 Model 有100個(gè)屬性的話,就需要寫100行這種代碼:

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

想想都頭疼,通過(guò) Runtime 我們就可以輕松解決這個(gè)問(wèn)題:

1.使用 class_copyIvarList 方法獲取當(dāng)前 Model 的所有成員變量.

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

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

舉個(gè)栗子,新建一個(gè) 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這個(gè)屬性的setter方法,則調(diào)用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 定義對(duì)象的實(shí)例變量,包括類型和名字
是objc_ivar的指針,包含變量名,變量類型等成員。ivars是一個(gè)數(shù)組,數(shù)組中每個(gè)元素是指向Ivar(變量信息)的指針

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

一般的字典轉(zhuǎn)模型是這樣滴:

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

可想而知,遇到的問(wèn)題跟歸檔時(shí)候一樣(后來(lái)使用MJExtension),這里我們稍微來(lái)學(xué)習(xí)一下其中原理,字典轉(zhuǎn)模型的時(shí)候:

1.根據(jù)字典的 key 生成 setter 方法

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

模型轉(zhuǎn)字典的時(shí)候:

1.調(diào)用 class_copyPropertyList 方法獲取當(dāng)前 Model 的所有屬性

2.調(diào)用 property_getName 獲取屬性名稱

3.根據(jù)屬性名稱生成 getter 方法

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

代碼如下:

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

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

字典轉(zhuǎn)模型中的知識(shí)點(diǎn)補(bǔ)充

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

@property 標(biāo)記了類中的屬性,這個(gè)不必多說(shuō)大家都很熟悉,它是一個(gè)指向objc_property 結(jié)構(gòu)體的指針.可以通過(guò) class_copyPropertyList 和 protocol_copyPropertyList 方法來(lái)獲取類和協(xié)議中的屬性

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

返回類型為指向指針的指針,哈哈,因?yàn)閷傩粤斜硎莻€(gè)數(shù)組,每個(gè)元素內(nèi)容都是一個(gè) objc_property_t 指針,而這兩個(gè)函數(shù)返回的值是指向這個(gè)數(shù)組的指針。
可以用 property_getName 函數(shù)來(lái)查找屬性名稱:

const char *property_getName(objc_property_t property)

可以用class_getProperty 和 protocol_getProperty通過(guò)給出的名稱來(lái)在類和協(xié)議中獲取屬性的引用:

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函數(shù)來(lái)發(fā)掘?qū)傩缘拿Q和@encode類型字符串:

const char *property_getAttributes(objc_property_t property)

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

  • objc_property_attribute_t:屬性指針對(duì)象的列表
    objc_property_attribute_t的value和name
常用attribute name value
nonatomic "N" ""
strong/retain "&" ""
weak "W" ""
屬性的類型type "T" "@TypeName", eg"@"NSString""
屬性對(duì)應(yīng)的實(shí)例變量Ivar "V" "Ivar_name", eg "_name"
readonly "R" ""
getter "G" "GetterName", eg"isRight"
setter "S" "SetterName", eg"setName"
assign/atomic 默認(rèn)即為assign和retain
動(dòng)態(tài)方法解析

如果某個(gè)對(duì)象調(diào)用了不存在的方法時(shí)會(huì)怎么樣,一般情況下程序會(huì)crash,錯(cuò)誤信息類似下面這樣:

unrecognized selector sent to instance 0x7fd0a141afd0

但是在程序crash之前,Runtime 會(huì)給我們動(dòng)態(tài)方法解析的機(jī)會(huì),消息發(fā)送的步驟大致如下:

  • 1.檢測(cè)這個(gè) selector 是不是要忽略的。比如 Mac OS X 開(kāi)發(fā),有了垃圾回收就不理會(huì) retain,release 這些函數(shù)了

  • 2.檢測(cè)這個(gè) target 是不是 nil 對(duì)象。ObjC 的特性是允許對(duì)一個(gè) nil 對(duì)象執(zhí)行任何一個(gè)方法不會(huì) Crash,因?yàn)闀?huì)被忽略掉

  • 3.如果上面兩個(gè)都過(guò)了,那就開(kāi)始查找這個(gè)類的 IMP,先從 cache 里面找,完了找得到就跳到對(duì)應(yīng)的函數(shù)去執(zhí)行,如果 cache 找不到就找一下方法分發(fā)表

  • 4.如果分發(fā)表找不到就到超類的分發(fā)表去找,一直找,直到找到NSObject類為止
    如果還找不到就要開(kāi)始進(jìn)入消息轉(zhuǎn)發(fā)了,消息轉(zhuǎn)發(fā)的大致過(guò)程如圖:


    消息轉(zhuǎn)發(fā)
  • 1.進(jìn)入 resolveInstanceMethod:方法,指定是否動(dòng)態(tài)添加方法。若返回NO,則進(jìn)入下一步,若返回YES,則通過(guò) class_addMethod 函數(shù)動(dòng)態(tài)地添加方法,消息得到處理,此流程完畢。
  • 2.resolveInstanceMethod: 方法返回 NO 時(shí),就會(huì)進(jìn)入 forwardingTargetForSelector:方法,這是 Runtime 給我們的第二次機(jī)會(huì),用于指定哪個(gè)對(duì)象響應(yīng)這個(gè) selector。返回nil,進(jìn)入下一步,返回某個(gè)對(duì)象,則會(huì)調(diào)用該對(duì)象的方法。
  • 3.若 forwardingTargetForSelector: 返回的是nil,則我們首先要通過(guò) methodSignatureForSelector:來(lái)指定方法簽名,返回nil,表示不處理,若返回方法簽名,則會(huì)進(jìn)入下一步。
  • 4.當(dāng)?shù)?methodSignatureForSelector: 方法返回方法簽名后,就會(huì)調(diào)用 forwardInvocation: 方法,我們可以通過(guò) anInvocation 對(duì)象做很多處理,比如修改實(shí)現(xiàn)方法,修改響應(yīng)對(duì)象等。如果到最后,消息還是沒(méi)有得到響應(yīng),程序就會(huì)crash

未完待續(xù)...

文章出處

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

推薦閱讀更多精彩內(nèi)容

  • 一. runtime簡(jiǎn)介 runtime簡(jiǎn)稱運(yùn)行時(shí),是一套底層的 C 語(yǔ)言 API。OC就是運(yùn)行時(shí)機(jī)制,運(yùn)行時(shí)機(jī)制...
    沈楓_ShenF閱讀 722評(píng)論 0 2
  • 什么是runtime? runtime 是 OC底層的一套C語(yǔ)言的API(引入 <objc/runtime.h>或...
    木子奕閱讀 1,616評(píng)論 0 16
  • 面試題 講一下 OC 的消息機(jī)制OC中的方法調(diào)用其實(shí)都是轉(zhuǎn)成了objc_msgSend函數(shù)的調(diào)用,給receive...
    e297b14c9e53閱讀 336評(píng)論 0 0
  • 一、概述 Objective-C 是一個(gè)動(dòng)態(tài)語(yǔ)言,這意味著它需要一個(gè)編譯器,也需要一個(gè)運(yùn)行時(shí)系統(tǒng)來(lái)動(dòng)態(tài)得創(chuàng)建類和對(duì)...
    inyourface閱讀 127評(píng)論 0 0
  • 一. RunTime簡(jiǎn)介 RunTime簡(jiǎn)稱運(yùn)行時(shí)。OC就是運(yùn)行時(shí)機(jī)制,也就是在運(yùn)行時(shí)候的一些機(jī)制,其中最主要的是...
    xx_cc閱讀 13,718評(píng)論 43 170