Runtime概念

runtime

運行時語言,實現Object-C的C語言庫,將OC轉換成C進行編譯的過渡者。

作為一門動態編程語言,Objective-C 會盡可能的將編譯和鏈接時要做的事情推遲到運行時。只要有可能,Objective-C 總是使用動態 的方式來解決問題。這意味著 Objective-C 語言不僅需要一個編譯環境,同時也需要一個運行時系統來執行編譯好的代碼,這就是runtime

運行時系統(runtime)扮演的角色類似于 Objective-C 語言的操作系統,Objective-C 基于該系統來工作。因此,runtime好比Objective-C的靈魂,很多東西都是在這個基礎上出現的。所以它是值的你花功夫去理解的。

實現

主要是用C語言實現,部分由匯編語言。實際上正是runtime將OC中面向對象的類轉換成C語言中面向過程的結構體。無論是實例對象還是類對象,實際上對應的都是C中的結構體。

使用

iskindofclass和isMemberOfClass就是兩個典型的運行時方法。
同時注意使用class_addMethod等方法時要引入#import <objc/runtime.h>頭文件。

關于32位與64位

runtime分為早期版本和現行版本,32位使用早期,64位使用現行。32位已被淘汰。
64位與32位的區別在于,操作系統上64位用于3D動畫等先進功能32位用于日常簡單使用;處理器上64位比32位更寬處理能力更強;軟件上基于32位和64位開發的不同;內存容量不同。現在必須支持arm64,代碼中主要注意類型長度上的區別。ios推薦使用的NSInteger區別于int的地方也就是在于前者可以根據系統位數使用較大的長度。

與傳統靜態語言C語言的區別

很常見的一個消息發送語句:

[receiver message]

會被編譯器轉化成

objc_msgSend(receiver, selector)

如果有參數則為

objc_msgSend(receiver, selector, arg1, arg2, …) 

receiver:消息接受者(receiver),self,某個類
selector:方法名(message)
arg:參數
傳統的靜態語言,編譯器會自上而下,在存在調用函數時會進入調用函數,逐句實現編譯。
而OC在編譯過程中并不會這樣做,只是編輯成明確方法名調用者參數的形式,該函數被如何實現以及是否實現并不關心。只有在程序運行時才會根據方法名進入具體函數中,這也就是會在運行中找不到對應函數時會造成崩潰的原因。
OC實際上是使用消息傳遞機制代替傳統C語言的函數調用。
_CMD,SEL類型,可以獲取當前方法名的關鍵字。

- (void)message  
{  
    self.name = @"James";//通過self關鍵字給當前對象的屬性賦值  
    SEL currentSel = _cmd;//通過_cmd關鍵字取到當前函數對應的SEL  
    NSLog(@"currentSel is :%s",(char *)currentSel);  
}

打印結果

ObjcRunTime[693:403] currentSel is :message

初步理解,其實objc_msgSend 所做的事情,就是通過我們傳入的self 指針,找到class 的method_list 然后根據SEL 做比較,沒有的話,就在super class 找,如此往復。直到找到匹配的SEL,然后,call imp。當然,runtime在其中又做了很多優化。

OC中的消息傳遞

OC中的消息傳遞與C中函數調用的最大區別在于OC中的消息傳遞在時運行時中進行的,而函數調用是在編譯時就可以進行的。

id num = @123;
//輸出123
NSLog(@"%@", num);
//程序崩潰,報錯[__NSCFNumber appendString:]: unrecognized selector sent to instance 0x7b27
[num appendString:@"Hello World"];

上述代碼沒有任何問題,id可以使任何類型包括NSString,但是在運行過程中在NSNumber中找不到appendString這個方法,所以會報錯。所以消息傳遞的強大之處在于可以在運行時添加新方法,缺點在于無法再編譯時發現錯誤。

OC中的消息傳遞轉化為C的函數調用

簡單的創建Person對象

//為了方便查看轉寫后的C語言代碼,將alloc和init分兩步完成
Person *p = [Person alloc];
p = [p init];
p.name = @"Jiaming Chen";
[p showMyself];

使用clang -rewrite-objc main.m可以轉化成C

Person *p = ((Person *(*)(id, SEL))(void *)objc_msgSend)((id)objc_getClass("Person"), sel_registerName("alloc"));
p = ((Person *(*)(id, SEL))(void *)objc_msgSend)((id)p, sel_registerName("init"));
((void (*)(id, SEL, NSString *))(void *)objc_msgSend)((id)p, sel_registerName("setName:"), (NSString *)&__NSConstantStringImpl__var_folders_1f_dz4kq57d4b19s4tfmds1mysh0000gn_T_main_f5b408_mi_1);
((void (*)(id, SEL))(void *)objc_msgSend)((id)p, sel_registerName("showMyself"));

