runtime---簡單學(xué)習(xí)筆記

前言


runtime其實在我們?nèi)粘i_發(fā)過程中很少使用到,尤其是像我現(xiàn)在比較初級的程序猿就更用不到了。但是去面試很多面試官都會問這個問題,這就很尷尬了,這就有一種裝逼的感覺,問這些對于現(xiàn)在的我們根本用不上的東西,要是很懂,那我們完全就是大牛了嘛。但是,話雖如此,runtime是object-c底層實現(xiàn)核心,如果我們明白了,對于以后我們解決問題,理解問題有很大的幫助,所以出于什么心態(tài)我們都應(yīng)該有必要去了解,甚至在一定時候去深入理解它(這個就是以后你欲求不滿,追求技術(shù)的最高境界時~~~)。

思考


1 runtime時候干嗎的?
2 為什么要有runtime?

開始了解runtime


簡單的理解:

眾所周知,簡單而高深的C語言是面向過程的編程語言,程序是按順序依次執(zhí)行,一直到結(jié)束。而在這就為了便于說明就不得不提c++這門語言,c++和object-c兩門語言都思想差不多,都是面向?qū)ο缶幊痰模际腔贑語言加入面向?qū)ο蟮乃枷?,但是c++和object-c在實現(xiàn)機制上有很大的差別,C++是一門靜態(tài)語言,簡而言之就是在編譯的時候編譯器就已經(jīng)把函數(shù)的地址編碼進可執(zhí)行文件了,這個時候函數(shù)調(diào)用就知道調(diào)用那段代碼了。而object-c和C++是相反的,object-c是一門動態(tài)語言,編譯器無法將函數(shù)的地址編碼進可執(zhí)行文件的,所以不僅需要一個編譯器,也需要一個運行時系統(tǒng)來動態(tài)得創(chuàng)建類和對象、進行消息傳遞和轉(zhuǎn)發(fā),而是在運行的時候根據(jù)runtime條件判斷,再去找相應(yīng)的調(diào)用的那段代碼。
總結(jié):
1 Objective-C Runtime是一個將C語言轉(zhuǎn)化為面向?qū)ο笳Z言的擴展。
2 Objective-C是基于C語言加入了面向?qū)ο筇匦院拖⑥D(zhuǎn)發(fā)機制的動態(tài)語言,這意味著它不僅需要一個編譯器,還需要Runtime系統(tǒng)來動態(tài)創(chuàng)建類和對象,進行消息發(fā)送和轉(zhuǎn)發(fā)。
3 [receiver message]真的不是一個簡簡單單的方法調(diào)用。因為這只是在編譯階段確定了要向接收者發(fā)送message這條消息,而receive將要如何響應(yīng)這條消息,那就要看運行時發(fā)生的情況來決定了。
4 因為Objc是一門動態(tài)語言,所以它總是想辦法把一些決定工作從編譯連接推遲到運行時。也就是說只有編譯器是不夠的,還需要一個運行時系統(tǒng) (runtime system) 來執(zhí)行編譯后的代碼。

與Runtime交互


Objc 從三種不同的層級上與 Runtime 系統(tǒng)進行交互,分別是通過 Objective-C 源代碼,通過 Foundation 框架的NSObject類定義的方法,通過對 runtime 函數(shù)的直接調(diào)用。

  • Objective-C源代碼
    大部分情況下你就只管寫你的Objc代碼就行,runtime 系統(tǒng)自動在幕后辛勤勞作著。
    還記得引言中舉的例子吧,消息的執(zhí)行會使用到一些編譯器為實現(xiàn)動態(tài)語言特性而創(chuàng)建的數(shù)據(jù)結(jié)構(gòu)和函數(shù),Objc中的類、方法和協(xié)議等在 runtime 中都由一些數(shù)據(jù)結(jié)構(gòu)來定義,這些內(nèi)容在后面會講到。(比如objc_msgSend函數(shù)及其參數(shù)列表中的id和SEL都是啥)
  • NSObject的方法
    Cocoa 中大多數(shù)類都繼承于NSObject類,也就自然繼承了它的方法。最特殊的例外是NSProxy,它是個抽象超類,它實現(xiàn)了一些消息轉(zhuǎn)發(fā)有關(guān)的方法,可以通過繼承它來實現(xiàn)一個其他類的替身類或是虛擬出一個不存在的類,說白了就是領(lǐng)導(dǎo)把自己展現(xiàn)給大家風(fēng)光無限,但是把活兒都交給幕后小弟去干。
    有的NSObject中的方法起到了抽象接口的作用,比如description方法需要你重載它并為你定義的類提供描述內(nèi)容。NSObject還有些方法能在運行時獲得類的信息,并檢查一些特性,比如class返回對象的類;isKindOfClass:和isMemberOfClass:則檢查對象是否在指定的類繼承體系中;respondsToSelector:檢查對象能否響應(yīng)指定的消息;conformsToProtocol:檢查對象是否實現(xiàn)了指定協(xié)議類的方法;methodForSelector:則返回指定方法實現(xiàn)的地址。
  • Runtime的函數(shù)
    Runtime 系統(tǒng)是一個由一系列函數(shù)和數(shù)據(jù)結(jié)構(gòu)組成,具有公共接口的動態(tài)共享庫。頭文件存放于/usr/include/objc目錄下。許多函數(shù)允許你用純C代碼來重復(fù)實現(xiàn) Objc 中同樣的功能。雖然有一些方法構(gòu)成了NSObject類的基礎(chǔ),但是你在寫 Objc 代碼時一般不會直接用到這些函數(shù)的,除非是寫一些 Objc 與其他語言的橋接或是底層的debug工作。在Objective-C Runtime Reference中有對 Runtime 函數(shù)的詳細文檔。

runtime術(shù)語了解


