Objective-C語言是一門動態(tài)語言,它將很多靜態(tài)語言在編譯和鏈接時期做的事放在運行時來處理,這種動態(tài)語言的優(yōu)勢在于:我們寫代碼時更具有靈活性,如我們可以把消息轉發(fā)給我們想要的對象或隨意交換一個方法的實現(xiàn)挚币。
這種特性意味著Objective-C不僅需要一個編譯器,還需要一個運行時系統(tǒng)來執(zhí)行編譯的代碼舶得。對于Objective-C來說梁肿。這個運行時系統(tǒng)就像一個操作系統(tǒng)一樣:它讓所有的工作可以正常的運行,這個運行時系統(tǒng)即Objc Runtime
。Objc Runtime
其實是一個Runtime
庫,它基本上是用C和匯編寫的,這個庫使得C語言有了面向對象的能力。
Runtime
庫主要做下面幾件事:
1.封裝:在這個庫中,對象可以用C語言中的結構體表示,而方法可以用C函數(shù)來實現(xiàn),另外加一些額外的特性与学。這些結構體和函數(shù)被runtime函數(shù)封裝后,我們可以在程序運行時創(chuàng)建,檢查,修改類對象和它們的方法了登渣。
2.找出方法的最終執(zhí)行代碼:當程序執(zhí)行[object dosomething]時,會向消息接受者(object)發(fā)送一條信息(doSomething),RunTime會根據(jù)消息接受者是否能響應消息做出不同的反應。
Class
Objective-C類是由Class類表示的,它實際是一個指向objc_class
結構體的指針,它的定義如下:
typedef struct objc_class *Class;
在objc/runtime.h
中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;
1.isa
:在Object-C中,所有的類的自身也是一個對象,類和類的實例沒有任何本質上的區(qū)別,任何對象都有isa
的指針邓了。isa
是一個Class類型的指針,每個實例對象都有一個isa的
指針,他指向對象的類,而類(Class)里也有個isa
的指針,指向meteClass(元類)恨诱。
2.super_class
:指向該類的父類,如果該類已經(jīng)是最頂層的根類(如NSObject)則super_class
為NULL。
3.char *name
: 類名骗炉。
4.version
:我們可以使用這個字段來提供類的版本信息照宝。
5.info
運行期使用的一些位標識。
6.instance_size
: 該類的實例變量大小句葵。
7.ivars
:objc_ivar_list
結構體存儲著objc_ivar
成員變量數(shù)組列表,而'obj_ivar'結構體存儲了類的單個成員變量的信息厕鹃。
在objec_class
中,所有得到成員變量,屬性是放在鏈表ivars
中的兢仰。ivars
是一個數(shù)組,數(shù)組中每個元素都指向Ivar
(變量信息)的指針啡氢。
objc_ivar_list *ivars
struct objc_ivar_list {
int ivar_count OBJC2_UNAVAILABLE;
ifdef LP64int space OBJC2_UNAVAILABLE;
endif/* variable length structure */
struct objc_ivar ivar_list[1] OBJC2_UNAVAILABLE;
} OBJC2_UNAVAILABLE;
Ivar
是表示實例變量的類型,其實際是一個指向objc_ivar
結構體的指針,其定義如下:
typedef struct objc_ivar *Ivar;
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īng)過runtime經(jīng)過struct的儲存形式存在,并且單個實例變量保存其名字寓娩、類型舞箍、偏移量和儲存空間貌亭。類中所有實例變量是以list類型進行儲存升敲。
8.objc_method_list
是Obj_method
方法列表媳维。
9.cache
:用于緩存最近使用的方法,一個接收者對象收到一個消息時,會根據(jù)isa
指針去查找能夠響應這個消息的對象,但是在實際使用中,這個對象只有一部分方法是常用的,很多方法很少或者根本用不上,這種情況下,如果每次消息來時,我們都是methodLists
中遍歷一遍,性能勢必很差,這時cache就有用了,在我們每次調用 一個方法后,這個方法就會被緩存到cache
列表中,下次調用的時候 runtime就會優(yōu)先去cache
中找,如果cache
沒有,才會去methodLists
中查找方法济锄。
objc_object 與 id
objc_object
是表示一個類的實例的結構體,它的定義如下(objc/objc.h
):
struct objc_object {
Class isa OBJC_ISA_AVAILABILITY;
};
typedef struct objc_object *id;
可以看到,objc_object
這個結構體只有一個字體Class isa OBJC_ISA_AVAILABILITY
是指向其類的isa
指針,這樣當我們向一個Objective-C對象發(fā)送消息時,Runtime
庫會根據(jù)實例對象的isa
指針找到這個實例對象所屬的類绒净。Runtime
庫會在類的方法列表及父類的方法列表中去尋找與消息對應的selector
指向的方法洪碳。找到后運行這個方法递览。
當創(chuàng)建一個特定類的實例 對象時,分配的內存包含一個objc_object
數(shù)據(jù)結構,然后是類的實例變量的數(shù)據(jù),NSObject類的alloc
和allocWithZone
方法使用函數(shù)class_createInstance
來創(chuàng)建objc_object
數(shù)據(jù)結構。
id
,它實際上是一個objc_object
結構類型的指針瞳腌。該類型的對象可以轉換為任何一種對象绞铃。
id
類型是動態(tài)類型的,即運行時再決定對象的類型。id
類型即通用的對象類,任何對象都可以被id指針所指,而在實際使用中,往往使用introspection來確定該對象的實際所屬類:
id obj = someInstance;
if ([obj isKindOfClass:someClass])
{
someClass *classSpecifiedInstance = (someClass *)obj;
// Do Something to classSpecifiedInstance which now is an instance of someClass
//...
}
元類(Meal Class)
所有的類的自身也是一個對象,我們可以向這個對象發(fā)送消息(即調用類方法),如:
NSDictionary *dictionary = [NSDictionary dictionary];
+dictionary
消息發(fā)送給了NSDictionary類,而這個NSDictionary也是一個
對象,既然是對象,那么它也是一個objc_object
指針,它包含一個指向其類的一個isa
指針嫂侍。為了調用+dictionary
方法,這個類的isa
指針必須指向一個包含這些類方法的一個objc_class
結構體儿捧。這就引出了meta-class
的概念
met-class是一個類對象的類。
當我們向一個對象發(fā)送消息時,runtime
會在這個對象所屬的這個類的方法列表中查找方法;而像一個類發(fā)送消息時,會在這個類的meta-class
的方法列表中查找挑宠。
meta-class
它存儲著一個類的所有類方法菲盾。每個類都會有一個單獨的meta-class
,每個類的類方法基本不可能完全相同。
meta-class
也是一個,也可以向它發(fā)送一個消息,那么它的isa
又是指向哪里,為了不讓這種結構無限延伸下去,Object-C的設計者讓所有的meta-class
的isa
指向基類的meta-class
,以此作為它們的所屬類各淀。即任何NSObject繼承體系下的meta-class
都會使用NSObject的meta-class
作為自己所屬類,而基類的isa
指針是指向它自己懒鉴。這樣就形成了一個完美的閉環(huán)
消息處理
SEL
SEL又叫選擇器,是表示一個方法的selector
的指針,其定義如下:
typedef struct objc_selector *SEL;
selector
用于表示運行時方法的名字,Objective-C編譯時,會根據(jù)每一個方法的名字、參數(shù)序列,生成一個唯一整型標識(Int類型的地址),這個標識就是SEL
,如下代碼所示:
SEL sel = @selector(method);
NSLog(@"sel : %p", sel);
上面的輸出為:
2016-11-28 14:04:41.151 RuntimeDemo[89015:1134944] sel : 0x103076a22**
兩個類之間,不管它們是父類與子類的關系,還是之間沒有這種關系,只要方法名相同,那么SEL就是一樣的,每一個方法都對應著一個SEL
碎浇。所以在Objective-C同一個類(及類的繼承體系)中,不能存在2個同名的方法,即使參數(shù)類型不同也不行临谱。相同的方法只能對應一個SEL。這就導致Objecttive-C在處理相同方法名字且參數(shù)個數(shù)相同但是參數(shù)類型不同的方法方面的能力很差奴璃。如:
- (void)dealTotalPriceWithProductCount:(int)productCount;
- (void)dealTotalPriceWithProductCount:(NSUInteger)productCount;
這種定義會被編譯器認為是一種編譯錯誤悉默。不同類的實例對象執(zhí)行相同的selector
時,會在各自的方法列表中去根據(jù)selector
去尋找自己對應的IMP
。
在一個工程中所有的SEL
組成一個Set
集合,Set 的特點是唯一,因此SEL是唯一的苟穆。因此,如果我們想到這個方法集合 中查找某個方法時,只要去找到這個方法對應的SEL就行了,SEL實際上就是根據(jù)方法名hash
化了一個字符串,而對字符串的比較僅僅需要比較它們的地址就可以了,速度上是非吵危快的。
本質上,SEL
只是一個指向方法的指針(準確說,只是一個方法名hash
化了的KEY
值,能唯一代表一個方法),它的存在只是為了加快方法的查詢速度雳旅。
我們可以在運行時添加新的selector
,也可以在運行時獲取已存在的selector
,有三種方法來獲取SEL:
1.sel_registerName
函數(shù)
2.Objective-C編譯器提供的@selector()
3.nSSelectorFromString()
方法
IMP
IMP
實際上是一個函數(shù)指針,指向方法實現(xiàn)的首地址跟磨。它是一個函數(shù)指針,由編譯器生成,SEL
就是為了查找方法的最終實現(xiàn)IMP
。每個方法對應唯一的SEL
,因此我們可以快速準確地獲得它對應的IMP
,取得IMP
后,我們就獲得了執(zhí)行這個方法代碼的入口點,通過取得IMP
,我們可以跳過Runtime的消息傳遞機制,直接執(zhí)行IMP
指向的函數(shù)實現(xiàn),這樣就省去了Runtime消息傳遞過程中的一系列查找操作,會比直接向對象發(fā)送消息高效一些攒盈。
id (*IMP)(id, SEL, ...)
第一個參數(shù)是指向self
的指針(如果是實例方法,則是類實例的內存地址;如果是類方法,則是指向元類的指針),第二個參數(shù)是方法選擇器(selector
),接下來是方法的實際參數(shù)列表抵拘。
Method
Method
用于表示類定義中的方法,則定義如下:
typedef struct objc_method *Method;
struct objc_method {
SEL method_name OBJC2_UNAVAILABLE; //方法名
char *method_types OBJC2_UNAVAILABLE; //方法類型
IMP method_imp OBJC2_UNAVAILABLE; // 方法實現(xiàn)
}
Method
結構體包含一個SEL
和 一個IMP
,實際上相當 與在SEL
和IMP
之間作了一個映射。有了SEL,我們可以找到對應的IMP
,從而調用方法的實現(xiàn)代碼沦童。
SEL
:代表方法名類型,在不同的類中定義,它們的方法選擇器也不一樣仑濒。
method_types
: method_types
方法類型是一個char指針,存儲著方法的參數(shù)類型和返回類型。
method_imp
指向了方法的實現(xiàn),本質上是一個函數(shù)指針偷遗。
方法調用流程
在Objective-C,消息直到運行時才綁定到方法實現(xiàn)上,編譯器會將消息表達式[receiver message]
轉化為一個消息函數(shù)的調用,即objc_msgSend
墩瞳。這個函數(shù)將消息接收者和方法名作為其基礎參數(shù),如下:
objc_msgSend(receiver, selector)
如果消息中還有其它參數(shù),則該方法的形式如下所示:
objc_msgSend(receiver, selector, arg1, arg2, ...)
這個函數(shù)完成了動態(tài)綁定的所有事情:
1..首先找到
selector`對應的方法實現(xiàn)。因為同一個方法可能在不同的類中有不同的實現(xiàn),所以我們需要依賴接受者的類來找到確切的實現(xiàn)氏豌。
2.它調用方法實現(xiàn),并將接受者對象及方法的所有參數(shù)傳給它喉酌。
3.最后,它將實現(xiàn)返回的值作為自己的返回值。
消息的關鍵在于前面有解釋的結構體objc_class
,這個結構體有連個字段是我們在分發(fā)消息的時候需要關注的:
1.指向父類的指針
2.一個類的方法分發(fā)表,即methodList
泵喘。
當我們創(chuàng)建一個新對象的時候,先為其分配內存,并初始化其成員變量泪电。其中isa指針也會被初始化,讓對象可以訪問類及類得得繼承體系。
當消息發(fā)送給一個對象時,objc_msgSend
通過對象的isa
指針獲取到類的結構體,然后在方法分發(fā)表里查找方法的selector
纪铺。如果沒有找到selector
,則通過objc_msgSend
結構體中的指向父類的指針找到其父類,并在父類的分發(fā)表里面查找方法的selector
相速。依次,會一直沿著類的繼承體系到達NSObject類。一旦定位到selector
,函數(shù)就獲取到了實現(xiàn)得得入口點,并傳入相應的參數(shù)來執(zhí)行方法的具體實現(xiàn)鲜锚。如果沒有定位到selector
,為了加速消息的處理,運行時系統(tǒng)緩存使用過的'selector'級對應的方法的地址突诬。
下面以實例對象調用方法[student speek]
為例描述調用的流程:
1.編譯器會把`[student speak]`轉化為`objc_msgSend(student, SEL)`,SEL為@selector(speek)。
2.runtime會在student對象對應的Student類的方法緩存列表里查找方法的SEL芜繁。
3.如果沒有找到,則在Student類的方法分發(fā)表查找SEL,類對象由對象isa指針指向,分發(fā)列表即methodList旺隙。
4.如果沒有找到,則在父類(設Student的父類是Person類)的方法分發(fā)表里查找方法的SEL(父類由類的superClass指向)。
5.如果沒有找到,則沿著繼承體系繼續(xù)找下去,最終到達NSObject類停止骏令。
6.如果在2 蔬捷、3 、4的其中一步找到,會通過SEL找打對應的IMP,即定位到了方法實現(xiàn)的入口,執(zhí)行具體實現(xiàn)榔袋。
7.如果最后還是沒有找到,則會進行消息轉發(fā)周拐。
獲取方法地址
Runtime中方法的動態(tài)綁讓我們寫代碼的時候更具有靈活性,如我們可以消息轉發(fā)給我們想要的對象,或者隨意交換一個方法的實現(xiàn)等動態(tài)綁定不過靈活性的提升也帶來了性能上的一些損耗。畢竟我們需要去查找方法的實現(xiàn)摘昌。而不像函數(shù)調用得那么直接速妖。當然方法得當緩存一定程度上解決了這一問題。
如果想要避開這種動態(tài)綁定方式,我們可以獲取方法實
Method Swizzing
Method swizzling 用于改變一個已經(jīng)存在的selector的實現(xiàn),這項技術使得在運行時候改變方法的調用成為可能聪黎。例如我們想要在一款iOS app中追蹤每一個界面呈現(xiàn)給力用戶多少次:可以通過在每個視圖控制器的viewDidAppear
方法中添加追蹤代碼來實現(xiàn),但這樣會有大量重復的代碼,繼承也會有同樣的問題,利用method swizzling可以較完美實現(xiàn):
#import <objc/runtime.h>
@implementation UIViewController (Tracking)
+ (void)load {
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
Class class = [self class];
//源方法的SEL
SEL originalSelector = @selector(viewWillAppear:);
//交換方法的SEL
SEL swizzledSelector = @selector(prefix_viewWillAppear:);
/*
通過class_getInstanceMethod( ) 函數(shù)從當前對象中的method list獲取method結構體,
如果類方法那么就使用class_getClassMethod ( ) 函數(shù)獲取罕容。
*/
Method originalMethod = class_getInstanceMethod(class, originalSelector);
Method swizzledMethod = class_getInstanceMethod(class, swizzledSelector);
/**
* 我們在這里使用class_addMethod()函數(shù)對Method Swizzling做了一層驗證,如果self沒有實現(xiàn)被交換的方法稿饰,會導致失敗锦秒。
* 而且self沒有交換的方法實現(xiàn),但是父類有這個方法喉镰,這樣就會調用父類的方法旅择,結果就不是我們想要的結果了。
* 所以我們在這里通過class_addMethod()的驗證侣姆,如果self實現(xiàn)了這個方法生真,class_addMethod()函數(shù)將會返回NO沉噩,我們就可以對其進行交換了。
*/
BOOL didAddMethod = class_addMethod(class,
originalSelector,
method_getImplementation(swizzledMethod),
method_getTypeEncoding(swizzledMethod)
);
if (didAddMethod) {
//添加成功:將源方法的實現(xiàn)替換到交換方法的實現(xiàn)
class_replaceMethod(class,
swizzledSelector,
method_getImplementation(originalMethod),
method_getTypeEncoding(originalMethod)
);
} else {
//添加失敗: 說明源方法已經(jīng)有實現(xiàn), 直接將兩個方法的實現(xiàn)交換即可
method_exchangeImplementations(originalMethod, swizzledMethod);
}
});
}
#pragma mark - Method Swizzling
- (void)xxx_viewWillAppear:(BOOL)animated {
[self xxx_viewWillAppear:animated];
NSLog(@"viewWillAppear: %@", self);
}
@end
swizzling應該只在+load中完成柱蟀。在Object-C的運行時中,每個類都有兩個方法自動調用川蒙。
+load是在一個類被初始裝載時調用,+initialize是在應用應用第一次調用該類的類方法或實例
方法前調用。兩個方法都是可選的,并且只有在方法被實現(xiàn)的情況下才會被調用长已。
** swizzling 應該只在 dispatch_once 中完成**
由于 swizzling 改變了全局的狀態(tài)畜眨,所以我們需要確保每個預防措施在運行時都是可用的。原子操作就是這樣一個用于確保代碼只會被執(zhí)行一次的預防措施术瓮,就算是在不同的線程中也能確保代碼只執(zhí)行一次康聂。Grand Central Dispatch 的 dispatch_once 滿足了所需要的需求,并且應該被當做使用 swizzling 的初始化單例方法的標準胞四。