第一行代碼簡要表示為

Person *p = objc_msgSend(objc_getClass("Person"), sel_registerName("alloc"));

第一步,獲取Person類,第二步注冊alloc方法,然后通過objc_msgSend將alloc消息發送給消息接受者Person。
第二行代碼簡要表示為

p = objc_msgSend(p, sel_registerName("init"));

由此可見,對于OC而言都是以消息傳遞實現,而正是runtime通過objc_msgSend將一個面向對象的消息傳遞轉換成了一個面向過程的的函數調用。

消息轉發

我們知道消息傳遞實際上做的事就是利用objc_msgSend,將消息傳遞給接受者,在接受者的結構體中的objc_method_list中查找方法。如果找到就可以直接調用,如果找不到就會通過super_class指針去它的父類中查找,直至基類NSObject。如果還是沒有找到就會調用NSObject的doesNotRecognizeSelector報出unrecognized的錯誤。也就是我們常出現的unrecognized selector sent to instance錯誤(對象提前被釋放或者為空導致無法識別其方法)。
但是在此之前,還有三次機會來處理這個消息來避免出現崩潰,這就是所謂的消息轉發。詳細見后文。

runtime源碼學習
  • selector
    selector可以叫做選擇器,其實指的就是對象的方法,也可以理解為C語言里面的函數指針,面向對象里面的對應概念。
    @selector(xxxx)的作用是找到名字為xxxx的方法。一般用于[a performSelector:@selector(b)];就是說去調用a對象的b方法,和[a b];的意思一樣,但是這樣更加動態一些。@selector(xxxx)返回的類型是SEL,看方法說明的時候如果參數類型是SEL,那么就是要接受@selector(xxxx)返回的值。
    在Objc中 SEL的定義是:
typedef struct objc_selector *SEL;  

我們注意到對于SEL的定義,實際上就是使用typedef將結構體objc_selector重新命名為*SEL。而SEL則是指向這個結構體的指針,所以SEL的對象不需要再加*。
在Mac OS X中SEL其實被映射為一個C字符串,可以看作是方法的名字(編號),它并不一個指向具體方法實現(IMP類型才是)。對于所有的類,只要方法名是相同的,產生的selector都是一樣的。
簡而言之,你可以理解 @selector()就是取類方法的編號,他的行為基本可以等同C語言的中函數指針,只不過C語言中,可以把函數名直接賦給一個函數指針,而Object-C的類不能直接應用函數指針,這樣只能做一個@selector語法來取。
它的結果是一個SEL類型。這個類型本質是類方法的名字(編號)。

注意1. @selector是查找當前類的方法,而[object @selector(方法名:方法參數..) ] ;是取object對應類的相應方法.
注意2.查找類方法時,除了方法名,方法參數也查詢條件之一.
注意3. 可以用字符串來找方法 SEL 變量名 = NSSelectorFromString(方法名字的字符串);
注意4. 可以運行中用SEL變量反向查出方法名字字符串
NSString *變量名 = NSStringFromSelector(SEL參數);

runtime 在實現selector時,實現了一個很大的Set,簡單的說就是一個經過了優化過的hash表。而Set的特點就是唯一,也就是SEL是唯一的。那么對于字符串的比較僅僅需要比較他們的地址就可以了。 所以OC中不允許使用方法名一樣參數不一樣的函數,編譯器會根據每個方法的方法名生成唯一的SEL。所以,在速度上是無與倫比的。
selector主要用于兩個對象之間進行松耦合的通訊.這種方法很多開發環境用到。比如GTK,Delphi.基本上整個Cocoa庫之間對象,控制之間通訊都是在這個基礎構建的。
用戶行為統計中的經典運用:

+ (void)setUpAnalytics
{
   __weak typeof(self) weakSelf = self;
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
     //讀取配置文件,獲取需要統計的事件列表
     for (NSString *classNameString in analyticsData.allKeys) {
         //使用運行時創建類對象
         const char *className = [classNameString UTF8String];
         //從一個字串返回一個類
         Class newClass = objc_getClass(className);
         NSArray *pageEventList = [[analyticsData objectForKey:classNameString] objectForKey:Event];
         for (NSDictionary *eventDict in pageEventList) {
             //事件方法名稱
             NSString *eventMethodName = eventDict[MethodName];
             SEL seletor = NSSelectorFromString(eventMethodName);
             NSString *eventId = eventDict[EventId];
             [weakSelf trackEventWithClass:newClass selector:seletor event:eventId];
             [weakSelf uAnalyticstrackEventWithEventdata:eventDict];
         }
     }
   });
}
  • id
    id是通用類型指針,能夠表示任何對象,換句話說,id 類型的變量可以存放任何數據類型的對象。在內部處理上,這種類型被定義為指向對象的指針,實際上是一個指向這種對象的實例變量的指針。
    查看到id數據結構如下:
// Represents an instance of a class.  
struct objc_object {  
    Class isa  OBJC_ISA_AVAILABILITY;  
};  
// A pointer to an instance of a class.  
typedef struct objc_object *id; 

實際上*id就是結構體objc_object的別名。而id則是一個指向objc_object結構體指針,它包含一個Class isa成員,根據isa指針就可以順藤摸瓜找到對象所屬的類。需要注意的是id 是一個指針,所以在使用id的時候不需要加星號。

NSString *str = [[NSString alloc] init];

這里我們定義的對象str,實際上同樣是一個objc_object結構體指針。

  • Class
    isa指針的數據類型是Class,Class表示對象所屬的類。
// An opaque type that represents an Objective-C class.  
typedef struct objc_class *Class;  

可以查看到Class其實就是一個objc_class結構體指針。class類型和id類型一樣,本身繼承于指針類型,用于儲存類對象的指針,故在聲明class類型變量時和id一樣不需要*號。

NSString *str = [[NSString alloc] init];
Class c = [str Class];

這里我們定義的c,實際上同樣是一個objc_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;  
/* Use `Class` instead of `struct objc_class *` */  

OC的類其實也是一個對象,即類對象,意思就是你可以向一個類發送消息。為了可以調用類方法,這個類的isa指針必須指向一個包含這些類方法的類結構體,也就是元類(meta-class)的概念。meta-class之所以重要,是因為它保存著創建類對象以及類方法所需的所有信息。每個類都會有一個單獨的meta-class,因為每個類的類方法基本不可能完全相同。
任何NSObject繼承體系下的meta-class都使用NSObject的meta-class作為自己所屬的類。所有的meta-class使用基類的meta-class作為它們的父類,而基類的meta-class也是屬于它自己,也就是說基類的meta-class的isa指針指向它自己。


meta圖解說明.png

上圖實線是super_class指針,虛線是isa指針。有幾個關鍵點需要解釋以下:
1.Root class (class)其實就是NSObject,NSObject是沒有超類的,所以Root class(class)的superclass指向nil。
2.每個Class都有一個isa指針指向唯一的Meta class
3.Root class(meta)的superclass指向Root class(class),也就是NSObject,形成一個回路。
4.每個Meta class的isa指針都指向Root class (meta)。

int main(int argc, const char * argv[]) {
    @autoreleasepool {
        Person *p = [[Person alloc] init];
        Class c1 = [p class];
        Class c2 = [Person class];
        //輸出 1
        NSLog(@"%d", c1 == c2);
    }
    return 0;
}

由此可見,類對象是單例。對于Person這個類對象來說,只有一個,所有類對象均是如此。
介紹兩個函數:

OBJC_EXPORT BOOL class_isMetaClass(Class cls) 
    OBJC_AVAILABLE(10.5, 2.0, 9.0, 1.0);
    
OBJC_EXPORT Class object_getClass(id obj) 
    OBJC_AVAILABLE(10.5, 2.0, 9.0, 1.0);

class_isMetaClass用于判斷Class對象是否為元類,object_getClass用于獲取對象的isa指針指向的對象。

int main(int argc, const char * argv[]) {
    @autoreleasepool {
        Person *p = [[Person alloc] init];
        //輸出1
        NSLog(@"%d", [p class] == object_getClass(p));
        //輸出0
        NSLog(@"%d", class_isMetaClass(object_getClass(p)));
        //輸出1
        NSLog(@"%d", class_isMetaClass(object_getClass([Person class])));
        //輸出0
        NSLog(@"%d", object_getClass(p) == object_getClass([Person class]));
    }
    return 0;
}

由上看出,實例的isa指針指向類,類的isa指針指向元類。

super_class表示實例對象對應的父類;
name表示類名,我們可以在運行期,通過這個名稱查找到該類(通過:id objc_getClass(const char *aClassName))或該類的 metaclass(id objc_getMetaClass(const char *aClassName));
ivars表示多個成員變量,它指向objc_ivar_list結構體。在runtime.h可以看到它的定義:

struct objc_ivar_list {  
  int ivar_count                                           OBJC2_UNAVAILABLE;  
#ifdef __LP64__  
  int space                                                OBJC2_UNAVAILABLE;  
#endif  
  /* variable length structure */  
  struct objc_ivar ivar_list[1]                            OBJC2_UNAVAILABLE;  
}  