id objc_msgSend ( id self, SEL op, ... );

這是個最基本的用于發(fā)送消息的函數(shù)。
其實編譯器會根據(jù)情況在objc_msgSend, objc_msgSend_stret,,objc_msgSendSuper, 或 objc_msgSendSuper_stret 四個方法中選擇一個來調(diào)用。如果消息是傳遞給超類,那么會調(diào)用名字帶有 Super 的函數(shù);如果消息返回值是數(shù)據(jù)結(jié)構(gòu)而不是簡單值時,那么會調(diào)用名字帶有stret的函數(shù)。
例子:

objc_msgSend(person,@selector(sayHello));

簡單過程如下:
第一個參數(shù)是要發(fā)送消息的實例,也就是 person 對象。 objc_msgSend 會先查詢它的 methodLists 方法列表,使用第二個參數(shù) sayHello 逐個和 person 的 methodLists 中的每一個方法信息的 SEL 進行對比,如果找到對應(yīng)的方法,就調(diào)用它所對應(yīng)的函數(shù),也就是 IMP,然后調(diào)用這個函數(shù)。

  • id和Class
    objc_msgSend第一個參數(shù)類型為id,大家對它都不陌生,它是一個指向類實例的指針:
typedef struct objc_object *id;

那objc_object又是啥呢:

struct objc_object { Class isa; };

Class是一個指向objc_class結(jié)構(gòu)體的指針,而id是一個指向objc_object結(jié)構(gòu)體的指針,其中的isa是一個指向objc_class結(jié)構(gòu)體的指針。其中的id就是我們所說的對象,Class就是我們所說的類。
PS:isa指針不總是指向?qū)嵗龑ο笏鶎俚念?,不能依靠它來確定類型,而是應(yīng)該用class方法來確定實例對象的類。因為KVO的實現(xiàn)機理就是將被觀察對象的isa指針指向一個中間類而不是真實的類,這是一種叫做 isa-swizzling 的技術(shù).
objc_class定義如下:

typedef struct objc_class *Class;
struct objc_class { 
 Class isa                                 OBJC_ISA_AVAILABILITY; // metaclass
#if !__OBJC2__
 Class super_class                         OBJC2_UNAVAILABLE; // 父類
 const char *name                          OBJC2_UNAVAILABLE; // 類名
 long version                              OBJC2_UNAVAILABLE; // 類的版本信息,默認(rèn)為0,可以通過runtime函數(shù)class_setVersion或者class_getVersion進行修改、讀取
 long info                                 OBJC2_UNAVAILABLE; // 類信息,供運行時期使用的一些位標(biāo)識,如CLS_CLASS (0x1L) 表示該類為普通 class,其中包含實例方法和變量;CLS_META (0x2L) 表示該類為 metaclass,其中包含類方法;
 long instance_size                        OBJC2_UNAVAILABLE; // 該類的實例變量大?。ò◤母割惱^承下來的實例變量)
 struct objc_ivar_list *ivars              OBJC2_UNAVAILABLE; // 該類的成員變量地址列表
 struct objc_method_list **methodLists     OBJC2_UNAVAILABLE; // 方法地址列表,與 info 的一些標(biāo)志位有關(guān),如CLS_CLASS (0x1L),則存儲實例方法,如CLS_META (0x2L),則存儲類方法;
 struct objc_cache *cache                  OBJC2_UNAVAILABLE; // 緩存最近使用的方法地址,用于提升效率;
 struct objc_protocol_list *protocols      OBJC2_UNAVAILABLE; // 存儲該類聲明遵守的協(xié)議的列表
#endif
}
/* Use `Class` instead of `struct objc_class *` */

不知道你是否注意到了objc_class中也有一個isa對象,這是因為一個 ObjC 類本身同時也是一個對象,為了處理類和對象的關(guān)系,runtime 庫創(chuàng)建了一種叫做元類 (Meta Class) 的東西,類對象所屬類型就叫做元類,它用來表述類對象本身所具備的元數(shù)據(jù)。類方法就定義于此處,因為這些方法可以理解成類對象的實例方法。每個類僅有一個類對象,而每個類對象僅有一個與之相關(guān)的元類。當(dāng)你發(fā)出一個類似[NSObject alloc]的消息時,你事實上是把這個消息發(fā)給了一個類對象 (Class Object) ,這個類對象必須是一個元類的實例,而這個元類同時也是一個根元類 (root meta class) 的實例。所有的元類最終都指向根元類為其超類。所有的元類的方法列表都有能夠響應(yīng)消息的類方法。所以當(dāng) [NSObject alloc] 這條消息發(fā)給類對象的時候,objc_msgSend()會去它的元類里面去查找能夠響應(yīng)消息的方法,如果找到了,然后對這個類對象執(zhí)行方法調(diào)用。
由以上代碼可見,類與對象的區(qū)別就是類比對象多了很多特征成員,類也可以當(dāng)做一個objc_object來對待,也就是說類和對象都是對象,分別稱作類對象(class object)和實例對象(instance object),這樣我們就可以區(qū)別對象和類了。
isa:objc_object(實例對象)中isa指針指向的類結(jié)構(gòu)稱為class(也就是該對象所屬的類)其中存放著普通成員變量與動態(tài)方法(“-”開頭的方法);此處isa指針指向的類結(jié)構(gòu)稱為metaclass,其中存放著static類型的成員變量與static類型的方法(“+”開頭的方法)。
super_class: 指向該類的父類的指針,如果該類是根類(如NSObject或NSProxy),那么super_class就為nil。
類與對象的繼承層次關(guān)系如下圖:

class-diagram

  • SEL
    objc_msgSend函數(shù)第二個參數(shù)類型為SEL,它是selector在Objc中的表示類型(Swift中是Selector類)。selector是方法選擇器,可以理解為區(qū)分方法的 ID,而這個 ID 的數(shù)據(jù)結(jié)構(gòu)是SEL:
typedef struct objc_selector *SEL;

objc_selector的定義如下:

struct objc_selector {
    char *name;                       OBJC2_UNAVAILABLE;// 名稱
    char *types;                      OBJC2_UNAVAILABLE;// 類型
};

其實它就是個映射到方法的C字符串,你可以用 Objc 編譯器命令@selector()或者 Runtime 系統(tǒng)的sel_registerName函數(shù)來獲得一個SEL類型的方法選擇器。
不同類中相同名字的方法所對應(yīng)的方法選擇器是相同的,即使方法名字相同而變量類型不同也會導(dǎo)致它們具有相同的方法選擇器,于是 Objc 中方法命名有時會帶上參數(shù)類型(NSNumber一堆抽象工廠方法拿走不謝),Cocoa 中有好多長長的方法哦。
如果你知道selector對應(yīng)的方法名是什么,可以通過NSString* NSStringFromSelector(SEL aSelector)方法將SEL轉(zhuǎn)化為字符串,再用NSLog打印。

  • Method
    Method是一種代表類中的某個方法的類型。
typedef struct objc_method *Method;

而objc_method在上面的方法列表中提到過,它存儲了方法名,方法類型和方法實現(xiàn):

struct objc_method {
    SEL method_name                                      OBJC2_UNAVAILABLE;
    char *method_types                                   OBJC2_UNAVAILABLE;
    IMP method_imp                                       OBJC2_UNAVAILABLE;
}  

方法名類型為SEL,前面提到過相同名字的方法即使在不同類中定義,它們的方法選擇器也相同。
方法類型method_types是個char指針,其實存儲著方法的參數(shù)類型和返回值類型。
最后一個參數(shù)是 IMP 類型,它表示這個 Selector 對應(yīng)的函數(shù)的地址。 對,是函數(shù)沒錯。 Objective-C 中定義的所有類的方法在底層實現(xiàn)上就是一個函數(shù)。

  • methodLists
struct objc_method_list **methodLists

methodLists 屬性表示當(dāng)前實例的方法列表,它是一個 objc_method_list 類型的結(jié)構(gòu):

struct objc_method_list {
    struct objc_method_list *obsolete                        OBJC2_UNAVAILABLE;

    int method_count                                         OBJC2_UNAVAILABLE;
#ifdef __LP64__
    int space                                                OBJC2_UNAVAILABLE;
#endif
    /* variable length structure */
    struct objc_method method_list[1]                        OBJC2_UNAVAILABLE;
}

這個結(jié)構(gòu)的定義可能大家會有些地方不太明白,比如 space 屬性是干什么的。咱們可以暫時拋開這些問題,只關(guān)心和消息分發(fā)相關(guān)的屬性 - method_count 屬性表示當(dāng)前這個實例中方法的個數(shù),method_list 結(jié)構(gòu)表示當(dāng)前實例上面所有方法的列表:

struct objc_method method_list[1]
  • Ivar
    Ivar是一種代表類中實例變量的類型。
typedef struct objc_ivar *Ivar;

objc_ivar的數(shù)據(jù)結(jié)構(gòu)如下:

struct objc_ivar {
    char *ivar_name                   OBJC2_UNAVAILABLE; // 變量名
    char *ivar_type                   OBJC2_UNAVAILABLE; // 變量類型
    int ivar_offset                   OBJC2_UNAVAILABLE; // 基地址偏移字節(jié)
#ifdef __LP64__
    int space                         OBJC2_UNAVAILABLE; // 占用空間
#endif
}   

可以根據(jù)實例查找其在類中的名字,也就是“反射”:

-(NSString *)nameWithInstance:(id)instance {
    unsigned int numIvars = 0;
    NSString *key=nil;
    Ivar * ivars = class_copyIvarList([self class], &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;
        }
        if ((object_getIvar(self, thisIvar) == instance)) {//此處若 crash 不要慌!
            key = [NSString stringWithUTF8String:ivar_getName(thisIvar)];
            break;
        }
    }
    free(ivars);
    return key;
} 
  • IMP
    IMPobjc.h中的定義是:
typedef id (*IMP)(id, SEL, ...);

SEL和IMP一一對應(yīng)

它就是一個函數(shù)指針,這是由編譯器生成的。當(dāng)你發(fā)起一個 ObjC 消息之后,最終它會執(zhí)行的那段代碼,就是由這個函數(shù)指針指定的。而 IMP 這個函數(shù)指針就指向了這個方法的實現(xiàn)。既然得到了執(zhí)行某個實例某個方法的入口,我們就可以繞開消息傳遞階段,直接執(zhí)行方法,這在后面會提到。
你會發(fā)現(xiàn)IMP指向的方法與objc_msgSend函數(shù)類型相同,參數(shù)都包含id和SEL類型。每個方法名都對應(yīng)一個SEL類型的方法選擇器,而每個實例對象中的SEL對應(yīng)的方法實現(xiàn)肯定是唯一的,通過一組id和SEL參數(shù)就能確定唯一的方法實現(xiàn)地址;反之亦然。

  • Cache
    在runtime.h中Cache的定義如下:
typedef struct objc_cache *Cache

還記得之前objc_class結(jié)構(gòu)體中有一個struct objc_cache *cache吧,它到底是緩存啥的呢,先看看objc_cache的實現(xiàn):

struct objc_cache {
    unsigned int mask /* total = mask + 1 */                 OBJC2_UNAVAILABLE;
    unsigned int occupied                                    OBJC2_UNAVAILABLE;
    Method buckets[1]                                        OBJC2_UNAVAILABLE;
};

