強(qiáng)烈推薦,關(guān)于runtime
只需要看下一縷殤流化隱半邊冰霜
的這幾篇文章就夠凸主。
作者:一縷殤流化隱半邊冰霜
神經(jīng)病院Objective-C Runtime住院第二天——消息發(fā)送與轉(zhuǎn)發(fā)
神經(jīng)病院Objective-C Runtime入院第一天——isa和Class
神經(jīng)病院Objective-C Runtime出院第三天——如何正確使用Runtime
**說明:此文是自己的總結(jié)筆記橘券,主要參考這幾篇文章:
iOS開發(fā)-Runtime詳解
NSHipster里面的這兩篇文章 :
Associated Objects
Method Swizzling
**
一.Runtime簡介
-
Runtime又叫運(yùn)行時,是一套底層的C語言API,其為iOS內(nèi)部的核心之一卿吐,我們平時編寫的OC代碼旁舰,底層都是基于它來實(shí)現(xiàn)的。
比如:[receiver doSomething]; 底層運(yùn)行會被編譯器轉(zhuǎn)化為: objc_msgSend(receiver, @selector(doSomething)) 如果帶有參數(shù)比如: [receiver doSomething:(id)arg...]; 底層運(yùn)行時會被編譯器轉(zhuǎn)化為: objc_msgSend(receiver, @selector(doSomething), arg1, arg2, ...)
可能通過以上你看不出它的價值嗡官,但是我們需要了解OC是一門動態(tài)語言箭窜,它會將一些工作放在代碼運(yùn)行時才處理而非在編譯的時候,也就是說衍腥,有很多類和成員變量在我們編譯的時候是不知道的绽快,而在運(yùn)行時芥丧,我們所編寫的代碼會轉(zhuǎn)換成完整的代碼運(yùn)行。
因此坊罢,編譯器是不夠,我們還需要一個運(yùn)行時的系統(tǒng)來處理編譯后的代碼
- Runtime 基本是C和匯編寫的擅耽,可以充分保證動態(tài)系統(tǒng)的高效性
二. Runtime的作用:
- 獲取某個類的所有成員變量
- 獲取某個類的所有屬性
- 獲取某個類的所有方法
- 交換方法實(shí)現(xiàn)
- 動態(tài)添加一個成員變量
- 動態(tài)添加一個方法
三.Runtime的術(shù)語的數(shù)據(jù)結(jié)構(gòu)
1.SEL
它是selector在objc中的表示(Swift 中是 Selector類)活孩。selector 是方法選擇器,其實(shí)作用是對方法名進(jìn)行包裝乖仇,以便找到對應(yīng)的方法實(shí)現(xiàn)(注意:Objc 在相同的類中不會有命名相同的兩個方法)憾儒。
對應(yīng)的數(shù)據(jù)結(jié)構(gòu):
typedef struct objc_selector *SEL;
我們可以看出它是個映射到方法的C字符串,你可以通過Objc編譯器命令@selector()或者Runtime系統(tǒng)的sel_registerName函數(shù)來獲取一個SEL類型的方法選擇器乃沙。注意:不同類中相同名字的方法所對應(yīng)的selector是相同的起趾,由于變量的類型不同(即實(shí)例變量所對應(yīng)的類的類型不同如NSString、NSMutableString等類型不同)警儒,所以不會導(dǎo)致它們調(diào)用方法實(shí)現(xiàn)的混亂训裆。
2.id
id 是一個參數(shù)類型,它是指向某個類的實(shí)例的指針蜀铲。定義如下:
typedef struct objc_object *id;
struct objc_object { Class isa; };
通過以上定義边琉,可以看到:objc_object結(jié)構(gòu)體包含一個isa指針,根據(jù)isa指針就可以找到對象所屬的類记劝。
注意:isa指針在代碼運(yùn)行時并不總是指向?qū)嵗龑ο笏鶎俚念愋捅湟蹋圆荒芤揽克鼇泶_定類型,要想確定類型需要用對象的 - class方法厌丑。
3.Class
typedef struct objc_class *Class;
Class 其實(shí)是指向 objc_class 結(jié)構(gòu)體的指針定欧。objc_class的數(shù)據(jù)結(jié)構(gòu)如下:
struct objc_class {
Class isa;//指針,顧名思義怒竿,表示是一個什么砍鸠,
//實(shí)例的isa指向類對象,類對象的isa指向元類
#if !__OBJC2__
Class super_class; //指向父類
const char *name; //類名
long version; // 類的版本信息愧口,初始化默認(rèn)為0睦番,可以通過runtime函數(shù)class_setVersion和class_getVersion進(jìn)行修改、讀取
long info; // 一些標(biāo)識信息,如CLS_CLASS (0x1L) 表示該類為普通 class 耍属,其中包含對象方法和成員變量;CLS_META (0x2L) 表示該類為 metaclass托嚣,其中包含類方法;
long instance_size; // 該類的實(shí)例變量大小(包括從父類繼承下來的實(shí)例變量);
struct objc_ivar_list *ivars // 成員變量列表
struct objc_method_list **methodLists; // 方法列表
struct objc_cache *cache;// 緩存,存儲最近使用的方法指針,用于提升效率
struct objc_protocol_list *protocols // 協(xié)議列表
#endif
} OBJC2_UNAVAILABLE;
/* Use `Class` instead of `struct objc_class *` */
從結(jié)構(gòu)體可以看出厚骗,一個運(yùn)行時類中關(guān)聯(lián)了它的父類指針示启、類名、成員變量领舰、方法夫嗓、緩存以及附屬的協(xié)議迟螺。
其中objc_ivar_list和objc_method_list 分別是成員變量列表和方法列表:
// 成員變量列表
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;
} 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;
}
我們都知道,OC中一切都被設(shè)計(jì)成對象舍咖,一個類被初始化成一個實(shí)例矩父,這個實(shí)例是一個對象。實(shí)際上一個類的本質(zhì)也是一個對象排霉,在runtime中用如上結(jié)構(gòu)體表示窍株。
關(guān)于isa指針:
比如 : NSString *tmpStr = [NSString string];
這里的tmpStr的isa指針指向類對象NSString,而NSString的isa指針指向元類NSObject.
4. 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;
}
objc_method存儲了方法名、方法類型和方法實(shí)現(xiàn):
方法名類型為SEL
方法類型 method_types 是個 char 指針攻柠,存儲方法的參數(shù)類型和返回值類型
method_imp 指向了方法的實(shí)現(xiàn)球订,本質(zhì)是一個函數(shù)指針
5.Ivar
Ivar 是成員變量的類型。
typedef struct objc_ivar *Ivar;
struct objc_ivar {
char *ivar_name // 變量名稱;
char *ivar_type // 變量類型;
int ivar_offset // 基地址偏移字節(jié);
#ifdef __LP64__
int space // 大小;
#endif
}
其中 ivar_offset 是基地址偏移字節(jié)
6.IMP
IMP在objc.h中的定義是:
typedef id (*IMP)(id, SEL, ...);
它是一個函數(shù)指針瑰钮,這是由編譯器生成的冒滩。當(dāng)你發(fā)送一個objc消息之后,最終它會執(zhí)行那段代碼浪谴,就是由這個函數(shù)指針指定的开睡。而IMP這個函數(shù)指針就指向了這個方法的實(shí)現(xiàn)。
我們發(fā)現(xiàn)IMP指向的方法和objc_msgSend函數(shù)類型相同较店,參數(shù)都包含id和SEL類型士八。每個方法名都對應(yīng)一個SEL類型的方法選擇器,而每個實(shí)例對象中的SEL對應(yīng)的方法實(shí)現(xiàn)肯定是唯一的梁呈,通過一組id和SEL參數(shù)就能確定唯一的實(shí)現(xiàn)方法地址婚度。所以一個確定的方法也只有一組id和SEL參數(shù):
比如:
NSString *tmpStr = [NSString string];
BOOL isContain = [tmpStr containsString:@"1"];
這里的containsString的實(shí)現(xiàn)方法就是由id(NSString)和SEL參數(shù)(containsString)確定的。
7.Cache
typedef struct objc_cache *Cache
struct objc_cache {
unsigned int mask /* total = mask + 1 */ OBJC2_UNAVAILABLE;
unsigned int occupied OBJC2_UNAVAILABLE;
Method buckets[1] OBJC2_UNAVAILABLE;
};
Cache主要用來提高查找效率官卡,當(dāng)一個方法被調(diào)用蝗茁,首先在Cache列表中查找,如果找到直接返回寻咒,如果沒有找到哮翘,再到類的方法列表去查找,找到了將該方法返回同時存入緩存列表毛秘。
8.Property
typedef struct objc_property *Property;
typedef struct objc_property *objc_property_t;//這個更常用
可以通過class_copyPropertyList 和 protocol_copyPropertyList 方法獲取類和協(xié)議中的屬性:
objc_property_t *class_copyPropertyList(Class cls, unsigned int *outCount)
objc_property_t *protocol_copyPropertyList(Protocol *proto, unsigned int *outCount)
注意返回的是屬性列表饭寺,列表中的每個元素都是一個objc_property_t指針
-
總結(jié):
// 描述類中的一個方法 typedef struct objc_method *Method; // 實(shí)例變量 typedef struct objc_ivar *Ivar; // 緩存(類方法) typedef struct objc_cache *Cache; // 實(shí)現(xiàn) 方法 typedef id (*IMP)(id, SEL, ...); // 類別Category typedef struct objc_category *Category; // 類中聲明的屬性 typedef struct objc_property *objc_property_t;
四.獲取列表
有時候會有這樣的需求叫挟,我們需要知道當(dāng)前類中每個屬性的名字(比如字典轉(zhuǎn)模型艰匙,字典的key和模型對象的屬性名字不匹配)。
我們可以通過runtime的一系列方法獲取類的一些信息(包括屬性列表抹恳、方法列表员凝、成員變量列表和遵循的協(xié)議列表)
unsigned int count;
//獲取屬性列表
objc_property_t *propertyList = class_copyPropertyList([self class], &count);
for (unsigned int i=0; i<count; i++) {
const char *propertyName = property_getName(propertyList[i]);
NSLog(@"property---->%@", [NSString stringWithUTF8String:propertyName]);
}
//獲取方法列表
Method *methodList = class_copyMethodList([self class], &count);
for (unsigned int i; i<count; i++) {
Method method = methodList[i];
NSLog(@"method---->%@", NSStringFromSelector(method_getName(method)));
}
//獲取成員變量列表
Ivar *ivarList = class_copyIvarList([self class], &count);
for (unsigned int i; i<count; i++) {
Ivar myIvar = ivarList[i];
const char *ivarName = ivar_getName(myIvar);
NSLog(@"Ivar---->%@", [NSString stringWithUTF8String:ivarName]);
}
//獲取協(xié)議列表
__unsafe_unretained Protocol **protocolList = class_copyProtocolList([self class], &count);
for (unsigned int i; i<count; i++) {
Protocol *myProtocal = protocolList[i];
const char *protocolName = protocol_getName(myProtocal);
NSLog(@"protocol---->%@", [NSString stringWithUTF8String:protocolName]);
}
五.方法調(diào)用
方法調(diào)用在運(yùn)行時的過程:
如果調(diào)用的是類方法,就會到類對象的isa指針指向的對象(也就是元類對象)中操作奋献。
比如:NSString *tmpStr = [NSString string];這里的 [NSString string]調(diào)用的是NSString的類方法健霹,就會到NSString類對象的isa指針指向的對象NSObject元類中操作旺上。-
如果用實(shí)例對象調(diào)用實(shí)例方法,會到實(shí)例的isa指針指向的對象(也就是類對象)操作糖埋。
比如: BOOL isContain = [tmpStr containsString:@"1"];這里的 [tmpStr containsString:@"1"],就是實(shí)例對象tmpStr調(diào)用containsString這個實(shí)例方法宣吱,得到實(shí)例tmpStr的isa指針指向的對象也就是類對象NSString操作。NSString *tmpStr = [NSString string]; BOOL isContain = [tmpStr containsString:@"1"];
1. 首先阶捆,在相應(yīng)操作對象中的緩存列表中查找調(diào)用的方法凌节,如果找到,轉(zhuǎn)向相應(yīng)的實(shí)現(xiàn)并執(zhí)行.(即先在tmpStr這個對象的緩存列表中查找是否有containsString這個方法洒试,如果有,則轉(zhuǎn)向相應(yīng)的實(shí)現(xiàn)函數(shù)朴上,并執(zhí)行)垒棋;
2.如果沒有找到,在相應(yīng)操作對象的方法中找調(diào)用的方法痪宰,如果找到叼架,轉(zhuǎn)向相應(yīng)的實(shí)現(xiàn)并執(zhí)行。(即tmpStr的緩存列表找沒有找到containsString這個方法衣撬,就到tmpStr的方法列表里面查找乖订,如果找到,則轉(zhuǎn)向相應(yīng)的實(shí)現(xiàn)函數(shù)具练,并執(zhí)行)乍构;
3.如果沒找到,去父類指針?biāo)赶虻膶ο笾袌?zhí)行1扛点,2(即如果在tmpStr的方法列表里面沒有找到containsString這個方法哥遮,則轉(zhuǎn)向tmpStr的父類,也就是NSObject類去查找該方法)
4.以此類推陵究,如果一直到根類都還沒找到眠饮,轉(zhuǎn)向攔截調(diào)用(即如果在父類NSObject里面也沒有找到containsString這個方法,就往上一層父類再去查找铜邮,知道最頂層(根層)父類仪召,因?yàn)镹SObject在OC中是根層父類,所以如果在NSObjec的方法列表找沒找到containsString松蒜,就轉(zhuǎn)向攔截調(diào)用)
5.如果沒有重寫攔截調(diào)用的方法扔茅,程序報(bào)錯。(即在tmpStr及其父類的方法列表中都沒有containsString這個方法牍鞠,就轉(zhuǎn)向攔截調(diào)用咖摹,但是卻沒有實(shí)現(xiàn)攔截調(diào)用的方法,系統(tǒng)就報(bào)錯)
所以:
重寫父類的方法难述,并沒有覆蓋父類的方法萤晴,只是在當(dāng)前類對象中找到了這個方法后吐句,就不會再去父類中尋找了。
-
如果子類重寫父類的方法店读,但也想調(diào)用父類方法的實(shí)現(xiàn)嗦枢,只需使用super這個編譯器標(biāo)識,它會在運(yùn)行時先去調(diào)用父類的方法屯断,在執(zhí)行之類的方法文虏。
比如我們很常見的viewWillAppear函數(shù):- (void)viewWillAppear:(BOOL)animated { [super viewWillAppear:animated]; [self.navigationController setNavigationBarHidden:YES animated:animated]; }
這里的super標(biāo)識,在運(yùn)行時會先去執(zhí)行父類的viewWillAppear方法殖演,執(zhí)行完畢之后氧秘,在回來接著執(zhí)行原函數(shù)里面的隱藏導(dǎo)航欄的操作,如果沒有 [super viewWillAppear:animated];就不會去調(diào)用父類的該方法趴久,直接執(zhí)行隱藏導(dǎo)航欄的操作丸相。
六.攔截調(diào)用
攔截調(diào)用就是在找不到調(diào)用方法程序崩潰之前,你有機(jī)會通過重寫NSObject的四個方法來處理彼棍,防止崩潰發(fā)生灭忠。
+ (BOOL)resolveClassMethod:(SEL)sel;
該方法就是當(dāng)你調(diào)用一個不存在的類方法的時候,會調(diào)用該方法座硕,默認(rèn)返回NO,你可以加上自己的處理然后返回YES.
+ (BOOL)resolveInstanceMethod:(SEL)sel;
這個方法和上一個方法相似弛作,處理的是實(shí)例方法。(備注: NSString *tmpStr = [NSString string];像 [NSString string]這里的string就是類方法华匾, [tmpStr containsString:@"1"]這里的containsString就是實(shí)例方法映琳。)
- (id)forwardingTargetForSelector:(SEL)aSelector;
該方法將你調(diào)用的不存在的方法重定向到一個聲明了這個方法的類,只需要你返回一個有這個方法的target.
- (void)forwardInvocation:(NSInvocation *)anInvocation;
該方法將你調(diào)用的不存在的方法打包成NSInvocation傳給你瘦真。做完你自己的處理后刊头,調(diào)用inovkeWithTarget:方法讓某個target觸發(fā)這個方法。
七.動態(tài)添加方法
重寫了攔截調(diào)用的方法并且返回YES,接下來可以根據(jù)傳入的SEL類型的selector诸尽,動態(tài)添加一個方法原杂。
首先從外部隱式調(diào)用一個不存在的方法:
// 隱式調(diào)用方法
[target performSelector:@selector(resolveAdd:) withObject:@"test"];
然后,在target對象內(nèi)部重寫攔截調(diào)用的方法您机,動態(tài)添加方法
void runAddMethod(id self, SEL _cmd, NSString *string){
NSLog(@"add C IMP ", string);
}
+ (BOOL)resolveInstanceMethod:(SEL)sel{
//給本類動態(tài)添加一個方法
if ([NSStringFromSelector(sel) isEqualToString:@"resolveAdd:"]) {
class_addMethod(self, sel, (IMP)runAddMethod, "v@:*");
}
return YES;
}
其中class_addMethod的四個參數(shù)分別是:
- Class cls 給哪個類添加方法穿肄,本例中是self
- SEL name 添加的方法繁成,本例中是重寫攔截調(diào)用傳進(jìn)來的selector
- IMP imp 方法的實(shí)現(xiàn)颠锉,C方法的實(shí)現(xiàn)可以直接獲得。如果是OC方法慎陵,可以用+(IMP)instanceMethodForSelector:(SEL)aSelector;獲得方法的實(shí)現(xiàn)仲闽。
- "v@:*"方法的簽名脑溢,代表有一個參數(shù)的方法
八.關(guān)聯(lián)對象
比如現(xiàn)在你準(zhǔn)備用一個系統(tǒng)的類,但是系統(tǒng)的類并不能滿足你的需求,你需要額外添加一個屬性屑彻。
這種情況的一般解決辦法就是繼承验庙。
但是,只增加一個屬性社牲,就去繼承一個類粪薛,總結(jié)太麻煩,這時候runtime的關(guān)聯(lián)屬性就發(fā)揮它的作用了搏恤。
1.首先定義一個全局變量违寿,用它的地址作為關(guān)聯(lián)對象的key
static char kAssociatedObjectKey;
2.在NSObject+AssociatedObject.h里面添加新的屬性
NSObject+AssociatedObject.h
@interface NSObject (AssociatedObject)
@property (nonatomic, strong) id associatedObject;
@end
3.在NSObject+AssociatedObject.m里面添加設(shè)置和獲取方法
//設(shè)置關(guān)聯(lián)對象
@implementation NSObject (AssociatedObject)
@dynamic associatedObject;
- (void)setAssociatedObject:(id)object {
objc_setAssociatedObject(self, @selector(associatedObject), object, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
}
//獲取關(guān)聯(lián)對象
- (id)associatedObject {
return objc_getAssociatedObject(self, @selector(associatedObject));
}
objc_setAssociatedObject的四個參數(shù):
id object 給誰設(shè)置關(guān)聯(lián)對象
const void *key 關(guān)聯(lián)對象唯一的key,獲取時會用到
id value 關(guān)聯(lián)的對象
-
objc_AssociationPolicy 關(guān)聯(lián)策略,有以下幾種策略:
enum { OBJC_ASSOCIATION_ASSIGN = 0, // 給關(guān)聯(lián)對象指定弱引用 OBJC_ASSOCIATION_RETAIN_NONATOMIC = 1, // 給關(guān)聯(lián)的對象指定非原子操作哦 OBJC_ASSOCIATION_COPY_NONATOMIC = 3, // 給關(guān)聯(lián)對象指定費(fèi)原子的copy特性 OBJC_ASSOCIATION_RETAIN = 01401, // 給關(guān)聯(lián)的對象指定原子的強(qiáng)引用 OBJC_ASSOCIATION_COPY = 01403 // 給關(guān)聯(lián)的對象指定原子的copy 特性 };
objc_getAssociatedObject 的兩個參數(shù):
- id object 獲取誰的關(guān)聯(lián)對象
- const void *key 根據(jù)這個唯一的key獲取關(guān)聯(lián)對象。
其實(shí)熟空,你還可以把添加和獲取關(guān)聯(lián)對象的方法寫在你需要用到這個功能類的類別里面藤巢,方便調(diào)用。
//添加關(guān)聯(lián)對象
- (void)addAssociatedObject:(id)object{
objc_setAssociatedObject(self, @selector(getAssociatedObject), object, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
}
//獲取關(guān)聯(lián)對象
- (id)getAssociatedObject{
return objc_getAssociatedObject(self, _cmd);
}
- getAssociatedObject 方法的地址作為唯一的key息罗,
- _cmd代表當(dāng)前調(diào)用方法的地址菌瘪,也就是getAssociatedObject方法的地址
** 注意 :**
4.移除關(guān)聯(lián)對象:
objc_removeAssociatedObjects()這個函數(shù)很容易讓對象恢復(fù)成它"原始狀態(tài)",你不應(yīng)該使用它來移除關(guān)聯(lián)的對象阱当,因?yàn)樗矔瞥ㄆ渌胤郊尤氲娜筷P(guān)聯(lián)對象。所以你一般只需要通過調(diào)用objc_setAssociatedObject并傳入nil值類清除關(guān)聯(lián)值糜工。
優(yōu)秀樣例
添加私有屬性用于更好地去實(shí)現(xiàn)細(xì)節(jié)弊添。當(dāng)擴(kuò)展一個內(nèi)建類的行為時,保持附加屬性的狀態(tài)可能非常必要捌木。注意以下說的是一種非常教科書式的關(guān)聯(lián)對象的用例:AFNetworking在 UIImageView
的category上用了關(guān)聯(lián)對象來保持一個operation對象油坝,用于從網(wǎng)絡(luò)上某URL異步地獲取一張圖片。添加public屬性來增強(qiáng)category的功能刨裆。有些情況下這種(通過關(guān)聯(lián)對象)讓category行為更靈活的做法比在用一個帶變量的方法來實(shí)現(xiàn)更有意義澈圈。在這些情況下,可以用關(guān)聯(lián)對象實(shí)現(xiàn)一個一個對外開放的屬性帆啃∷才回到上個AFNetworking的例子中的 UIImageView
category,它的 imageResponseSerializer
方法允許圖片通過一個濾鏡來顯示努潘、或在緩存到硬盤之前改變圖片的內(nèi)容诽偷。創(chuàng)建一個用于KVO的關(guān)聯(lián)觀察者。當(dāng)在一個category的實(shí)現(xiàn)中使用KVO時疯坤,建議用一個自定義的關(guān)聯(lián)對象而不是該對象本身作觀察者报慕。
錯誤模式
在不必要的時候使用關(guān)聯(lián)對象。使用視圖時一個常見的情況是通過數(shù)據(jù)模型或一些復(fù)合的值來創(chuàng)建一個便利的方法設(shè)置填充字段或?qū)傩匝沟 H绻@些值在后面不會再被使用到眠冈,最好就不要使用關(guān)聯(lián)對象了。(比如你將自定義的UITableViewCell跟模型關(guān)聯(lián)起來菌瘫,但這個cell值用在一個ViewController里面蜗顽,也就是說這個關(guān)聯(lián)對象只用到一處布卡,之后就不再使用,這種情況下就沒必要使用關(guān)聯(lián)對象)诫舅。
使用關(guān)聯(lián)對象來保存一個可以被推算出來的值羽利。例如,有人可能想通過關(guān)聯(lián)對象存儲UITableViewCell上一個自定義accessoryView的引用刊懈,使用tableView:accessoryButtonTappedForRowWithIndexPath: 和 cellForRowAtIndexPath:即可以達(dá)到要求这弧。
使用關(guān)聯(lián)對象來代替X。其中X代表下面的一些項(xiàng):
子類化虚汛,當(dāng)使用繼承比使用組合更合適的時候匾浪。
Target-Action給響應(yīng)者添加交互事件。
手勢識別卷哩,當(dāng)target-action模式不夠用的時候蛋辈。
代理,當(dāng)事件可以委托給其他對象将谊。
消息 & 消息中心使用低耦合的方式來廣播消息冷溶。
九.方法交換
顧名思義:就是將兩個方法的實(shí)現(xiàn)交換,比如尊浓,將A方法和B方法交換逞频,調(diào)用A方法的時候,就回去執(zhí)行B方法中的代碼栋齿,反之亦然苗胀。
參考Mattt Thompson的[Method Swizzling]文章:
#import <objc/runtime.h>
@implementation UIViewController (Tracking)
+ (void)load {
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
Class class = [self class];
SEL originalSelector = @selector(viewWillAppear:);
SEL swizzledSelector = @selector(xxx_viewWillAppear:);
Method originalMethod = class_getInstanceMethod(class, originalSelector);
Method swizzledMethod = class_getInstanceMethod(class, swizzledSelector);
// When swizzling a class method, use the following:
// Class class = object_getClass((id)self);
// ...
// Method originalMethod = class_getClassMethod(class, originalSelector);
// Method swizzledMethod = class_getClassMethod(class, swizzledSelector);
BOOL didAddMethod =
class_addMethod(class,
originalSelector,
method_getImplementation(swizzledMethod),
method_getTypeEncoding(swizzledMethod));
if (didAddMethod) {
class_replaceMethod(class,
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
在自己定義的viewController中重寫viewWillAppear
- (void)viewWillAppear:(BOOL)animated {
[super viewWillAppear:animated];
NSLog(@"viewWillAppear");
}
就會調(diào)用xxx_viewWillAppear,輸出log, method swizzling在視圖控制器的生命周期、響應(yīng)事件瓦堵、繪制視圖或者Foundation框架的網(wǎng)絡(luò)棧等方法中需要插入代碼的時候基协,都是很好的解決方法。
+load vs +initialize:
swizzling應(yīng)該只在+load中完成菇用。 在 Objective-C 的運(yùn)行時中澜驮,每個類有兩個方法都會自動調(diào)用。
+load 是在一個類被初始裝載時調(diào)用(iOS應(yīng)用啟動的時候刨疼,就會加載所有的類泉唁,就會調(diào)用這個方法);并且因?yàn)榧虞d進(jìn)內(nèi)存只會加載一次揩慕,所以也一般也只會調(diào)用一次
+initialize 是在應(yīng)用第一次調(diào)用該類的類方法或?qū)嵗椒ㄇ罢{(diào)用的亭畜,調(diào)用次數(shù)根據(jù)子類和具體調(diào)用情況確定。
dispatch_once:
swizzling 應(yīng)該只在 dispatch_once 中完成迎卤。
由于 swizzling 改變了全局的狀態(tài)拴鸵,所以我們需要確保每個預(yù)防措施在運(yùn)行時都是可用的。原子操作就是這樣一個用于確保代碼只會被執(zhí)行一次的預(yù)防措施,就算是在不同的線程中也能確保代碼只執(zhí)行一次劲藐。Grand Central Dispatch 的 dispatch_once 滿足了所需要的需求八堡,并且應(yīng)該被當(dāng)做使用 swizzling 的初始化單例方法的標(biāo)準(zhǔn)。
Selectors, Methods, & Implementations
蘋果定義:
Selector(typedef struct objc_selector *SEL):在運(yùn)行時 Selectors 用來代表一個方法的名字聘芜。Selector 是一個在運(yùn)行時被注冊(或映射)的C類型字符串兄渺。Selector由編譯器產(chǎn)生并且在當(dāng)類被加載進(jìn)內(nèi)存時由運(yùn)行時自動進(jìn)行名字和實(shí)現(xiàn)的映射。
Method(typedef struct objc_method *Method):方法是一個不透明的用來代表一個方法的定義的類型汰现。
Implementation(typedef id (*IMP)(id, SEL,...)):這個數(shù)據(jù)類型指向一個方法的實(shí)現(xiàn)的最開始的地方挂谍。該方法為當(dāng)前CPU架構(gòu)使用標(biāo)準(zhǔn)的C方法調(diào)用來實(shí)現(xiàn)。該方法的第一個參數(shù)指向調(diào)用方法的自身(即內(nèi)存中類的實(shí)例對象瞎饲,若是調(diào)用類方法口叙,該指針則是指向元類對象metaclass)。第二個參數(shù)是這個方法的名字selector嗅战,該方法的真正參數(shù)緊隨其后妄田。
三者之間的關(guān)系:
在運(yùn)行時,類(Class)維護(hù)了一個消息分發(fā)列表來解決消息的正確發(fā)送驮捍。每一個消息列表的入口是一個方法(Method)疟呐,這個方法映射了一對鍵值對,其中鍵值是這個方法的名字 selector(SEL)东且,值是指向這個方法實(shí)現(xiàn)的函數(shù)指針 implementation(IMP)萨醒。 Method swizzling 修改了類的消息分發(fā)列表使得已經(jīng)存在的 selector 映射了另一個實(shí)現(xiàn) implementation,同時重命名了原生方法的實(shí)現(xiàn)為一個新的 selector苇倡。
也就是說swizzling只是交換兩個方法在函數(shù)表中的指向地址而已。
調(diào)用 _cmd
- (void)xxx_viewWillAppear:(BOOL)animated {
[self xxx_viewWillAppear:animated];
NSLog(@"viewWillAppear: %@", NSStringFromClass([self class]));
}
初看這段代碼囤踩,我們都會覺得會出現(xiàn)遞歸死循環(huán)旨椒。但事實(shí)不是這樣的:
method swizzling 在交換方法的實(shí)現(xiàn)后,xxx_viewWillAppear:方法的實(shí)現(xiàn)已經(jīng)被替換為UIViewController 的-viewWillAppear:這個原生方法堵漱。
所以當(dāng)我們在UIViewController調(diào)用這個- (void)viewWillAppear:(BOOL)animated 方法的時候综慎,實(shí)際上調(diào)用的是xxx_viewWillAppear這個方法,而 [self xxx_viewWillAppear:animated];這個方法實(shí)際上調(diào)用的是系統(tǒng)的viewWillAppear勤庐。
這就證實(shí)了swizzling只是交換兩個方法在函數(shù)表中的指向地址而已示惊。
常見坑
Method swizzling 是非原子性的,在多線程環(huán)境下可能被多次修改愉镰,但同樣 Method swizzling 又是全局性的米罚,就會造成不可預(yù)知的錯誤。
可能出現(xiàn)命名沖突的問題丈探,這樣就不會調(diào)用到系統(tǒng)原方法录择,可能導(dǎo)致未知問題。
Method swizzling 看起來像遞歸,對新人來說不容易理解隘竭。
出現(xiàn)問題 Method swizzling 不容易進(jìn)行debug塘秦,來發(fā)現(xiàn)問題
隨著項(xiàng)目迭代和人員更換,使用Method swizzling 的項(xiàng)目不容易維護(hù)动看,因?yàn)殚_發(fā)人員有時根本不知道在Method swizzling 里面修改了東西尊剔。
預(yù)防措施
- 在交換方法實(shí)現(xiàn)后記得要調(diào)用原生方法的實(shí)現(xiàn)(除非你非常確定可以不用調(diào)用原生方法的實(shí)現(xiàn)):APIs 提供了輸入輸出的規(guī)則,而在輸入輸出中間的方法實(shí)現(xiàn)就是一個看不見的黑盒菱皆。交換了方法實(shí)現(xiàn)并且一些回調(diào)方法不會調(diào)用原生方法的實(shí)現(xiàn)這可能會造成底層實(shí)現(xiàn)的崩潰须误。
- 避免沖突:為分類的方法加前綴,一定要確保調(diào)用了原生方法的所有地方不會因?yàn)槟憬粨Q了方法的實(shí)現(xiàn)而出現(xiàn)意想不到的結(jié)果搔预。
- 理解實(shí)現(xiàn)原理: 只是簡單的拷貝粘貼交換方法實(shí)現(xiàn)的代碼而不去理解實(shí)現(xiàn)原理不僅會讓 App 很脆弱霹期,并且浪費(fèi)了學(xué)習(xí) Objective-C 運(yùn)行時的機(jī)會。閱讀 Objective-C Runtime Reference 并且瀏覽 能夠讓你更好理解實(shí)現(xiàn)原理拯田。
- 持續(xù)的預(yù)防: 不管你對你理解 swlzzling 框架历造,UIKit 或者其他內(nèi)嵌框架有多自信,一定要記住所有東西在下一個發(fā)行版本都可能變得不再好使船庇。做好準(zhǔn)備吭产,在使用這個黑魔法中走得更遠(yuǎn),不要讓程序反而出現(xiàn)不可思議的行為鸭轮。
十.感想
runtime是把雙刃劍臣淤,因?yàn)樗械拇a都運(yùn)行在它之上,改變它窃爷,可能會改變代碼的正常運(yùn)行邏輯和所有與之交互的東西邑蒋,因此會產(chǎn)生可怕的副作用。但同時它強(qiáng)大的功能也可以給應(yīng)用的框架或者代碼的編寫帶來非常大的便利按厘。
因此医吊,對于runtime唯一的建議就是,需謹(jǐn)慎使用逮京,一旦使用卿堂,必須先了解runtime的相關(guān)原理,做好預(yù)防措施懒棉,在添加完自己的代碼之后草描,一定要調(diào)用系統(tǒng)原來的方法。
十一.最后:
送上一張喜歡的圖片:
提醒:不應(yīng)該把runtime的使用看成是高大上的東西策严,并以使用這個為榮穗慕,實(shí)際開發(fā)中runtime能少用應(yīng)該少用,正常的系統(tǒng)方法才是正道妻导!
這是一篇總結(jié)筆記揍诽,大家有興趣可以蠻看一下诀蓉,如果覺得不錯,麻煩給個喜歡或star,若發(fā)現(xiàn)有錯誤的地方請及時反饋暑脆,謝謝渠啤!