objc_ivar_list其實就是一個鏈表,存儲多個objc_ivar,而objc_ivar結構體存儲類的單個成員變量信息。
(* struct objc_ivar ivar_list[1] 這個稱為結構體數組,顧名思義,結構數組是指能夠存放多個結構體類型(objc_ivar)的一種數組(ivar_list)形式。
methodLists表示方法列表,它指向objc_method_list結構體的二級指針,可以動態修改
methodLists的值來添加成員方法,也是Category實現原理,同樣也解釋Category不能添加屬性的原因。
Category只能在運行時中添加屬性。代碼如下:

///例如可能是這樣的使用
  static const void *propertyKey = &propertyKey;
  /// 將value通過運行時綁定到self
  objc_setAssociatedObject(self, propertyKey, value, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
  /// 將value在運行時中通過propertyKey取出綁定的值
  id value = objc_getAssociatedObject(self, propertyKey);

在runtime.h可以看到它的定義

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

同理,objc_method_list也是一個鏈表,存儲多個objc_method,而objc_method結構體存儲類的某個方法的信息。
cache用來緩存經常訪問的方法,它指向objc_cache結構體,后面會重點講到。
protocols表示類遵循哪些協議。

  • Method
    Method表示類中的某個方法,在runtime.h文件中找到它的定義:
/// An opaque type that represents a method in a class definition.  
typedef struct objc_method *Method;  
struct objc_method {  
    SEL method_name                                          OBJC2_UNAVAILABLE;  
    char *method_types                                       OBJC2_UNAVAILABLE;  
    IMP method_imp                                           OBJC2_UNAVAILABLE;  
}  

其實Method就是一個指向objc_method結構體指針,它存儲了方法名(method_name)、方法類型(method_types)和方法實現(method_imp)等信息。而method_imp的數據類型是IMP,它是一個函數指針,后面會重點提及。

  • Ivar
    Ivar表示類中的實例變量,在runtime.h文件中找到它的定義:
/// An opaque type that represents an instance variable.  
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  
}  

Ivar其實就是一個指向objc_ivar結構體指針,它包含了變量名(ivar_name)、變量類型(ivar_type)等信息。

  • IMP
    在上面講Method時就說過,IMP本質上就是一個函數指針,指向方法的實現,在objc.h找到它的定義:
/// A pointer to the function of a method implementation.   
#if !OBJC_OLD_DISPATCH_PROTOTYPES  
typedef void (*IMP)(void /* id, SEL, ... */ );   
#else  
typedef id (*IMP)(id, SEL, ...);   
#endif

當你向某個對象發送一條信息,可以由這個函數指針來指定方法的實現,它最終就會執行那段代碼,這樣可以繞開消息傳遞階段而去執行另一個方法實現。

  • cache
    顧名思義,Cache主要用來緩存,那它緩存什么呢?我們先在runtime.h文件看看它的定義
typedef struct objc_cache *Cache                             OBJC2_UNAVAILABLE;  
struct objc_cache {  
    unsigned int mask /* total = mask + 1 */                 OBJC2_UNAVAILABLE;  
    unsigned int occupied                                    OBJC2_UNAVAILABLE;  
    Method buckets[1]                                        OBJC2_UNAVAILABLE;  
};  

Cache其實就是一個存儲Method的鏈表,主要是為了優化方法調用的性能。當對象receiver調用方法message時,首先根據對象receiver的isa指針查找到它對應的類,然后在類的methodLists中搜索方法,如果沒有找到,就使用super_class指針到父類中的methodLists查找,一旦找到就調用方法。如果沒有找到,有可能消息轉發,也可能忽略它。但這樣查找方式效率太低,因為往往一個類大概只有20%的方法經常被調用,占總調用次數的80%。所以使用Cache來緩存經常調用的方法,當調用方法時,優先在Cache查找,如果沒有找到,再到methodLists查找。

消息發送
  • objc_msgSend函數
    在前面已經提過,當某個對象使用語法[receiver message]來調用某個方法時,其實[receiver message]被編譯器轉化為:
id objc_msgSend ( id self, SEL op, ... );  

現在讓我們看一下objc_msgSend它具體是如何發送消息:
1.首先根據receiver對象的isa指針獲取它對應的class;
2.優先在class的cache查找message方法,如果找不到,再到methodLists查找;
3.如果沒有在class找到,再到super_class查找;
4.一旦找到message這個方法,就執行它實現的IMP。

  • self與super
    為了讓大家更好地理解self和super,借用sunnyxx博客的iOS程序員6級考試一道題目:下面的代碼分別輸出什么?
@implementation Son : Father  
-(id)init  
{  
    self = [super init];  
    if (self)  
    {  
        NSLog(@"%@", NSStringFromClass([self class]));  
        NSLog(@"%@", NSStringFromClass([super class]));  
    }  
    return self;  
}  
@end  

self表示當前這個類的對象,而super是一個編譯器標示符,和self指向同一個消息接受者(son)。在本例中,無論是[self class]還是[super class],接受消息者都是Son對象,但super與self不同的是,self調用class方法時,是在子類Son中查找方法,而super調用class方法時,是在父類Father中查找方法。
當調用[self class]方法時,會轉化為objc_msgSend函數,這個函數定義如下:

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

這時會從當前Son類的方法列表中查找,如果沒有,就到Father類查找,還是沒有,最后在NSObject類查找到。我們可以從NSObject.mm文件中看到- (Class)class的實現:

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

所以NSLog(@"%@", NSStringFromClass([self class]));會輸出Son。
當調用[super class]方法時,會轉化為objc_msgSendSuper,這個函數定義如下:

id objc_msgSendSuper(struct objc_super *super, SEL op, ...) 

objc_msgSendSuper函數第一個參數super的數據類型是一個指向objc_super的結構體,從message.h文件中查看它的定義:

/// Specifies the superclass of an instance.   
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 */  
};  
#endif 