Cache為方法調(diào)用的性能進行優(yōu)化,通俗地講,每當(dāng)實例對象接收到一個消息時,它不會直接在isa指向的類的方法列表中遍歷查找能夠響應(yīng)消息的方法,因為這樣效率太低了,而是優(yōu)先在Cache中查找。Runtime 系統(tǒng)會把被調(diào)用的方法存到Cache中(理論上講一個方法如果被調(diào)用,那么它有可能今后還會被調(diào)用),下次查找的時候效率更高。這根計算機組成原理中學(xué)過的 CPU 繞過主存先訪問Cache的道理挺像,而我猜蘋果為提高Cache命中率應(yīng)該也做了努力吧。

  • Catagory
    這個就是我們平時所說的類別了,很熟悉吧。它可以動態(tài)的為已存在的類添加新的方法。
    它的定義如下:
typedef struct objc_category *Category;

objc_category的定義如下:

struct objc_category {
    char *category_name                           OBJC2_UNAVAILABLE; // 類別名稱
    char *class_name                              OBJC2_UNAVAILABLE; // 類名
    struct objc_method_list *instance_methods     OBJC2_UNAVAILABLE; // 實例方法列表
    struct objc_method_list *class_methods        OBJC2_UNAVAILABLE; // 類方法列表
    struct objc_protocol_list *protocols          OBJC2_UNAVAILABLE; // 協(xié)議列表
}
  • objc_property_t
    objc_property_t是屬性,它的定義如下:
typedef struct objc_property *objc_property_t;

objc_property是內(nèi)置的類型,與之關(guān)聯(lián)的還有一個objc_property_attribute_t,它是屬性的attribute,也就是其實是對屬性的詳細描述,包括屬性名稱、屬性編碼類型、原子類型/非原子類型等。它的定義如下:

typedef struct {
    const char *name; // 名稱
    const char *value;  // 值(通常是空的)
} objc_property_attribute_t;

消息


Objc 中發(fā)送消息是用中括號([])把接收者和消息括起來,而直到運行時才會把消息與方法實現(xiàn)綁定。消息發(fā)送和轉(zhuǎn)發(fā)流程可以概括為:消息發(fā)送(Messaging)是 Runtime 通過 selector 快速查找 IMP 的過程,有了函數(shù)指針就可以執(zhí)行對應(yīng)的方法實現(xiàn);消息轉(zhuǎn)發(fā)(Message Forwarding)是在查找 IMP 失敗后執(zhí)行一系列轉(zhuǎn)發(fā)流程的慢速通道,如果不作轉(zhuǎn)發(fā)處理,則會打日志和拋出異常。

objc_msgSend函數(shù)

此函數(shù)是消息發(fā)送必經(jīng)之路,但只要一提 objc_msgSend,都會說它的偽代碼如下或類似的邏輯,反正就是獲取 IMP 并調(diào)用:

id objc_msgSend(id self, SEL _cmd, ...) {
  Class class = object_getClass(self);
  IMP imp = class_getMethodImplementation(class, _cmd);
  return imp ? imp(self, _cmd, ...) : 0;
}
  • objc_msgSend匯編語言編寫
    當(dāng)需要發(fā)送消息時,編譯器會生成中間代碼,根據(jù)情況分別調(diào)用 objc_msgSend, objc_msgSend_stret,objc_msgSendSuper, 或 objc_msgSendSuper_stret 其中之一。
    這也是為什么`` objc_msgSend``` 要用匯編語言而不是 OC、C 或 C++ 語言來實現(xiàn),因為單獨一個方法定義滿足不了多種類型返回值,有的方法返回 id,有的返回 int??紤]到不同類型參數(shù)返回值排列組合映射不同方法簽名(method signature)的問題,那 switch 語句得老長了。。。這些原因可以總結(jié)為 Calling Convention,也就是說函數(shù)調(diào)用者與被調(diào)用者必須約定好參數(shù)與返回值在不同架構(gòu)處理器上的存取規(guī)則,比如參數(shù)是以何種順序存儲在棧上,或是存儲在哪些寄存器上。除此之外還有其他原因,比如其可變參數(shù)用匯編處理起來最方便,因為找到 IMP 地址后參數(shù)都在棧上。要是用 C++ 傳遞可變參數(shù)那就悲劇了,prologue 機制會弄亂地址(比如 i386 上為了存儲 ebp 向后移位 4byte),最后還要用 epilogue 打掃戰(zhàn)場。而且匯編程序執(zhí)行效率高,在 Objective-C Runtime 中調(diào)用頻率較高的函數(shù)好多都用匯編寫的。

lookUpImpOrForward函數(shù)

使用 lookUpImpOrForward 快速查找 IMP。
其實_class_lookupMethodAndLoadCache3 函數(shù)(objc_msgSend匯編語言中查找IMP的函數(shù))其實只是簡單的調(diào)用了 lookUpImpOrForward 函數(shù):

IMP _class_lookupMethodAndLoadCache3(id obj, SEL sel, Class cls)
{
    return lookUpImpOrForward(cls, sel, obj, 
                              YES/*initialize*/, NO/*cache*/, YES/*resolver*/);
}

注意lookUpImpOrForward 調(diào)用時使用緩存參數(shù)傳入為 NO,因為之前已經(jīng)嘗試過查找緩存了。IMP lookUpImpOrForward(Class cls, SEL sel, id inst, bool initialize, bool cache, bool resolver) 實現(xiàn)了一套查找 IMP 的標(biāo)準(zhǔn)路徑,也就是在消息轉(zhuǎn)發(fā)(Forward)之前的邏輯。

消息發(fā)送

1 檢測這個 selector 是不是要忽略的。比如 Mac OS X 開發(fā),有了垃圾回收就不理會 retain, release 這些函數(shù)了。
2 檢測這個 target 是不是 nil 對象。ObjC 的特性是允許對一個 nil 對象執(zhí)行任何一個方法不會 Crash,因為會被忽略掉。
3 如果上面兩個都過了,那就開始查找這個類的 IMP,先從 cache 里面找,完了找得到就跳到對應(yīng)的函數(shù)去執(zhí)行。
4 如果 cache 找不到就找一下方法分發(fā)表。
5 如果分發(fā)表找不到就到超類的分發(fā)表去找,一直找,直到找到NSObject類為止。
6 如果還找不到就要開始進入動態(tài)方法解析了,后面會提到。
PS:這里說的分發(fā)表其實就是Class中的方法列表,它將方法選擇器和方法實現(xiàn)地址聯(lián)系起來。

messaging1

其實編譯器會根據(jù)情況在objc_msgSend, objc_msgSend_stret, objc_msgSendSuper, 或objc_msgSendSuper_stret四個方法中選擇一個來調(diào)用。如果消息是傳遞給超類,那么會調(diào)用名字帶有”Super”的函數(shù);如果消息返回值是數(shù)據(jù)結(jié)構(gòu)而不是簡單值時,那么會調(diào)用名字帶有”stret”的函數(shù)。排列組合正好四個方法。

值得一提的是在 i386 平臺處理返回類型為浮點數(shù)的消息時,需要用到objc_msgSend_fpret函數(shù)來進行處理,這是因為返回類型為浮點數(shù)的函數(shù)對應(yīng)的 ABI(Application Binary Interface) 與返回整型的函數(shù)的 ABI 不兼容。此時objc_msgSend不再適用,于是objc_msgSend_fpret被派上用場,它會對浮點數(shù)寄存器做特殊處理。不過在 PPC 或 PPC64 平臺是不需要麻煩它的。

PS:有木有發(fā)現(xiàn)這些函數(shù)的命名規(guī)律哦?帶“Super”的是消息傳遞給超類;“stret”可分為“st”+“ret”兩部分,分別代表“struct”和“return”;“fpret”就是“fp”+“ret”,分別代表“floating-point”和“return”。

方法中的隱藏參數(shù)

我們經(jīng)常在方法中使用self關(guān)鍵字來引用實例本身,但從沒有想過為什么self就能取到調(diào)用當(dāng)前方法的對象吧。其實self的內(nèi)容是在方法運行時被偷偷的動態(tài)傳入的。
當(dāng)objc_msgSend找到方法對應(yīng)的實現(xiàn)時,它將直接調(diào)用該方法實現(xiàn),并將消息中所有的參數(shù)都傳遞給方法實現(xiàn),同時,它還將傳遞兩個隱藏的參數(shù):
接收消息的對象(也就是self指向的內(nèi)容)
方法選擇器(_cmd指向的內(nèi)容)
如:

[person sayHello];
/*
最后調(diào)用sayHello時,是sayHello(person,@selector(sayHello));這樣
*/

之所以說它們是隱藏的是因為在源代碼方法的定義中并沒有聲明這兩個參數(shù)。它們是在代碼被編譯時被插入實現(xiàn)中的。盡管這些參數(shù)沒有被明確聲明,在源代碼中我們?nèi)匀豢梢砸盟鼈?。在下面的例子中?code>self引用了接收者對象,而_cmd引用了方法本身的選擇器:

- strange
{
    id  target = getTheReceiver();
    SEL method = getTheMethod();
 
    if ( target == self || method == _cmd )
        return nil;
    return [target performSelector:method];
}

在這兩個參數(shù)中,self 更有用。實際上,它是在方法實現(xiàn)中訪問消息接收者對象的實例變量的途徑。
而當(dāng)方法中的super關(guān)鍵字接收到消息時,編譯器會創(chuàng)建一個objc_super結(jié)構(gòu)體:

struct objc_super { id receiver; Class class; };

這個結(jié)構(gòu)體指明了消息應(yīng)該被傳遞給特定超類的定義。但receiver仍然是self本身,這點需要注意,因為當(dāng)我們想通過[super class]獲取超類時,編譯器只是將指向self的id指針和class的SEL傳遞給了objc_msgSendSuper函數(shù),因為只有在NSObject類才能找到class方法,然后class方法調(diào)用object_getClass(),接著調(diào)用objc_msgSend(objc_super->receiver, @selector(class)),傳入的第一個參數(shù)是指向self的id指針,與調(diào)用[self class]相同,所以我們得到的永遠都是self的類型。

獲取方法地址

在IMP那節(jié)提到過可以避開消息綁定而直接獲取方法的地址并調(diào)用方法。這種做法很少用,除非是需要持續(xù)大量重復(fù)調(diào)用某方法的極端情況,避開消息發(fā)送泛濫而直接調(diào)用該方法會更高效。
NSObject類中有個methodForSelector:實例方法,你可以用它來獲取某個方法選擇器對應(yīng)的IMP,舉個栗子:

void (*setter)(id, SEL, BOOL);
int i;
 
setter = (void (*)(id, SEL, BOOL))[target
    methodForSelector:@selector(setFilled:)];
for ( i = 0 ; i < 1000 ; i++ )
    setter(targetList[i], @selector(setFilled:), YES);

當(dāng)方法被當(dāng)做函數(shù)調(diào)用時,上節(jié)提到的兩個隱藏參數(shù)就需要我們明確給出了。上面的例子調(diào)用了1000次函數(shù),你可以試試直接給target發(fā)送1000次setFilled:消息會花多久。
PS:methodForSelector:方法是由 Cocoa 的 Runtime 系統(tǒng)提供的,而不是 Objc 自身的特性。

動態(tài)方法解析


你可以動態(tài)地提供一個方法的實現(xiàn)。例如我們可以用@dynamic關(guān)鍵字在類的實現(xiàn)文件中修飾一個屬性:

@dynamic propertyName;

這表明我們會為這個屬性動態(tài)提供存取方法,也就是說編譯器不會再默認(rèn)為我們生成setPropertyName:propertyName方法,而需要我們動態(tài)提供。我們可以通過分別重載resolveInstanceMethod:resolveClassMethod:方法分別添加實例方法實現(xiàn)和類方法實現(xiàn)。因為當(dāng) Runtime 系統(tǒng)在Cache和方法分發(fā)表中(包括超類)找不到要執(zhí)行的方法時,Runtime會調(diào)用resolveInstanceMethod:resolveClassMethod:來給程序員一次動態(tài)添加方法實現(xiàn)的機會。我們需要用class_addMethod函數(shù)完成向特定類添加特定方法實現(xiàn)的操作:

ynamicMethodIMP(id self, SEL _cmd) {
    // implementation ....
}
@implementation MyClass
+ (BOOL)resolveInstanceMethod:(SEL)aSEL
{
    if (aSEL == @selector(resolveThisMethodDynamically)) {
          class_addMethod([self class], aSEL, (IMP) dynamicMethodIMP, "v@:");
          return YES;
    }
    return [super resolveInstanceMethod:aSEL];
}
@end

上面的例子為resolveThisMethodDynamically方法添加了實現(xiàn)內(nèi)容,也就是dynamicMethodIMP方法中的代碼。其中 “v@:” 表示返回值和參數(shù),這個符號涉及 Type Encoding
PS:動態(tài)方法解析會在消息轉(zhuǎn)發(fā)機制浸入前執(zhí)行。如果 respondsToSelector:instancesRespondToSelector:方法被執(zhí)行,動態(tài)方法解析器將會被首先給予一個提供該方法選擇器對應(yīng)的IMP的機會。如果你想讓該方法選擇器被傳送到轉(zhuǎn)發(fā)機制,那么就讓resolveInstanceMethod:返回NO。
評論區(qū)有人問如何用 resolveClassMethod: 解析類方法,我將他貼出有問題的代碼做了糾正和優(yōu)化后如下,可以順便將實例方法和類方法的動態(tài)方法解析對比下:
頭文件:

#import <Foundation/Foundation.h>

@interface Student : NSObject
+ (void)learnClass:(NSString *) string;
- (void)goToSchool:(NSString *) name;
@end
m 文件:
#import "Student.h"
#import <objc/runtime.h>

@implementation Student
+ (BOOL)resolveClassMethod:(SEL)sel {
    if (sel == @selector(learnClass:)) {
        class_addMethod(object_getClass(self), sel, class_getMethodImplementation(object_getClass(self), @selector(myClassMethod:)), "v@:");
        return YES;
    }
    return [class_getSuperclass(self) resolveClassMethod:sel];
}

+ (BOOL)resolveInstanceMethod:(SEL)aSEL
{
    if (aSEL == @selector(goToSchool:)) {
        class_addMethod([self class], aSEL, class_getMethodImplementation([self class], @selector(myInstanceMethod:)), "v@:");
        return YES;
    }
    return [super resolveInstanceMethod:aSEL];
}

+ (void)myClassMethod:(NSString *)string {
    NSLog(@"myClassMethod = %@", string);
}

- (void)myInstanceMethod:(NSString *)string {
    NSLog(@"myInstanceMethod = %@", string);
}
@end

需要深刻理解[self class]object_getClass(self) 甚至 object_getClass([self class]) 的關(guān)系,其實并不難,重點在于 self 的類型:
當(dāng) self 為實例對象時,[self class]object_getClass(self)等價,因為前者會調(diào)用后者。object_getClass([self class])得到元類。
當(dāng) self 為類對象時,[self class] 返回值為自身,還是 self。object_getClass(self)object_getClass([self class])等價。
凡是涉及到類方法時,一定要弄清楚元類、selector、IMP 等概念,這樣才能做到舉一反三,隨機應(yīng)變。

消息轉(zhuǎn)發(fā)


消息轉(zhuǎn)發(fā)

重定向()

在消息轉(zhuǎn)發(fā)機制執(zhí)行前,Runtime 系統(tǒng)會再給我們一次偷梁換柱的機會,即通過重載- (id)forwardingTargetForSelector:(SEL)aSelector方法替換消息的接受者為其他對象:

- (id)forwardingTargetForSelector:(SEL)aSelector
{
    if(aSelector == @selector(mysteriousMethod:)){
        return alternateObject;
    }
    return [super forwardingTargetForSelector:aSelector];
}

畢竟消息轉(zhuǎn)發(fā)要耗費更多時間,抓住這次機會將消息重定向給別人是個不錯的選擇,不過千萬別返回self,因為那樣會死循環(huán)。 如果此方法返回nil或self,則會進入消息轉(zhuǎn)發(fā)機制(forwardInvocation:);否則將向返回的對象重新發(fā)送消息。
如果想替換類方法的接受者,需要覆寫+ (id)forwardingTargetForSelector:(SEL)aSelector 方法,并返回類對象:

+ (id)forwardingTargetForSelector:(SEL)aSelector {
    if(aSelector == @selector(xxx)) {
        return NSClassFromString(@"Class name");
    }
    return [super forwardingTargetForSelector:aSelector];
}

轉(zhuǎn)發(fā)

當(dāng)動態(tài)方法解析不作處理返回NO時,消息轉(zhuǎn)發(fā)機制會被觸發(fā)。在這時forwardInvocation:方法會被執(zhí)行,我們可以重寫這個方法來定義我們的轉(zhuǎn)發(fā)邏輯:

- (void)forwardInvocation:(NSInvocation *)anInvocation
{
    if ([someOtherObject respondsToSelector:
            [anInvocation selector]])
        [anInvocation invokeWithTarget:someOtherObject];
    else
        [super forwardInvocation:anInvocation];
}

該消息的唯一參數(shù)是個NSInvocation類型的對象——該對象封裝了原始的消息和消息的參數(shù)。我們可以實現(xiàn)forwardInvocation:方法來對不能處理的消息做一些默認(rèn)的處理,也可以將消息轉(zhuǎn)發(fā)給其他對象來處理,而不拋出錯誤。
這里需要注意的是參數(shù)anInvocation是從哪的來的呢?其實在forwardInvocation:消息發(fā)送前,Runtime系統(tǒng)會向?qū)ο蟀l(fā)送methodSignatureForSelector:消息,并取到返回的方法簽名用于生成NSInvocation對象。所以我們在重寫forwardInvocation:的同時也要重寫methodSignatureForSelector:方法,否則會拋異常。
當(dāng)一個對象由于沒有相應(yīng)的方法實現(xiàn)而無法響應(yīng)某消息時,運行時系統(tǒng)將通過forwardInvocation:消息通知該對象。每個對象都從NSObject類中繼承了forwardInvocation:方法。然而,NSObject中的方法實現(xiàn)只是簡單地調(diào)用了doesNotRecognizeSelector:。通過實現(xiàn)我們自己的forwardInvocation:方法,我們可以在該方法實現(xiàn)中將消息轉(zhuǎn)發(fā)給其它對象。
forwardInvocation:方法就像一個不能識別的消息的分發(fā)中心,將這些消息轉(zhuǎn)發(fā)給不同接收對象?;蛘咚部梢韵笠粋€運輸站將所有的消息都發(fā)送給同一個接收對象。它可以將一個消息翻譯成另外一個消息,或者簡單的”吃掉“某些消息,因此沒有響應(yīng)也沒有錯誤。forwardInvocation:方法也可以對不同的消息提供同樣的響應(yīng),這一切都取決于方法的具體實現(xiàn)。該方法所提供是將不同的對象鏈接到消息鏈的能力。
注意: forwardInvocation:方法只有在消息接收對象中無法正常響應(yīng)消息時才會被調(diào)用。 所以,如果我們希望一個對象將negotiate消息轉(zhuǎn)發(fā)給其它對象,則這個對象不能有negotiate方法。否則,forwardInvocation:將不可能會被調(diào)用。

Method Swizzling


之前所說的消息轉(zhuǎn)發(fā)雖然功能強大,但需要我們了解并且能更改對應(yīng)類的源代碼,因為我們需要實現(xiàn)自己的轉(zhuǎn)發(fā)邏輯。當(dāng)我們無法觸碰到某個類的源代碼,卻想更改這個類某個方法的實現(xiàn)時,該怎么辦呢?可能繼承類并重寫方法是一種想法,但是有時無法達到目的。這里介紹的是 Method Swizzling ,它通過重新映射方法對應(yīng)的實現(xiàn)來達到“偷天換日”的目的。跟消息轉(zhuǎn)發(fā)相比,Method Swizzling 的做法更為隱蔽,甚至有些冒險,也增大了debug的難度。
交換方法的幾種實現(xiàn)方式

  • 利用 method_exchangeImplementations 交換兩個方法的實現(xiàn)
  • 利用 class_replaceMethod 替換方法的實現(xiàn)
  • 利用 method_setImplementation 來直接設(shè)置某個方法的IMP
交換方法

這里摘抄一個 NSHipster 的例子:

#import <objc/runtime.h> 
 
@implementation UIViewController (Tracking) 
 
+ (void)load { 
    static dispatch_once_t onceToken; 
    dispatch_once(&onceToken, ^{ 
        Class aClass = [self class]; 
 
        SEL originalSelector = @selector(viewWillAppear:); 
        SEL swizzledSelector = @selector(xxx_viewWillAppear:); 
 
        Method originalMethod = class_getInstanceMethod(aClass, originalSelector); 
        Method swizzledMethod = class_getInstanceMethod(aClass, swizzledSelector); 
        
        // When swizzling a class method, use the following:
        // Class aClass = object_getClass((id)self);
        // ...
        // Method originalMethod = class_getClassMethod(aClass, originalSelector);
        // Method swizzledMethod = class_getClassMethod(aClass, swizzledSelector);
 
        BOOL didAddMethod = 
            class_addMethod(aClass, 
                originalSelector, 
                method_getImplementation(swizzledMethod), 
                method_getTypeEncoding(swizzledMethod)); 
 
        if (didAddMethod) { 
            class_replaceMethod(aClass, 
                swizzledSelector, 
                method_getImplementation(originalMethod), 
                method_getTypeEncoding(originalMethod)); 
        } else { 
            method_exchangeImplementations(originalMethod, swizzledMethod); 
        } 
    }); 
} 
 
#pragma mark - Method Swizzling 
 
- (void)xxx_viewWillAppear:(BOOL)animated { 
    [self xxx_viewWillAppear:animated]; 
    NSLog(@"viewWillAppear: %@", self); 
} 
 
@end

上面的代碼通過添加一個Tracking類別到UIViewController類中,將UIViewController類的viewWillAppear:方法和Tracking類別中xxx_viewWillAppear:方法的實現(xiàn)相互調(diào)換。Swizzling 應(yīng)該在+ load方法中實現(xiàn),因為+ load是在一個類最開始加載時調(diào)用。dispatch_once是GCD中的一個方法,它保證了代碼塊只執(zhí)行一次,并讓其為一個原子操作,線程安全是很重要的。
如果類中不存在要替換的方法,那就先用class_addMethodclass_replaceMethod函數(shù)添加和替換兩個方法的實現(xiàn);如果類中已經(jīng)有了想要替換的方法,那么就調(diào)用method_exchangeImplementations函數(shù)交換了兩個方法的 IMP,這是蘋果提供給我們用于實現(xiàn) Method Swizzling 的便捷方法。
可能有人注意到了這行:

// When swizzling a class method, use the following:
// Class aClass = object_getClass((id)self);
// ...
// Method originalMethod = class_getClassMethod(aClass, originalSelector);
// Method swizzledMethod = class_getClassMethod(aClass, swizzledSelector);

object_getClass((id)self)[self class] 返回的結(jié)果類型都是 Class,但前者為元類,后者為其本身,因為此時 self 為 Class 而不是實例.注意 [NSObject class][object class]的區(qū)別:

+ (Class)class {
    return self;
}

- (Class)class {
    return object_getClass(self);
}

PS:如果類中沒有想被替換實現(xiàn)的原方法時,class_replaceMethod相當(dāng)于直接調(diào)用class_addMethod向類中添加該方法的實現(xiàn);否則調(diào)用method_setImplementation方法,types參數(shù)會被忽略。method_exchangeImplementations方法做的事情與如下的原子操作等價:

IMP imp1 = method_getImplementation(m1);
IMP imp2 = method_getImplementation(m2);
method_setImplementation(m1, imp2);
method_setImplementation(m2, imp1);

最后xxx_viewWillAppear:方法的定義看似是遞歸調(diào)用引發(fā)死循環(huán),其實不會的。因為[self xxx_viewWillAppear:animated]消息會動態(tài)找到xxx_viewWillAppear:方法的實現(xiàn),而它的實現(xiàn)已經(jīng)被我們與viewWillAppear:方法實現(xiàn)進行了互換,所以這段代碼不僅不會死循環(huán),如果你把[self xxx_viewWillAppear:animated]換成[self viewWillAppear:animated]反而會引發(fā)死循環(huán)。

runtime下class的各種常用操作


// 獲取方法列表
    unsigned int methodListCount;
    Method *methodList = class_copyMethodList([self class], &methodListCount);
    for (unsigned int i = 0; i < methodListCount; i++) {
        Method method = methodList[i];
        NSLog(@"method----->%@", NSStringFromSelector(method_getName(method)));
    }

    // 獲取成員變量列表
    unsigned int ivarCount;
    Ivar *ivarList = class_copyIvarList([self class], &ivarCount);
    for (unsigned int i = 0; i < ivarCount; i++) {
        Ivar ivar = ivarList[i];
        const char *ivarName = ivar_getName(ivar);
        NSLog(@"Ivar----->%@", [NSString stringWithUTF8String:ivarName]);
    }

    // 獲取協(xié)議列表
    unsigned int protocolListCount;
    __unsafe_unretained Protocol **protocolList = class_copyProtocolList([self class], &protocolListCount);
    for (unsigned int i = 0; i < protocolListCount; i++) {
        Protocol *protocol = protocolList[1];
        const char *protocolName = protocol_getName(protocol);
        NSLog(@"protocol----->%@", [NSString stringWithUTF8String:protocolName]);
    }

    // 獲取類方法
    Method classMethod = class_getClassMethod([self class], @selector(cancelPreviousPerformRequestsWithTarget:));
    NSLog(@"class method----->%@", NSStringFromSelector(method_getName(classMethod)));

    // 獲取實例方法
    Method instanceMethod = class_getInstanceMethod([self class], @selector(viewDidLoad));
    NSLog(@"instance method----->%@", NSStringFromSelector(method_getName(instanceMethod)));
    
    // 替換方法的實現(xiàn)
    Method newMethod = class_getInstanceMethod([self class], @selector(newReplaceMethod));
    class_replaceMethod([self class], @selector(oldReplaceMethod), method_getImplementation(newMethod), method_getTypeEncoding(newMethod));
    [self oldReplaceMethod];
    
    
    // 交換兩個方法
    Method oldChangeMethod = class_getInstanceMethod([self class], @selector(oldexchangeMethod));
    Method newChangeMethod = class_getInstanceMethod([self class], @selector(newexchangeMethod));
    method_exchangeImplementations(oldChangeMethod, newChangeMethod);
    [self oldexchangeMethod];
    [self newexchangeMethod];

總結(jié)


我們之所以讓自己的類繼承NSObject不僅僅因為蘋果幫我們完成了復(fù)雜的內(nèi)存分配問題,更是因為這使得我們能夠用上 Runtime 系統(tǒng)帶來的便利。可能我們平時寫代碼時可能很少會考慮一句簡單的[receiver message]背后發(fā)生了什么,而只是當(dāng)做方法或函數(shù)調(diào)用。深入理解 Runtime 系統(tǒng)的細節(jié)更有利于我們利用消息機制寫出功能更強大的代碼,比如 Method Swizzling 等。

轉(zhuǎn)載:Objective-C Runtime
參考

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

  • 轉(zhuǎn)至元數(shù)據(jù)結(jié)尾創(chuàng)建: 董瀟偉,最新修改于: 十二月 23, 2016 轉(zhuǎn)至元數(shù)據(jù)起始第一章:isa和Class一....
    40c0490e5268閱讀 2,101評論 0 9
  • 轉(zhuǎn)載:http://yulingtianxia.com/blog/2014/11/05/objective-c-r...
    F麥子閱讀 842評論 0 2
  • 本文轉(zhuǎn)載自:http://yulingtianxia.com/blog/2014/11/05/objective-...
    ant_flex閱讀 892評論 0 1
  • 我們常常會聽說 Objective-C 是一門動態(tài)語言,那么這個「動態(tài)」表現(xiàn)在哪呢?我想最主要的表現(xiàn)就是 Obje...
    Ethan_Struggle閱讀 2,351評論 0 7
  • 本文詳細整理了 Cocoa 的 Runtime 系統(tǒng)的知識,它使得 Objective-C 如虎添翼,具備了靈活的...
    lylaut閱讀 870評論 0 4

友情鏈接更多精彩內(nèi)容