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指針指向它自己。
上圖實線是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數據結構、消息轉發機制有助于你更容易地閱讀和學習開源項目。