結構體包含兩個成員,第一個是receiver,表示某個類的實例。第二個是super_class表示當前類的父類。
這時首先會構造出objc_super結構體,這個結構體第一個成員是self,第二個成員是(id)class_getSuperclass(objc_getClass("Son")),實際上該函數會輸出Father。然后在Father類查找class方法,查找不到,最后在NSObject查到。此時,內部使用objc_msgSend(objc_super->receiver, @selector(class))去調用,與[self class]調用相同,所以結果還是Son。
Method Resolution
Fast Forwarding
Normal Forwarding
我的理解:objc_super結構體中receiver是son,superclass是father,而objc_msgSend(objc_super->receiver, @selector(class))內部是使用objc_super中的receiver作為消息發送者,也就是son調用NSObject中的class方法,所以結果是son。

方法解析與消息轉發

[receiver message]調用方法時,如果在message方法在receiver對象的類繼承體系中沒有找到方法,那怎么辦?一般情況下,程序在運行時就會Crash掉,拋出unrecognized selector sent to…類似這樣的異常信息。但在拋出異常之前,還有三次機會按以下順序讓你拯救程序。
1.Method Resolution 即所屬類動態方法解析
2.Fast Forwarding 即備援接受者
3.Normal Forwarding

  • Method Resolution首先Objective-C在運行時調用+ resolveInstanceMethod:或+ resolveClassMethod:方法,讓你添加方法的實現。如果你添加方法并返回YES,那系統在運行時就會重新啟動一次消息發送的過程。
    舉一個簡單例子,定義一個類Message,它主要定義一個方法sendMessage,下面就是它的設計與實現:
@interface Message : NSObject  
-(void)sendMessage:(NSString *)word;  
@end  
@implementation Message  
-(void)sendMessage:(NSString *)word  
{  
    NSLog(@"normal way : send message = %@", word);  
}  
@end  

如果我在viewDidLoad方法中創建Message對象并調用sendMessage方法:

-(void)viewDidLoad {  
    [super viewDidLoad];  
    Message *message = [Message new];  
    [message sendMessage:@"Sam Lau"];  
}  

控制臺會打印以下信息:

normal way : send message = Sam Lau

但現在我將原來sendMessage方法實現給注釋掉,覆蓋resolveInstanceMethod方法:

#pragma mark - Method Resolution  
/// override resolveInstanceMethod or resolveClassMethod for changing sendMessage method implementation  
+(BOOL)resolveInstanceMethod:(SEL)sel  
{  
    if (sel == @selector(sendMessage:)) {  
        class_addMethod([self class], sel, imp_implementationWithBlock(^(id self, NSString *word) {  
            NSLog(@"method resolution way : send message = %@", word);  
        }), "v@*");  
    }  
    return YES;  
} 

控制臺就會打印以下信息:

method resolution way : send message = Sam Lau

該方法主要原型為
BOOL class_addMethod(Class cls, SEL name, IMP imp, const char *types);
方法名為給class添加方法,返回一個Bool類型返回值。第一個參數為需要添加方法的類,第二個參數為實例方法的名字也就是+(BOOL)resolveInstanceMethod:(SEL)sel方法中的參數sel,第三個參數為IMP類型的變量也就是函數的實現。需要傳一個C函數,該函數至少要有兩個變量,一個是id self,一個是SEL _cmd,也可以傳字符串。
另一種寫法。

void dynamicAdditionMethodIMP(id self, SEL _cmd) {
    //實現
    NSLog(@"dynamicAdditionMethodIMP");
}
class_addMethod([self class], name, (IMP)dynamicAdditionMethodIMP, "v@:");

第四個參數傳入一個字符串"v@*,它表示方法的參數和返回值,詳情請參考Type Encodings。
如果resolveInstanceMethod方法返回NO,運行時就跳轉到下一步:消息轉發(Message Forwarding)。

  • Fast Forwarding
    當對象所屬類不能動態添加方法后,runtime就會詢問當前的接受者是否有其他對象可以處理這個未知的selector。方法就是:
- (id)forwardingTargetForSelector:(SEL)aSelector;

如果目標對象實現- forwardingTargetForSelector:方法,系統就會在運行時調用這個方法,只要這個方法返回的不是nil或self,也會重啟消息發送的過程,把這消息轉發給其他對象來處理。否則,就會繼續Normal Fowarding。
繼續上面Message類的例子,將sendMessage和resolveInstanceMethod方法注釋掉,然后添加forwardingTargetForSelector方法的實現:

#pragma mark - Fast Forwarding  
-(id)forwardingTargetForSelector:(SEL)aSelector  
{  
    if (aSelector == @selector(sendMessage:)) {  
        return [MessageForwarding new];  
    }  
    return nil;  
}  

此時還缺一個轉發消息的類MessageForwarding,這個類的設計與實現如下:

@interface MessageForwarding : NSObject  
-(void)sendMessage:(NSString *)word;  
@end  
@implementation MessageForwarding  
-(void)sendMessage:(NSString *)word  
{  
    NSLog(@"fast forwarding way : send message = %@", word);  
}  
@end  

此時,控制臺會打印以下信息:

fast forwarding way : send message = Sam Lau

這里叫Fast,是因為這一步不會創建NSInvocation對象,但Normal Forwarding會創建它,所以相對于更快點。

  • Normal Forwarding
    如果沒有使用Fast Forwarding來消息轉發,最后只有使用Normal Forwarding來進行消息轉發。它首先調用methodSignatureForSelector:方法來獲取函數的參數和返回值,如果返回為nil,程序會Crash掉,并拋出unrecognized selector sent to instance異常信息。如果返回一個函數簽名,系統就會創建一個NSInvocation對象并調用-forwardInvocation:方法。
    繼續前面的例子,將forwardingTargetForSelector方法注釋掉,添加methodSignatureForSelector和forwardInvocation方法的實現:
