這段時間在公司要做一個組件開發(fā),需要用到OC Runtime特性的地方很多,于是在以前的了解上又惡補(bǔ)了一下相關(guān)知識屡贺,以下是自己的一些總結(jié)。如果有不對的地方,歡迎大家及時指出.
一甩栈、Runtime 是什么泻仙?
Runtime機(jī)制是Objective-C的一個重要特性,是其區(qū)別于C語言這種靜態(tài)語言的根本量没,C語言的函數(shù)調(diào)用會在編譯期確定好玉转,在編譯完成后直接順序執(zhí)行。而OC是一門動態(tài)語言殴蹄,函數(shù)調(diào)用變成了消息發(fā)送(msgSend)究抓,在編譯期不能確定調(diào)用哪個函數(shù),所以Runtime就是解決如何在運(yùn)行期找到調(diào)用方法這樣的問題袭灯。
二刺下、類的結(jié)構(gòu)定義
要想理解清楚Runtime,首先要清楚的了解類的結(jié)構(gòu)稽荧, 因為Objective-C 是面向?qū)ο笳Z言橘茉,所以可以說 OC 里“一切皆對象”,首先要牢記這一點(diǎn)姨丈。眾所周知一個實例instance 是一個類實例化生成的對象(以下簡稱實例對象)畅卓,那各個不同的類呢?實際上各個不同的類本質(zhì)上也是各個不同的對象(以下簡稱類對象)
先來看張圖:
上圖中:
superClass:類對象所擁有的父類指針蟋恬,指向當(dāng)前類的父類.
isa: 實例和類對象都擁有的指針翁潘,指向所屬類,即當(dāng)前對象由哪個類構(gòu)造生成.
所以從上圖我們可以得出以下幾點(diǎn)結(jié)論:
- 實例對象的isa指針指向所屬類筋现,所屬類的isa指針指向元類(metaClass) .
- metaClass也有isa 和superClass 指針唐础,其中isa指針指向Root class (meta) 根元類.
- superClass 指針追溯整個繼承鏈,自底向上直至根類 (NSObject或NSProxy) .
- Root class (meta) 根元類的superClass指針指向根類
- 根類和根元類的isa 指針都指向Root class (meta) 根元類
好矾飞,到這里我們清楚的了解了實例和類在內(nèi)存中的布局構(gòu)造一膨,那么接下來我們來看一下類的結(jié)構(gòu)定義,在 objc/runtime.h中洒沦,類由objc_class結(jié)構(gòu)體構(gòu)造而成豹绪,如下是在objc/runtime.h中,類的結(jié)構(gòu)的定義:
struct objc_class {
Class isa OBJC_ISA_AVAILABILITY; // isa指針 指向所屬類
#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; // 遵循的協(xié)議列表
#endif
} OBJC2_UNAVAILABLE;
/* Use `Class` instead of `struct objc_class *` */
結(jié)合上述結(jié)構(gòu)定義和Runtime提供的一系列方法,我們可以輕而易舉的獲取一個類的相關(guān)信息括尸,譬如:成員變量列表ivars巷蚪、實例方法列表methodLists、遵循協(xié)議列表protocols和屬性列表propertyList等濒翻。
下面簡單的列舉一下獲取相關(guān)列表的方法:
#import <objc/runtime.h>
例如:獲取UIView類的相關(guān)信息
id LenderClass = objc_getClass("UIView");
unsigned int outCount, i;
//獲取成員變量列表
Ivar *ivarList = class_copyIvarList(LenderClass, &outCount);
for (i=0; i<outCount; i++) {
Ivar ivar = ivarList[i];
fprintf(stdout, "Ivar:%s \n", ivar_getName(ivar));
}
//獲取實例方法列表
Method *methodList = class_copyMethodList(LenderClass, &outCount);
for (i=0; i<outCount; i++) {
Method method = methodList[i];
NSLog(@"instanceMethod:%@", NSStringFromSelector(method_getName(method)));
}
//獲取協(xié)議列表
__unsafe_unretained Protocol **protocolList = class_copyProtocolList(LenderClass, &outCount);
for (i=0; i<outCount; i++) {
Protocol *protocol = protocolList[i];
fprintf(stdout, "protocol:%s \n", protocol_getName(protocol));
}
//獲取屬性列表
objc_property_t *properties = class_copyPropertyList(LenderClass, &outCount);
for (i = 0; i < outCount; i++) {
objc_property_t property = properties[i];
//第二個輸出為屬性特性屁柏,包含類型編碼啦膜、讀寫權(quán)限等
fprintf(stdout, "%s %s\n", property_getName(property), property_getAttributes(property));
}
//注意釋放
free(ivarList);
free(methodList);
free(protocolList);
free(properties);
通過上面的列子,我們不難發(fā)現(xiàn)一個對象所擁有的實例方法淌喻,都注冊在其所屬類的方法列表methodLists中僧家,同理你會發(fā)現(xiàn)所有的類方法,都注冊在這個類所屬元類的方法列表中裸删。
三八拱、Method
既然我們已經(jīng)清楚的知道不同類型的方法都保存在相對應(yīng)的方法列表methodLists中,那方法列表中所存儲的方法的結(jié)構(gòu)又是怎樣的呢涯塔?弄清這一點(diǎn)對我們下面理解方法調(diào)用很有幫助肌稻。
好,我們回頭看下上面類的結(jié)構(gòu)定義中方法列表的定義:
struct objc_method_list **methodLists OBJC2_UNAVAILABLE; // 方法地址列表
不難發(fā)現(xiàn)methodLists是一個指向objc_method_list 結(jié)構(gòu)體類型指針的指針伤塌,那objc_method_list 的結(jié)構(gòu)又是怎樣的呢灯萍?在runtime.h里搜索其定義:
struct objc_method {
SEL method_name //方法id OBJC2_UNAVAILABLE;
char *method_types //各參數(shù)和返回值類型的typeEncode OBJC2_UNAVAILABLE;
IMP method_imp //方法實現(xiàn) OBJC2_UNAVAILABLE;
} OBJC2_UNAVAILABLE;
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里有SEL 和 IMP 這兩個關(guān)鍵點(diǎn):
- SEL:在編譯時期,根據(jù)根據(jù)方法名字生成的唯一int標(biāo)識(會轉(zhuǎn)為char*字符串使用)齿风,可以將其理解為方法的ID药薯。
- IMP:方法實現(xiàn)、函數(shù)指針救斑,該指針指向最終的函數(shù)實現(xiàn)童本。
SEL 結(jié)構(gòu)如下:
typedef struct objc_selector *SEL;
struct objc_selector {
char *name; OBJC2_UNAVAILABLE;
char *types; OBJC2_UNAVAILABLE;
};
注:既然SEL 和 IMP 一一對應(yīng),那么方法列表中會存在兩個SEL相同的方法嗎脸候?
答案是:會的穷娱。因為methodLists方法列表是一個數(shù)組,當(dāng)我們給一個類添加一個分類运沦,并在分類中重寫這個類的方法時泵额,編譯后會發(fā)現(xiàn)方法列表中有兩個SEL相同的method,對應(yīng)兩個不同的IMP携添,那么當(dāng)調(diào)用這個方法時嫁盲,會調(diào)用執(zhí)行那個IMP呢?答案是分類的那個烈掠,原理會在以后的文章中補(bǔ)上羞秤。
相信到這里,你已經(jīng)大致猜到了方法調(diào)用的過程左敌,其實一個方法的調(diào)用就是通過方法名生成的SEL瘾蛋,到相應(yīng)類的方法列表methodLists中,遍歷查找相匹配的IMP矫限,獲取最終實現(xiàn)并執(zhí)行的過程哺哼。當(dāng)然OC實現(xiàn)這些過程京革,還依賴于一個Runtime的核心:objc_msgSend
四、objc_msgSend
objc_msgSend定義:
/**
* Sends a message with a simple return value to an instance of a class.
*
* @param self A pointer to the instance of the class that is to receive the message.
* @param op The selector of the method that handles the message.
* @param ...
* A variable argument list containing the arguments to the method.
*
* @return The return value of the method.
*/
void objc_msgSend(void /* id self, SEL op, ... */ )
OC中所有的方法調(diào)用最終都會走到objc_msgSend去調(diào)用幸斥,這個方法支持任意返回值類型匹摇,任意參數(shù)類型和個數(shù)的函數(shù)調(diào)用。
支持所有的函數(shù)調(diào)用甲葬?廊勃?這不是違背了Calling Convention(“調(diào)用規(guī)則”)?這樣最后底層執(zhí)行的時候能夠正確取參并正確返回嗎经窖?難道不會報錯崩潰坡垫?答案是當(dāng)然不會,因為objc_msgSend是用匯編寫的画侣,調(diào)用執(zhí)行的時候冰悠,直接執(zhí)行自己的匯編代碼就OK了,不再需要編譯器根據(jù)相應(yīng)的調(diào)用規(guī)則生成匯編指令配乱,所以它也就不需要遵循相應(yīng)的調(diào)用規(guī)則溉卓。后續(xù)會寫一篇Libffi相關(guān)的文章表述一下。
當(dāng)一個對象調(diào)用[receiver message]的時候搬泥,會被改寫成objc_magSend(self桑寨,_cmd,···)忿檩,其中self 是指向消息接受者的指針尉尾,_cmd 是根據(jù)方法名生成的SEL,后面是方法調(diào)用所需參數(shù)燥透。執(zhí)行過程中就會拿著生成的SEL沙咏,到消息接受者所屬類的方法列表中遍歷查找對應(yīng)的IMP,然后調(diào)用執(zhí)行班套≈辏可以看出OC的方法調(diào)用中間經(jīng)歷了一系列過程,而不是像C一樣直接按地址取用孽尽,所以我們可以利用這一點(diǎn)窖壕,在消息處理的過程中對消息做一些特殊處理,譬如:消息的轉(zhuǎn)發(fā)杉女,消息的替換瞻讽,消息的防崩潰處理等。
objc_msgSend 調(diào)用流程:
- 檢查SEL是否應(yīng)該被忽略
- 檢查target 是否為空熏挎,為空則忽略該消息
- 查找與SEL相匹配的IMP
- 如果是調(diào)用實例方法速勇,則通過isa指針找到實例對象所屬類,遍歷其緩存列表及方法列表查找對應(yīng)IMP坎拐,如果找不到則去super_class指針?biāo)父割愔胁檎曳炒牛敝粮?
- 如果是調(diào)用類方法养匈,則通過isa指針找到類對象所屬元類,遍歷其緩存列表及方法列表查找對應(yīng)IMP都伪,如果找不到則去super_class指針?biāo)父割愔胁檎遗缓酰敝粮?
- 如果都沒找到,則轉(zhuǎn)向攔截調(diào)用陨晶,進(jìn)行消息動態(tài)解析
- 如果沒有覆寫攔截調(diào)用相關(guān)方法猬仁,則程序報錯:
unrecognized selector sent to instance.
注:上述過程中的緩存列表就是類結(jié)構(gòu)定義中的 struct objc_cache *cache
因為OC調(diào)用要經(jīng)過一系列的流程比較慢,所以引入了緩存列表機(jī)制先誉,調(diào)用過的方法會存到緩存列表中湿刽,這一點(diǎn)極大的提高了OC函數(shù)調(diào)用的效率。
五褐耳、動態(tài)消息解析
如四所述诈闺,如果在objc_msgSend調(diào)用的前3個步驟結(jié)束,還未找到SEL 對應(yīng) IMP铃芦,則會轉(zhuǎn)向動態(tài)消息解析流程雅镊,也可簡稱為攔截調(diào)用,所謂攔截調(diào)用就是在消息無法處理 unrecognized selector sent to instance.
之前杨帽,我們有機(jī)會覆寫NSObject 的幾個方法來處理消息漓穿,這也正是OC 動態(tài)性的體現(xiàn)。
這幾個方法分別是:
/* 所調(diào)用類方法是否為動態(tài)添加 */
+ (BOOL)resolveClassMethod:(SEL)sel;
/* 所調(diào)用實例方法是否為動態(tài)添加 */
+ (BOOL)resolveInstanceMethod:(SEL)sel;
/* 將消息轉(zhuǎn)發(fā)到其他目標(biāo)對象處理 */
- (id)forwardingTargetForSelector:(SEL)aSelector;
/* 返回方法簽名 */
- (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector注盈;
/* 在這里觸發(fā)調(diào)用 */
- (void)forwardInvocation:(NSInvocation *)anInvocation;
同時來看張網(wǎng)上流轉(zhuǎn)較廣的關(guān)于動態(tài)消息解析的流程圖:
流程說明:
- 通過
resolveClassMethod:
和resolveInstanceMethod:
判斷所調(diào)用方法是否為動態(tài)添加,默認(rèn)返回NO叙赚,返回YES則通過 class_addMethod 動態(tài)添加方法老客,處理消息。 -
forwardingTargetForSelector:
將消息轉(zhuǎn)發(fā)給某個指定的目標(biāo)對象來處理震叮,效率比較高胧砰,如果返回空則進(jìn)入下一步。 -
methodSignatureForSelector:
此方法用于方法簽名苇瓣,將調(diào)用方法的參數(shù)類型和返回值進(jìn)行封裝并返回尉间,如果返回nil,則說明消息無法處理unrecognized selector sent to instance.
击罪,正常返回則進(jìn)入forwardInvocation:
此步拿到的anInvocation哲嘲,包含了方法調(diào)用所需要的全部信息,在這里可以修改方法實現(xiàn)媳禁,修改響應(yīng)對象眠副,然后invoke 執(zhí)行,執(zhí)行成功則結(jié)束竣稽。失敗則報錯unrecognized selector sent to instance.
六囱怕、Runtime相關(guān)實踐
經(jīng)過上面的講解霍弹,相信大家已經(jīng)對Runtime 的原理有了比較清晰的理解,那么下面我們來看看Runtime的相關(guān)應(yīng)用吧娃弓。
- 動態(tài)添加方法
如果我們調(diào)用一個方法列表中不存在的方法newMethod:
典格,根據(jù)上述的動態(tài)消息解析流程可知,會先走進(jìn)resolveClassMethod:
或 resolveInstanceMethod:
台丛,假設(shè)消息接受者receiver為一個實例對象:
/* 調(diào)用一個不存在的方法 */
[receiver performSelector:@selector(newMethod:) withObject:@"add_newMethod_suc"];
層層查找方法列表均為找到對應(yīng)IMP钝计,轉(zhuǎn)向動態(tài)消息解析,此時需要在目標(biāo)對象的類里重寫resolveInstanceMethod:
void newMethod(id self, SEL _cmd, NSString *string){
NSLog(@"%@", string);
}
+ (BOOL)resolveInstanceMethod:(SEL)sel {
if (sel == @selector(newMethod)) {
// 參數(shù)依次是:給哪個類添加方法齐佳、方法ID:SEL私恬、函數(shù)實現(xiàn):IMP、方法類型編碼:types
class_addMethod(self, @selector(newMethod), newMethod, "v@:@");
return YES;
}
return [super resolveInstanceMethod:sel];
}
/**class_addMethod
* Adds a new method to a class with a given name and implementation.
*
* @param cls The class to which to add a method.
* @param name A selector that specifies the name of the method being added.
* @param imp A function which is the implementation of the new method. The function must take at least two arguments—self and _cmd.
* @param types An array of characters that describe the types of the arguments to the method.
*
* @return YES if the method was added successfully, otherwise NO
*/
BOOL class_addMethod(Class cls, SEL name, IMP imp, const char *types)
- 方法替換 & 關(guān)聯(lián)對象
關(guān)于這兩個方面炼吴,下面通過一個UIButton的防重點(diǎn)擊的實現(xiàn)來說明:
#import <UIKit/UIKit.h>
@interface UIButton (IgnoreEvent)
// 按鈕點(diǎn)擊的間隔時間
@property (nonatomic, assign) NSTimeInterval clickDurationTime;
@end
#import "UIButton+IgnoreEvent.h"
#import <objc/runtime.h>
// 默認(rèn)的點(diǎn)擊間隔時間
static const NSTimeInterval defaultDuration = 0.0001f;
// 記錄是否忽略按鈕點(diǎn)擊事件本鸣,默認(rèn)第一次執(zhí)行事件
static BOOL _isIgnoreEvent = NO;
// 設(shè)置執(zhí)行按鈕事件狀態(tài)
static void resetState() {
_isIgnoreEvent = NO;
}
@implementation UIButton (IgnoreEvent)
@dynamic clickDurationTime;
+ (void)load {
SEL originSEL = @selector(sendAction:to:forEvent:);
SEL mySEL = @selector(my_sendAction:to:forEvent:);
Method originM = class_getInstanceMethod([self class], originSEL);
IMP originIMP = method_getImplementation(originM);
const char *typeEncodinds = method_getTypeEncoding(originM);
Method newM = class_getInstanceMethod([self class], mySEL);
IMP newIMP = method_getImplementation(newM);
// 方法替換
if (class_addMethod([self class], originSEL, newIMP, typeEncodinds)) {
class_replaceMethod([self class], mySEL, originIMP, typeEncodinds);
} else {
method_exchangeImplementations(originM, newM);
}
}
- (void)my_sendAction:(SEL)action to:(id)target forEvent:(UIEvent *)event {
if ([self isKindOfClass:[UIButton class]]) {
//1. 按鈕點(diǎn)擊間隔事件
self.clickDurationTime = self.clickDurationTime == 0 ? defaultDuration : self.clickDurationTime;
//2. 是否忽略按鈕點(diǎn)擊事件
if (_isIgnoreEvent) {
//2.1 忽略按鈕事件
return;
} else if(self.clickDurationTime > 0) {
//2.2 不忽略按鈕事件
// 后續(xù)在間隔時間內(nèi)直接忽略按鈕事件
_isIgnoreEvent = YES;
// 間隔事件后,執(zhí)行按鈕事件
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(self.clickDurationTime * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
resetState();
});
// 發(fā)送按鈕點(diǎn)擊消息
[self my_sendAction:action to:target forEvent:event];
}
} else {
[self my_sendAction:action to:target forEvent:event];
}
}
#pragma mark - associate
// 關(guān)聯(lián)對象
- (void)setClickDurationTime:(NSTimeInterval)clickDurationTime {
objc_setAssociatedObject(self, @selector(clickDurationTime), @(clickDurationTime), OBJC_ASSOCIATION_RETAIN_ASSIGN);
}
- (NSTimeInterval)clickDurationTime {
return [objc_getAssociatedObject(self, @selector(clickDurationTime)) doubleValue];
}
@end
上述方法交換的代碼已經(jīng)很清楚了硅蹦,簡單說下關(guān)聯(lián)對象的兩個函數(shù):
- 設(shè)置關(guān)聯(lián)對象 :
objc_setAssociatedObject(id object, const void *key, id value, objc_AssociationPolicy policy)
-
id object
:給誰設(shè)置關(guān)聯(lián)對象 -
const void *key
: 關(guān)聯(lián)對象唯一的key -
id value
: 關(guān)聯(lián)對象的值 -
objc_AssociationPolicy policy
:關(guān)聯(lián)策略荣德,有以下幾種:
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. */
};
```
- 獲取關(guān)聯(lián)對象 :
id objc_getAssociatedObject(id object, const void *key)
-
id object
:獲取誰的關(guān)聯(lián)對象 -
const void *key
: 根據(jù)key獲取相應(yīng)的關(guān)聯(lián)對象值
Runtime的相關(guān)應(yīng)用還有很多很多,大家可以在以后的開發(fā)過程中慢慢探索童芹。
綜上涮瞻,就是這次對Runtime的一些總結(jié),對于Runtime整體來說可能只是很小的一部分假褪,但是對于大家理解一些常見的Runtime使用應(yīng)該還是有所幫助的署咽,鑒于蘋果API一直在更新和自己能力尚淺,文章中如有錯誤或不妥之處生音,還請大家及時指出宁否。