#pragma mark - Normal Forwarding  
-(NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector  
{  
    NSMethodSignature *methodSignature = [super methodSignatureForSelector:aSelector];  
    if (!methodSignature) {  
        methodSignature = [NSMethodSignature signatureWithObjCTypes:"v@:*"];  
    }  
    return methodSignature;  
}  
-(void)forwardInvocation:(NSInvocation *)anInvocation  
{  
    MessageForwarding *messageForwarding = [MessageForwarding new];  
    if ([messageForwarding respondsToSelector:anInvocation.selector]) {  
        [anInvocation invokeWithTarget:messageForwarding];  
    }  
}  
三種方法的選擇

Runtime提供三種方式來將原來的方法實現代替掉,那該怎樣選擇它們呢?

  • Method Resolution
    由于Method Resolution不能像消息轉發那樣可以交給其他對象來處理,所以只適用于在原來的類中代替掉。
  • Fast Forwarding
    它可以將消息處理轉發給其他對象,使用范圍更廣,不只是限于原來的對象。
  • Normal Forwarding
    它跟Fast Forwarding一樣可以消息轉發,但它能通過NSInvocation對象獲取更多消息發送的信息,例如:target、selector、arguments和返回值等信息。
Associated Objects

如果我們想對系統的類添加方法時,可以使用擴展,但是添加屬性時,只能使用繼承。如果不想使用繼承,則可以用runtime來關聯對象,這與我們自定義類的添加屬性不同。本質上是使用類別進行擴展,通過添加get方法和set方法從而在使用時可以使用點方法使用。與普通使用方法一致。
同時當使用Category對某個類進行擴展時,有時需要存儲屬性,Category是不支持的,這時需要使用Associated Objects來給已存在的類Category添加自定義的屬性。Associated Objects提供三個C語言API來向對象添加、獲取和刪除關聯值:

  • void objc_setAssociatedObject (id object, const void *key, id value, objc_AssociationPolicy policy )
  • id objc_getAssociatedObject (id object, const void *key )
  • void objc_removeAssociatedObjects (id object )

其中objc_AssociationPolicy是個枚舉類型,它可以指定Objc內存管理的引用計數機制。

  typedef OBJC_ENUM(uintptr_t, objc_AssociationPolicy) {  
    OBJC_ASSOCIATION_ASSIGN = 0,           /**< Specifies a weak reference to the associated object. */  
    OBJC_ASSOCIATION_RETAIN_NONATOMIC = 1, /**< Specifies a strong reference to the associated object.  
                                            *   The association is not made atomically. */  
    OBJC_ASSOCIATION_COPY_NONATOMIC = 3,   /**< Specifies that the associated object is copied.  
                                            *   The association is not made atomically. */  
    OBJC_ASSOCIATION_RETAIN = 01401,       /**< Specifies a strong reference to the associated object. 
                                            *   The association is made atomically. */  
    OBJC_ASSOCIATION_COPY = 01403          /**< Specifies that the associated object is copied. 
                                            *   The association is made atomically. */  
};  

下面有個關于NSObject+AssociatedObject Category添加屬性associatedObject的示例代碼:
NSObject+AssociatedObject.h

@interface NSObject (AssociatedObject)  
@property (strong, nonatomic) id associatedObject;  
@end 

NSObject+AssociatedObject.m

@implementation NSObject (AssociatedObject)  
- (void)setAssociatedObject:(id)associatedObject  
{  
    objc_setAssociatedObject(self, @selector(associatedObject), associatedObject, OBJC_ASSOCIATION_RETAIN_NONATOMIC);  
}  
- (id)associatedObject  
{  
    return objc_getAssociatedObject(self, _cmd);  
}  
@end 

Associated Objects的key要求是唯一并且是常量,而SEL是滿足這個要求的,所以上面的采用隱藏參數_cmd作為key。
一個給scrollview添加refreshView的實際用例:

@interface UIScrollView (Refresh)  
@property (nonatomic) RefreshView * refreshView;  
@end  
#import <objc/runtime.h>  
static char kRefreshView;  
@implementation UIScrollView (Refresh)  
@dynamic refreshView;  
  
- (void)setRefreshView:(RefreshView *)aView {  
      
    objc_setAssociatedObject(self, &kRefreshView, aView, OBJC_ASSOCIATION_RETAIN_NONATOMIC);  
}  
  
  
- (RefreshView *)refreshView {  
      
    return objc_getAssociatedObject(self, &kRefreshView);  
}  
  
@end  
GetClass

得到一個實例的類。代碼如下:

#import "ViewController.h"  
#import <objc/runtime.h>  
#import "Person.h"  
  
@interface ViewController ()  
  
@end  
  
@implementation ViewController  
  
- (void)viewDidLoad {  
    [super viewDidLoad];  
    // Do any additional setup after loading the view, typically from a nib.  
      
      
    Person * p1 = [[Person alloc] init];  
      
    Class c1 = object_getClass(p1);  
    NSLog(@"%@", c1);  
    Person * p2 = [[[c1 class] alloc] init];  
    NSLog(@"%@", p2.name);  
      
} 
isKindOfClass和isMemberOfClass

先看看isKindOfClass和isMemberOfClass在Object.mm中的實現:

- (BOOL)isKindOf:aClass
{
     Class cls;
     for (cls = isa; cls; cls = cls->superclass)
          if (cls == (Class)aClass)
               return YES;
     return NO;
}

- (BOOL)isMemberOf:aClass
{
     return isa == (Class)aClass;
}

可以看到isKindOfClass是取自身指針isa與參數指針做對比,如果相等返回YES,如果不等則取其superClass指針,直到superClass為空為止,循環結束返回空。

int main(int argc, const char * argv[]) {
     @autoreleasepool {
          BOOL res1 = [(id)[NSObject class] isKindOfClass:[NSObject class]];
          BOOL res2 = [(id)[NSObject class] isMemberOfClass:[NSObject class]];
          BOOL res3 = [(id)[Sark class] isKindOfClass:[Sark class]];
          BOOL res4 = [(id)[Sark class] isMemberOfClass:[Sark class]];
          NSLog(@"%d %d %d %d", res1, res2, res3, res4);
     }
     return 0;
}

//輸出
2014-11-05 14:45:08.474 Test[9412:721945] 1 0 0 0

res1中NSObject的isa指針指向NSOject的meta-class,繼續循環,meta-class的superClass是NSObject所以為ture;
res3中Sark的isa指針指向sark的meta-class,接下來指向NSObject的meta-class,再接下來指向NSObject;
res2中NSObject的isa指針指向NSOject的meta-class,與NSObject不一致;
res4中Sark的isa指針指向sark的meta-class,與Sark不一致。
注意上面是類與類相比。下面是類的實例與類相比:

Person *person = [[Person alloc] init];  
Teacher *teacher = [[Teacher alloc] init];  
  
//YES   
if ([teacher isKindOfClass:[Teacher class]]) {  
    NSLog(@"teacher 是 Teacher類或Teacher的子類");  
}  
//YES   
if ([teacher isKindOfClass:[Person class]]) {  
    NSLog(@"teacher 是 Person類或Person的子類");  
}  
//YES   
if ([teacher isKindOfClass:[NSObject class]]) {  
    NSLog(@"teacher 是 NSObject類或NSObject的子類");  
} 

// YES   
if ( [teacher respondsToSelector: @selector( setName: )] == YES ) {  
    NSLog(@"teacher responds to setSize: method" );  
}  
  
// NO   
if ( [teacher respondsToSelector: @selector( abcde )] == YES ) {  
    NSLog(@"teacher responds to nonExistant method" );  
}  
  
// YES   
if ( [Teacher respondsToSelector: @selector( alloc )] == YES ) {  
    NSLog(@"teacher class responds to alloc method\n" );  
}  
Method Swizzling

Method Swizzling就是在運行時將一個方法的實現代替為另一個方法的實現。由于Foundation等框架都是閉源的,我們沒有辦法直接修改代碼,通常情況下只能通過繼承,類別,關聯屬性等手段添加屬性和實例方法,較為繁瑣。而Method Swizzling可以通過將一個方法的實現代替另一個方法的實現,從而達到修改閉源代碼的目的。
例如一個想要統計每個頁面出現的次數,如果在所有頁面的viewWillAppear中都添加統計代碼,則太過繁瑣。那我們使用一個類別,并自定義一個方法替代系統的viewWillAppear,在其中做統計,并在統計結束后調用系統的viewWillAppear即可。

@interface UIViewController (MyUIViewController)

@end

@implementation UIViewController(MyUIViewController)
+ (void)load {
    static dispatch_once_t onceToken;
    dispatch_once(&onceToken, ^{
        
        SEL originalSelector = @selector(viewWillAppear:);
        Method originalMethod = class_getInstanceMethod([self class], originalSelector);
        
        SEL exchangeSelector = @selector(myViewWillAppear:);
        Method exchangeMethod = class_getInstanceMethod([self class], exchangeSelector);
        
        method_exchangeImplementations(originalMethod, exchangeMethod);
    });
}

- (void)myViewWillAppear:(BOOL)animated {
    //這里實際上是調用系統的viewWillAppear,不會遞歸調用。
    [self myViewWillAppear:animated];
    NSLog(@"MyViewWillAppear %@", [self class]);
}

使用load實現預加載。使用GCD的dispatch_once_t保證只會交換一次不會重復交換。使用method_exchangeImplementations來實現交換。
將上述代碼放入工程中就可以打印所有viewcontroller的加載情況。

Aspect-Oriented Programming(AOP)

類似記錄日志、身份驗證、緩存等事務非常瑣碎,與業務邏輯無關,很多地方都有,又很難抽象出一個模塊,這種程序設計問題,業界給它們起了一個名字叫橫向關注點(Cross-cutting concern),AOP作用就是分離橫向關注點(Cross-cutting concern)來提高模塊復用性,它可以在既有的代碼添加一些額外的行為(記錄日志、身份驗證、緩存)而無需修改代碼。

runtime如何實現weak置為nil

runtime對注冊的類會進行布局,對于weak修飾的對象會放入一個hash表中,用weak指向的對象內存地址作為key。當此對象的引用計數為0的時候會dealloc,假如weak指向的對象內存地址是a,那么就會以a為鍵在這個weak表中搜索,找到所有以a為鍵的weak對象,從而設置為nil。

NSString *name = [[NSString alloc] initWithString: @"Jiaming Chen"];
__weak NSString *weakStr = name;

當為weakStr這一weak類型的對象賦值時,編譯器會根據name的地址為key去查找weak哈希表,該表項的值為一個數組,將weakStr對象的地址加入到數組中,當name變量超出變量作用域或引用計數為0時,會執行dealloc函數,在執行該函數時,編譯器會以name變量的地址去查找weak哈希表的值,并將數組里所有 weak對象全部賦值為nil。

總結

雖然在平時項目不是經常用到Objective-C的Runtime特性,但當你閱讀一些iOS開源項目時,你就會發現很多時候都會用到。所以深入理解Objective-C的Runtime數據結構、消息轉發機制有助于你更容易地閱讀和學習開源項目。

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

推薦閱讀更多精彩內容

  • 轉至元數據結尾創建: 董瀟偉,最新修改于: 十二月 23, 2016 轉至元數據起始第一章:isa和Class一....
    40c0490e5268閱讀 1,746評論 0 9
  • 轉載:http://yulingtianxia.com/blog/2014/11/05/objective-c-r...
    F麥子閱讀 753評論 0 2
  • 我們常常會聽說 Objective-C 是一門動態語言,那么這個「動態」表現在哪呢?我想最主要的表現就是 Obje...
    Ethan_Struggle閱讀 2,222評論 0 7
  • 本文轉載自:http://yulingtianxia.com/blog/2014/11/05/objective-...
    ant_flex閱讀 772評論 0 1
  • runtime 和 runloop 作為一個程序員進階是必須的,也是非常重要的, 在面試過程中是經常會被問到的, ...
    made_China閱讀 1,215評論 0 7