
前言
到了今天終于要"出院"了,要總結(jié)一下住院幾天的收獲,談?wù)凴untime到底能為我們開發(fā)帶來些什么好處。當(dāng)然它也是把雙刃劍,使用不當(dāng)?shù)脑挘矔?huì)成為開發(fā)路上的一個(gè)大坑。
目錄
- 1.Runtime的優(yōu)點(diǎn)
- (1) 實(shí)現(xiàn)多繼承Multiple Inheritance
- (2) Method Swizzling
- (3) Aspect Oriented Programming
- (4) Isa Swizzling
- (5) Associated Object關(guān)聯(lián)對(duì)象
- (6) 動(dòng)態(tài)的增加方法
- (7) NSCoding的自動(dòng)歸檔和自動(dòng)解檔
- (8) 字典和模型互相轉(zhuǎn)換
- 2.Runtime的缺點(diǎn)
一. 實(shí)現(xiàn)多繼承Multiple Inheritance
在上一篇文章里面講到的forwardingTargetForSelector:方法就能知道,一個(gè)類可以做到繼承多個(gè)類的效果,只需要在這一步將消息轉(zhuǎn)發(fā)給正確的類對(duì)象就可以模擬多繼承的效果。
在官方文檔上記錄了這樣一段例子。

在OC程序中可以借用消息轉(zhuǎn)發(fā)機(jī)制來實(shí)現(xiàn)多繼承的功能。 在上圖中,一個(gè)對(duì)象對(duì)一個(gè)消息做出回應(yīng),類似于另一個(gè)對(duì)象中的方法借過來或是“繼承”過來一樣。 在圖中,warrior實(shí)例轉(zhuǎn)發(fā)了一個(gè)negotiate消息到Diplomat實(shí)例中,執(zhí)行Diplomat中的negotiate方法,結(jié)果看起來像是warrior實(shí)例執(zhí)行了一個(gè)和Diplomat實(shí)例一樣的negotiate方法,其實(shí)執(zhí)行者還是Diplomat實(shí)例。
這使得不同繼承體系分支下的兩個(gè)類可以“繼承”對(duì)方的方法,這樣一個(gè)類可以響應(yīng)自己繼承分支里面的方法,同時(shí)也能響應(yīng)其他不相干類發(fā)過來的消息。在上圖中Warrior和Diplomat沒有繼承關(guān)系,但是Warrior將negotiate消息轉(zhuǎn)發(fā)給了Diplomat后,就好似Diplomat是Warrior的超類一樣。
消息轉(zhuǎn)發(fā)提供了許多類似于多繼承的特性,但是他們之間有一個(gè)很大的不同:
多繼承:合并了不同的行為特征在一個(gè)單獨(dú)的對(duì)象中,會(huì)得到一個(gè)重量級(jí)多層面的對(duì)象。
消息轉(zhuǎn)發(fā):將各個(gè)功能分散到不同的對(duì)象中,得到的一些輕量級(jí)的對(duì)象,這些對(duì)象通過消息通過消息轉(zhuǎn)發(fā)聯(lián)合起來。
這里值得說明的一點(diǎn)是,即使我們利用轉(zhuǎn)發(fā)消息來實(shí)現(xiàn)了“假”繼承,但是NSObject類還是會(huì)將兩者區(qū)分開。像respondsToSelector:和 isKindOfClass:這類方法只會(huì)考慮繼承體系,不會(huì)考慮轉(zhuǎn)發(fā)鏈。比如上圖中一個(gè)Warrior對(duì)象如果被問到是否能響應(yīng)negotiate消息:
if ( [aWarrior respondsToSelector:@selector(negotiate)] )
結(jié)果是NO,雖然它能夠響應(yīng)negotiate消息而不報(bào)錯(cuò),但是它是靠轉(zhuǎn)發(fā)消息給Diplomat類來響應(yīng)消息的。
如果非要制造假象,反應(yīng)出這種“假”的繼承關(guān)系,那么需要重新實(shí)現(xiàn) respondsToSelector:和 isKindOfClass:來加入你的轉(zhuǎn)發(fā)算法:
- (BOOL)respondsToSelector:(SEL)aSelector
{
if ( [super respondsToSelector:aSelector] )
return YES;
else {
/* Here, test whether the aSelector message can *
* be forwarded to another object and whether that *
* object can respond to it. Return YES if it can. */
}
return NO;
}
除了respondsToSelector:和 isKindOfClass:之外,instancesRespondToSelector:中也應(yīng)該寫一份轉(zhuǎn)發(fā)算法。如果使用了協(xié)議,conformsToProtocol:也一樣需要重寫。類似地,如果一個(gè)對(duì)象轉(zhuǎn)發(fā)它接受的任何遠(yuǎn)程消息,它得給出一個(gè)methodSignatureForSelector:來返回準(zhǔn)確的方法描述,這個(gè)方法會(huì)最終響應(yīng)被轉(zhuǎn)發(fā)的消息。比如一個(gè)對(duì)象能給它的替代者對(duì)象轉(zhuǎn)發(fā)消息,它需要像下面這樣實(shí)現(xiàn)methodSignatureForSelector:
- (NSMethodSignature*)methodSignatureForSelector:(SEL)selector
{
NSMethodSignature* signature = [super methodSignatureForSelector:selector];
if (!signature) {
signature = [surrogate methodSignatureForSelector:selector];
}
return signature;
}
Note: This is an advanced technique, suitable only for situations where no other solution is possible. It is not intended as a replacement for inheritance. If you must make use of this technique, make sure you fully understand the behavior of the class doing the forwarding and the class you’re forwarding to.
需要引起注意的一點(diǎn),實(shí)現(xiàn)methodSignatureForSelector方法是一種先進(jìn)的技術(shù),只適用于沒有其他解決方案的情況下。它不會(huì)作為繼承的替代。如果您必須使用這種技術(shù),請(qǐng)確保您完全理解類做的轉(zhuǎn)發(fā)和您轉(zhuǎn)發(fā)的類的行為。請(qǐng)勿濫用!
二.Method Swizzling

提到Objective-C 中的 Runtime,大多數(shù)人第一個(gè)想到的可能就是黑魔法Method Swizzling。畢竟這是Runtime里面很強(qiáng)大的一部分,它可以通過Runtime的API實(shí)現(xiàn)更改任意的方法,理論上可以在運(yùn)行時(shí)通過類名/方法名hook到任何 OC 方法,替換任何類的實(shí)現(xiàn)以及新增任意類。
舉的最多的例子應(yīng)該就是埋點(diǎn)統(tǒng)計(jì)用戶信息的例子。
假設(shè)我們需要在頁面上不同的地方統(tǒng)計(jì)用戶信息,常見做法有兩種:
- 傻瓜式的在所有需要統(tǒng)計(jì)的頁面都加上代碼。這樣做簡單,但是重復(fù)的代碼太多。
- 把統(tǒng)計(jì)的代碼寫入基類中,比如說BaseViewController。這樣雖然代碼只需要寫一次,但是UITableViewController,UICollectionViewcontroller都需要寫一遍,這樣重復(fù)的代碼依舊不少。
基于這兩點(diǎn),我們這時(shí)候選用Method Swizzling來解決這個(gè)事情最優(yōu)雅。
1. Method Swizzling原理
Method Swizzing是發(fā)生在運(yùn)行時(shí)的,主要用于在運(yùn)行時(shí)將兩個(gè)Method進(jìn)行交換,我們可以將Method Swizzling代碼寫到任何地方,但是只有在這段Method Swilzzling代碼執(zhí)行完畢之后互換才起作用。而且Method Swizzling也是iOS中AOP(面相切面編程)的一種實(shí)現(xiàn)方式,我們可以利用蘋果這一特性來實(shí)現(xiàn)AOP編程。
Method Swizzling本質(zhì)上就是對(duì)IMP和SEL進(jìn)行交換。
2.Method Swizzling使用
一般我們使用都是新建一個(gè)分類,在分類中進(jìn)行Method Swizzling方法的交換。交換的代碼模板如下:
#import <objc/runtime.h>
@implementation UIViewController (Swizzling)
+ (void)load {
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
Class class = [self class];
// When swizzling a class method, use the following:
// Class class = object_getClass((id)self);
SEL originalSelector = @selector(viewWillAppear:);
SEL swizzledSelector = @selector(xxx_viewWillAppear:);
Method originalMethod = class_getInstanceMethod(class, originalSelector);
Method swizzledMethod = class_getInstanceMethod(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
Method Swizzling可以在運(yùn)行時(shí)通過修改類的方法列表中selector對(duì)應(yīng)的函數(shù)或者設(shè)置交換方法實(shí)現(xiàn),來動(dòng)態(tài)修改方法。可以重寫某個(gè)方法而不用繼承,同時(shí)還可以調(diào)用原先的實(shí)現(xiàn)。所以通常應(yīng)用于在category中添加一個(gè)方法。
3.Method Swizzling注意點(diǎn)

1.Swizzling應(yīng)該總在+load中執(zhí)行
Objective-C在運(yùn)行時(shí)會(huì)自動(dòng)調(diào)用類的兩個(gè)方法+load和+initialize。+load會(huì)在類初始加載時(shí)調(diào)用, +initialize方法是以懶加載的方式被調(diào)用的,如果程序一直沒有給某個(gè)類或它的子類發(fā)送消息,那么這個(gè)類的 +initialize方法是永遠(yuǎn)不會(huì)被調(diào)用的。所以Swizzling要是寫在+initialize方法中,是有可能永遠(yuǎn)都不被執(zhí)行。
和+initialize比較+load能保證在類的初始化過程中被加載。
關(guān)于+load和+initialize的比較可以參看這篇文章《Objective-C +load vs +initialize》
2.Swizzling應(yīng)該總是在dispatch_once中執(zhí)行
Swizzling會(huì)改變?nèi)譅顟B(tài),所以在運(yùn)行時(shí)采取一些預(yù)防措施,使用dispatch_once就能夠確保代碼不管有多少線程都只被執(zhí)行一次。這將成為Method Swizzling的最佳實(shí)踐。
這里有一個(gè)很容易犯的錯(cuò)誤,那就是繼承中用了Swizzling。如果不寫dispatch_once就會(huì)導(dǎo)致Swizzling失效!
舉個(gè)例子,比如同時(shí)對(duì)NSArray和NSMutableArray中的objectAtIndex:方法都進(jìn)行了Swizzling,這樣可能會(huì)導(dǎo)致NSArray中的Swizzling失效的。
可是為什么會(huì)這樣呢?
原因是,我們沒有用dispatch_once控制Swizzling只執(zhí)行一次。如果這段Swizzling被執(zhí)行多次,經(jīng)過多次的交換IMP和SEL之后,結(jié)果可能就是未交換之前的狀態(tài)。
比如說父類A的B方法和子類C的D方法進(jìn)行交換,交換一次后,父類A持有D方法的IMP,子類C持有B方法的IMP,但是再次交換一次,就又還原了。父類A還是持有B方法的IMP,子類C還是持有D方法的IMP,這樣就相當(dāng)于咩有交換??梢钥闯?,如果不寫dispatch_once,偶數(shù)次交換以后,相當(dāng)于沒有交換,Swizzling失效!
3.Swizzling在+load中執(zhí)行時(shí),不要調(diào)用[super load]
原因同注意點(diǎn)二,如果是多繼承,并且對(duì)同一個(gè)方法都進(jìn)行了Swizzling,那么調(diào)用[super load]以后,父類的Swizzling就失效了。
4.上述模板中沒有錯(cuò)誤
有些人懷疑我上述給的模板可能有錯(cuò)誤。在這里需要講解一下。
在進(jìn)行Swizzling的時(shí)候,我們需要用class_addMethod先進(jìn)行判斷一下原有類中是否有要替換的方法的實(shí)現(xiàn)。
如果class_addMethod返回NO,說明當(dāng)前類中有要替換方法的實(shí)現(xiàn),所以可以直接進(jìn)行替換,調(diào)用method_exchangeImplementations即可實(shí)現(xiàn)Swizzling。
如果class_addMethod返回YES,說明當(dāng)前類中沒有要替換方法的實(shí)現(xiàn),我們需要在父類中去尋找。這個(gè)時(shí)候就需要用到method_getImplementation去獲取class_getInstanceMethod里面的方法實(shí)現(xiàn)。然后再進(jìn)行class_replaceMethod來實(shí)現(xiàn)Swizzling。
這是Swizzling需要判斷的一點(diǎn)。
還有一點(diǎn)需要注意的是,在我們替換的方法- (void)xxx_viewWillAppear:(BOOL)animated中,調(diào)用了[self xxx_viewWillAppear:animated];這不是死循環(huán)了么?
其實(shí)這里并不會(huì)死循環(huán)。
由于我們進(jìn)行了Swizzling,所以其實(shí)在原來的- (void)viewWillAppear:(BOOL)animated方法中,調(diào)用的是- (void)xxx_viewWillAppear:(BOOL)animated方法的實(shí)現(xiàn)。所以不會(huì)造成死循環(huán)。相反的,如果這里把[self xxx_viewWillAppear:animated];改成[self viewWillAppear:animated];就會(huì)造成死循環(huán)。因?yàn)橥饷嬲{(diào)用[self viewWillAppear:animated];的時(shí)候,會(huì)交換方法走到[self xxx_viewWillAppear:animated];這個(gè)方法實(shí)現(xiàn)中來,然后這里又去調(diào)用[self viewWillAppear:animated],就會(huì)造成死循環(huán)了。
所以按照上述Swizzling的模板來寫,就不會(huì)遇到這4點(diǎn)需要注意的問題啦。
4.Method Swizzling使用場景
Method Swizzling使用場景其實(shí)有很多很多,在一些特殊的開發(fā)需求中適時(shí)的使用黑魔法,可以做法神來之筆的效果。這里就舉3種常見的場景。
1.實(shí)現(xiàn)AOP
AOP的例子在上一篇文章中舉了一個(gè)例子,在下一章中也打算詳細(xì)分析一下其實(shí)現(xiàn)原理,這里就一筆帶過。
2.實(shí)現(xiàn)埋點(diǎn)統(tǒng)計(jì)
如果app有埋點(diǎn)需求,并且要自己實(shí)現(xiàn)一套埋點(diǎn)邏輯,那么這里用到Swizzling是很合適的選擇。優(yōu)點(diǎn)在開頭已經(jīng)分析了,這里不再贅述??吹揭黄治龅耐实穆顸c(diǎn)的文章,推薦大家閱讀。
iOS動(dòng)態(tài)性(二)可復(fù)用而且高度解耦的用戶統(tǒng)計(jì)埋點(diǎn)實(shí)現(xiàn)
3.實(shí)現(xiàn)異常保護(hù)
日常開發(fā)我們經(jīng)常會(huì)遇到NSArray數(shù)組越界的情況,蘋果的API也沒有對(duì)異常保護(hù),所以需要我們開發(fā)者開發(fā)時(shí)候多多留意。關(guān)于Index有好多方法,objectAtIndex,removeObjectAtIndex,replaceObjectAtIndex,exchangeObjectAtIndex等等,這些設(shè)計(jì)到Index都需要判斷是否越界。
常見做法是給NSArray,NSMutableArray增加分類,增加這些異常保護(hù)的方法,不過如果原有工程里面已經(jīng)寫了大量的AtIndex系列的方法,去替換成新的分類的方法,效率會(huì)比較低。這里可以考慮用Swizzling做。
#import "NSArray+ Swizzling.h"
#import "objc/runtime.h"
@implementation NSArray (Swizzling)
+ (void)load {
Method fromMethod = class_getInstanceMethod(objc_getClass("__NSArrayI"), @selector(objectAtIndex:));
Method toMethod = class_getInstanceMethod(objc_getClass("__NSArrayI"), @selector(swizzling_objectAtIndex:));
method_exchangeImplementations(fromMethod, toMethod);
}
- (id)swizzling_objectAtIndex:(NSUInteger)index {
if (self.count-1 < index) {
// 異常處理
@try {
return [self swizzling_objectAtIndex:index];
}
@catch (NSException *exception) {
// 打印崩潰信息
NSLog(@"---------- %s Crash Because Method %s ----------\n", class_getName(self.class), __func__);
NSLog(@"%@", [exception callStackSymbols]);
return nil;
}
@finally {}
} else {
return [self swizzling_objectAtIndex:index];
}
}
@end
注意,調(diào)用這個(gè)objc_getClass方法的時(shí)候,要先知道類對(duì)應(yīng)的真實(shí)的類名才行,NSArray其實(shí)在Runtime中對(duì)應(yīng)著__NSArrayI,NSMutableArray對(duì)應(yīng)著__NSArrayM,NSDictionary對(duì)應(yīng)著__NSDictionaryI,NSMutableDictionary對(duì)應(yīng)著__NSDictionaryM。
三. Aspect Oriented Programming

Wikipedia 里對(duì) AOP 是這么介紹的:
An aspect can alter the behavior of the base code by applying advice (additional behavior) at various join points (points in a program) specified in a quantification or query called a pointcut (that detects whether a given join point matches).
類似記錄日志、身份驗(yàn)證、緩存等事務(wù)非?,嵥?,與業(yè)務(wù)邏輯無關(guān),很多地方都有,又很難抽象出一個(gè)模塊,這種程序設(shè)計(jì)問題,業(yè)界給它們起了一個(gè)名字叫橫向關(guān)注點(diǎn)(Cross-cutting concern),AOP作用就是分離橫向關(guān)注點(diǎn)(Cross-cutting concern)來提高模塊復(fù)用性,它可以在既有的代碼添加一些額外的行為(記錄日志、身份驗(yàn)證、緩存)而無需修改代碼。
接下來分析分析AOP的工作原理。
在上一篇中我們分析過了,在objc_msgSend函數(shù)查找IMP的過程中,如果在父類也沒有找到相應(yīng)的IMP,那么就會(huì)開始執(zhí)行_class_resolveMethod方法,如果不是元類,就執(zhí)行_class_resolveInstanceMethod,如果是元類,執(zhí)行_class_resolveClassMethod。在這個(gè)方法中,允許開發(fā)者動(dòng)態(tài)增加方法實(shí)現(xiàn)。這個(gè)階段一般是給@dynamic屬性變量提供動(dòng)態(tài)方法的。
如果_class_resolveMethod無法處理,會(huì)開始選擇備援接受者接受消息,這個(gè)時(shí)候就到了forwardingTargetForSelector方法。如果該方法返回非nil的對(duì)象,則使用該對(duì)象作為新的消息接收者。
- (id)forwardingTargetForSelector:(SEL)aSelector
{
if(aSelector == @selector(Method:)){
return otherObject;
}
return [super forwardingTargetForSelector:aSelector];
}
同樣也可以替換類方法
+ (id)forwardingTargetForSelector:(SEL)aSelector {
if(aSelector == @selector(xxx)) {
return NSClassFromString(@"Class name");
}
return [super forwardingTargetForSelector:aSelector];
}
替換類方法返回值就是一個(gè)類對(duì)象。
forwardingTargetForSelector這種方法屬于單純的轉(zhuǎn)發(fā),無法對(duì)消息的參數(shù)和返回值進(jìn)行處理。
最后到了完整轉(zhuǎn)發(fā)階段。
Runtime系統(tǒng)會(huì)向?qū)ο蟀l(fā)送methodSignatureForSelector:消息,并取到返回的方法簽名用于生成NSInvocation對(duì)象。為接下來的完整的消息轉(zhuǎn)發(fā)生成一個(gè) NSMethodSignature對(duì)象。NSMethodSignature 對(duì)象會(huì)被包裝成 NSInvocation 對(duì)象,forwardInvocation: 方法里就可以對(duì) NSInvocation 進(jìn)行處理了。
// 為目標(biāo)對(duì)象中被調(diào)用的方法返回一個(gè)NSMethodSignature實(shí)例
#warning 運(yùn)行時(shí)系統(tǒng)要求在執(zhí)行標(biāo)準(zhǔn)轉(zhuǎn)發(fā)時(shí)實(shí)現(xiàn)這個(gè)方法
- (NSMethodSignature *)methodSignatureForSelector:(SEL)sel{
return [self.proxyTarget methodSignatureForSelector:sel];
}
對(duì)象需要?jiǎng)?chuàng)建一個(gè)NSInvocation對(duì)象,把消息調(diào)用的全部細(xì)節(jié)封裝進(jìn)去,包括selector, target, arguments 等參數(shù),還能夠?qū)Ψ祷亟Y(jié)果進(jìn)行處理。
AOP的多數(shù)操作就是在forwardInvocation中完成的。一般會(huì)分為2個(gè)階段,一個(gè)是Intercepter注冊(cè)階段,一個(gè)是Intercepter執(zhí)行階段。
1. Intercepter注冊(cè)

首先會(huì)把類里面的某個(gè)要切片的方法的IMP加入到Aspect中,類方法里面如果有forwardingTargetForSelector:的IMP,也要加入到Aspect中。

然后對(duì)類的切片方法和forwardingTargetForSelector:的IMP進(jìn)行替換。兩者的IMP相應(yīng)的替換為objc_msgForward()方法和hook過的forwardingTargetForSelector:。這樣主要的Intercepter注冊(cè)就完成了。
2. Intercepter執(zhí)行

當(dāng)執(zhí)行func()方法的時(shí)候,會(huì)去查找它的IMP,現(xiàn)在它的IMP已經(jīng)被我們替換為了objc_msgForward()方法,于是開始查找備援轉(zhuǎn)發(fā)對(duì)象。
查找備援接受者調(diào)用forwardingTargetForSelector:這個(gè)方法,由于這里是被我們hook過的,所以IMP指向的是hook過的forwardingTargetForSelector:方法。這里我們會(huì)返回Aspect的target,即選取Aspect作為備援接受者。
有了備援接受者之后,就會(huì)重新objc_msgSend,從消息發(fā)送階段重頭開始。
objc_msgSend找不到指定的IMP,再進(jìn)行_class_resolveMethod,這里也沒有找到,forwardingTargetForSelector:這里也不做處理,接著就會(huì)methodSignatureForSelector。在methodSignatureForSelector方法中創(chuàng)建一個(gè)NSInvocation對(duì)象,傳遞給最終的forwardInvocation方法。
Aspect里面的forwardInvocation方法會(huì)干所有切面的事情。這里轉(zhuǎn)發(fā)邏輯就完全由我們自定義了。Intercepter注冊(cè)的時(shí)候我們也加入了原來方法中的method()和forwardingTargetForSelector:方法的IMP,這里我們可以在forwardInvocation方法中去執(zhí)行這些IMP。在執(zhí)行這些IMP的前后都可以任意的插入任何IMP以達(dá)到切面的目的。
以上就是AOP的原理。
四. Isa Swizzling
前面第二點(diǎn)談到了黑魔法Method Swizzling,本質(zhì)上就是對(duì)IMP和SEL進(jìn)行交換。其實(shí)接下來要說的Isa Swizzling,和它類似,本質(zhì)上也是交換,不過交換的是Isa。
在蘋果的官方庫里面有一個(gè)很有名的技術(shù)就用到了這個(gè)Isa Swizzling,那就是KVO——Key-Value Observing。
官方文檔上對(duì)于KVO的定義是這樣的:
Automatic key-value observing is implemented using a technique called isa-swizzling.
The isa pointer, as the name suggests, points to the object's class which maintains a dispatch table. This dispatch table essentially contains pointers to the methods the class implements, among other data.
When an observer is registered for an attribute of an object the isa pointer of the observed object is modified, pointing to an intermediate class rather than at the true class. As a result the value of the isa pointer does not necessarily reflect the actual class of the instance.
You should never rely on the isa pointer to determine class membership. Instead, you should use the class method to determine the class of an object instance.
官方給的就這么多,具體實(shí)現(xiàn)也沒有說的很清楚。那只能我們自己來實(shí)驗(yàn)一下。
KVO是為了監(jiān)聽一個(gè)對(duì)象的某個(gè)屬性值是否發(fā)生變化。在屬性值發(fā)生變化的時(shí)候,肯定會(huì)調(diào)用其setter方法。所以KVO的本質(zhì)就是監(jiān)聽對(duì)象有沒有調(diào)用被監(jiān)聽屬性對(duì)應(yīng)的setter方法。具體實(shí)現(xiàn)應(yīng)該是重寫其setter方法即可。
官方是如何優(yōu)雅的實(shí)現(xiàn)重寫監(jiān)聽類的setter方法的呢?實(shí)驗(yàn)代碼如下:
Student *stu = [[Student alloc]init];
[stu addObserver:self forKeyPath:@"name" options:NSKeyValueObservingOptionNew context:nil];
我們可以打印觀察isa指針的指向
Printing description of stu->isa:
Student
Printing description of stu->isa:
NSKVONotifying_Student
通過打印,我們可以很明顯的看到,被觀察的對(duì)象的isa變了,變成了NSKVONotifying_Student這個(gè)類了。
在@interface NSObject(NSKeyValueObserverRegistration) 這個(gè)分類里面,蘋果定義了KVO的方法。
- (void)addObserver:(NSObject *)observer forKeyPath:(NSString *)keyPath options:(NSKeyValueObservingOptions)options context:(nullable void *)context;
- (void)removeObserver:(NSObject *)observer forKeyPath:(NSString *)keyPath context:(nullable void *)context NS_AVAILABLE(10_7, 5_0);
- (void)removeObserver:(NSObject *)observer forKeyPath:(NSString *)keyPath;
KVO在調(diào)用addObserver方法之后,蘋果的做法是在執(zhí)行完addObserver: forKeyPath: options: context: 方法之后,把isa指向到另外一個(gè)類去。
在這個(gè)新類里面重寫被觀察的對(duì)象四個(gè)方法。class,setter,dealloc,_isKVOA。
1. 重寫class方法
重寫class方法是為了我們調(diào)用它的時(shí)候返回跟重寫繼承類之前同樣的內(nèi)容。
static NSArray * ClassMethodNames(Class c)
{
NSMutableArray * array = [NSMutableArray array];
unsigned int methodCount = 0;
Method * methodList = class_copyMethodList(c, &methodCount);
unsigned int i;
for(i = 0; i < methodCount; i++) {
[array addObject: NSStringFromSelector(method_getName(methodList[i]))];
}
free(methodList);
return array;
}
int main(int argc, char * argv[]) {
Student *stu = [[Student alloc]init];
NSLog(@"self->isa:%@",object_getClass(stu));
NSLog(@"self class:%@",[stu class]);
NSLog(@"ClassMethodNames = %@",ClassMethodNames(object_getClass(stu)));
[stu addObserver:self forKeyPath:@"name" options:NSKeyValueObservingOptionNew context:nil];
NSLog(@"self->isa:%@",object_getClass(stu));
NSLog(@"self class:%@",[stu class]);
NSLog(@"ClassMethodNames = %@",ClassMethodNames(object_getClass(stu)));
}
打印結(jié)果
self->isa:Student
self class:Student
ClassMethodNames = (
".cxx_destruct",
name,
"setName:"
)
self->isa:NSKVONotifying_Student
self class:Student
ClassMethodNames = (
"setName:",
class,
dealloc,
"_isKVOA"
)
這里也可以看出,這是object_getClass方法和class方法的區(qū)別。
這里要特別說明一下,為何打印 object_getClass 方法和 class 方法打印出來結(jié)果不同。
- (Class)class {
return object_getClass(self);
}
Class object_getClass(id obj)
{
if (obj) return obj->getIsa();
else return Nil;
}
從實(shí)現(xiàn)上看,兩個(gè)方法的實(shí)現(xiàn)都一樣的,按道理來說,打印結(jié)果應(yīng)該相同,可是為何在加了 KVO 以后會(huì)出現(xiàn)打印結(jié)果不同呢?
** 根本原因:對(duì)于KVO,底層交換了 NSKVONotifying_Student 的 class 方法,讓其返回 Student。**
打印這句話 object_getClass(stu) 的時(shí)候,isa 當(dāng)然是 NSKVONotifying_Student。
+ (BOOL)respondsToSelector:(SEL)sel {
if (!sel) return NO;
return class_respondsToSelector_inst(object_getClass(self), sel, self);
}
當(dāng)我們執(zhí)行 NSLog 的時(shí)候,會(huì)執(zhí)行上面這個(gè)方法,這個(gè)方法的 sel 是encodeWithOSLogCoder:options:maxLength:,這個(gè)時(shí)候,self
是 NSKVONotifying_Student,上面那個(gè) respondsToSelector 方法里面 return 的 object_getClass(self) 結(jié)果還是NSKVONotifying_Student。
打印 [stu class] 的時(shí)候,isa 當(dāng)然還是 NSKVONotifying_Student。當(dāng)執(zhí)行到 NSLog 的時(shí)候,+ (BOOL)respondsToSelector:(SEL)sel,又會(huì)執(zhí)行到這個(gè)方法,這個(gè)時(shí)候的 self 變成了 Student,這個(gè)時(shí)候 respondsToSelector 方法里面的 object_getClass(self) 輸出當(dāng)然就是 Student 了。
2. 重寫setter方法
在新的類中會(huì)重寫對(duì)應(yīng)的set方法,是為了在set方法中增加另外兩個(gè)方法的調(diào)用:
- (void)willChangeValueForKey:(NSString *)key
- (void)didChangeValueForKey:(NSString *)key
在didChangeValueForKey:方法再調(diào)用
- (void)observeValueForKeyPath:(NSString *)keyPath
ofObject:(id)object
change:(NSDictionary *)change
context:(void *)context
這里有幾種情況需要說明一下:
1)如果使用了KVC
如果有訪問器方法,則運(yùn)行時(shí)會(huì)在setter方法中調(diào)用will/didChangeValueForKey:方法;
如果沒用訪問器方法,運(yùn)行時(shí)會(huì)在setValue:forKey方法中調(diào)用will/didChangeValueForKey:方法。
所以這種情況下,KVO是奏效的。
2)有訪問器方法
運(yùn)行時(shí)會(huì)重寫訪問器方法調(diào)用will/didChangeValueForKey:方法。
因此,直接調(diào)用訪問器方法改變屬性值時(shí),KVO也能監(jiān)聽到。
3)直接調(diào)用will/didChangeValueForKey:方法。
綜上所述,只要setter中重寫will/didChangeValueForKey:方法就可以使用KVO了。
3. 重寫dealloc方法
銷毀新生成的NSKVONotifying_類。
4. 重寫_isKVOA方法
這個(gè)私有方法估計(jì)可能是用來標(biāo)示該類是一個(gè) KVO 機(jī)制聲稱的類。
Foundation 到底為我們提供了哪些用于 KVO 的輔助函數(shù)。打開 terminal,使用 nm -a 命令查看 Foundation 中的信息:
nm -a /System/Library/Frameworks/Foundation.framework/Versions/C/Foundation
里面包含了以下這些KVO中可能用到的函數(shù):
00000000000233e7 t __NSSetDoubleValueAndNotify
00000000000f32ba t __NSSetFloatValueAndNotify
0000000000025025 t __NSSetIntValueAndNotify
000000000007fbb5 t __NSSetLongLongValueAndNotify
00000000000f33e8 t __NSSetLongValueAndNotify
000000000002d36c t __NSSetObjectValueAndNotify
0000000000024dc5 t __NSSetPointValueAndNotify
00000000000f39ba t __NSSetRangeValueAndNotify
00000000000f3aeb t __NSSetRectValueAndNotify
00000000000f3512 t __NSSetShortValueAndNotify
00000000000f3c2f t __NSSetSizeValueAndNotify
00000000000f363b t __NSSetUnsignedCharValueAndNotify
000000000006e91f t __NSSetUnsignedIntValueAndNotify
0000000000034b5b t __NSSetUnsignedLongLongValueAndNotify
00000000000f3766 t __NSSetUnsignedLongValueAndNotify
00000000000f3890 t __NSSetUnsignedShortValueAndNotify
00000000000f3060 t __NSSetValueAndNotifyForKeyInIvar
00000000000f30d7 t __NSSetValueAndNotifyForUndefinedKey
Foundation 提供了大部分基礎(chǔ)數(shù)據(jù)類型的輔助函數(shù)(Objective C中的 Boolean 只是 unsigned char 的 typedef,所以包括了,但沒有 C++中的 bool),此外還包括一些常見的結(jié)構(gòu)體如 Point, Range, Rect, Size,這表明這些結(jié)構(gòu)體也可以用于自動(dòng)鍵值觀察,但要注意除此之外的結(jié)構(gòu)體就不能用于自動(dòng)鍵值觀察了。對(duì)于所有 Objective C 對(duì)象對(duì)應(yīng)的是 __NSSetObjectValueAndNotify 方法。
KVO即使是蘋果官方的實(shí)現(xiàn),也是有缺陷的,這里有一篇文章詳細(xì)了分析了KVO中的缺陷,主要問題在KVO的回調(diào)機(jī)制,不能傳一個(gè)selector或者block作為回調(diào),而必須重寫-addObserver:forKeyPath:options:context:方法所引發(fā)的一系列問題。而且只監(jiān)聽一兩個(gè)屬性值還好,如果監(jiān)聽的屬性多了, 或者監(jiān)聽了多個(gè)對(duì)象的屬性, 那有點(diǎn)麻煩,需要在方法里面寫很多的if-else的判斷。
最后,官方文檔上對(duì)于KVO的實(shí)現(xiàn)的最后,給出了需要我們注意的一點(diǎn)是,永遠(yuǎn)不要用用isa來判斷一個(gè)類的繼承關(guān)系,而是應(yīng)該用class方法來判斷類的實(shí)例。
五. Associated Object 關(guān)聯(lián)對(duì)象

Associated Objects是Objective-C 2.0中Runtime的特性之一。眾所周知,在 Category 中,我們無法添加@property,因?yàn)樘砑恿薂property之后并不會(huì)自動(dòng)幫我們生成實(shí)例變量以及存取方法。那么,我們現(xiàn)在就可以通過關(guān)聯(lián)對(duì)象來實(shí)現(xiàn)在 Category 中添加屬性的功能了。
1. 用法
借用這篇經(jīng)典文章Associated Objects里面的例子來說明一下用法。
// NSObject+AssociatedObject.h
@interface NSObject (AssociatedObject)
@property (nonatomic, strong) id associatedObject;
@end
// NSObject+AssociatedObject.m
@implementation NSObject (AssociatedObject)
@dynamic associatedObject;
- (void)setAssociatedObject:(id)object {
objc_setAssociatedObject(self, @selector(associatedObject), object, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
}
- (id)associatedObject {
return objc_getAssociatedObject(self, @selector(associatedObject));
}
這里涉及到了3個(gè)函數(shù):
OBJC_EXPORT void objc_setAssociatedObject(id object, const void *key, id value, objc_AssociationPolicy policy)
__OSX_AVAILABLE_STARTING(__MAC_10_6, __IPHONE_3_1);
OBJC_EXPORT id objc_getAssociatedObject(id object, const void *key)
__OSX_AVAILABLE_STARTING(__MAC_10_6, __IPHONE_3_1);
OBJC_EXPORT void objc_removeAssociatedObjects(id object)
__OSX_AVAILABLE_STARTING(__MAC_10_6, __IPHONE_3_1);
來說明一下這些參數(shù)的意義:
1.id object 設(shè)置關(guān)聯(lián)對(duì)象的實(shí)例對(duì)象
2.const void *key 區(qū)分不同的關(guān)聯(lián)對(duì)象的 key。這里會(huì)有3種寫法。
使用 &AssociatedObjectKey 作為key值
static char AssociatedObjectKey = "AssociatedKey";
使用AssociatedKey 作為key值
static const void *AssociatedKey = "AssociatedKey";
使用@selector
@selector(associatedKey)
3種方法都可以,不過推薦使用更加簡潔的第三種方式。
3.id value 關(guān)聯(lián)的對(duì)象
4.objc_AssociationPolicy policy 關(guān)聯(lián)對(duì)象的存儲(chǔ)策略,它是一個(gè)枚舉,與property的attribute 相對(duì)應(yīng)。
| Behavior | @property Equivalent | Description |
|---|---|---|
| OBJC_ASSOCIATION_ASSIGN | @property (assign) / @property (unsafe_unretained) | 弱引用關(guān)聯(lián)對(duì)象 |
| OBJC_ASSOCIATION_RETAIN_NONATOMIC | @property (nonatomic, strong) | 強(qiáng)引用關(guān)聯(lián)對(duì)象,且為非原子操 |
| OBJC_ASSOCIATION_COPY_NONATOMIC | @property (nonatomic, copy) | 復(fù)制關(guān)聯(lián)對(duì)象,且為非原子操作 |
| OBJC_ASSOCIATION_RETAIN | @property (atomic, strong) | 強(qiáng)引用關(guān)聯(lián)對(duì)象,且為原子操作 |
| OBJC_ASSOCIATION_COPY | @property (atomic, copy) | 復(fù)制關(guān)聯(lián)對(duì)象,且為原子操作 |
這里需要注意的是標(biāo)記成OBJC_ASSOCIATION_ASSIGN的關(guān)聯(lián)對(duì)象和
@property (weak) 是不一樣的,上面表格中等價(jià)定義寫的是 @property (unsafe_unretained),對(duì)象被銷毀時(shí),屬性值仍然還在。如果之后再次使用該對(duì)象就會(huì)導(dǎo)致程序閃退。所以我們?cè)谑褂肙BJC_ASSOCIATION_ASSIGN時(shí),要格外注意。
According to the Deallocation Timeline described in WWDC 2011, Session 322(~36:00), associated objects are erased surprisingly late in the object lifecycle, inobject_dispose(), which is invoked by NSObject -dealloc.
關(guān)于關(guān)聯(lián)對(duì)象還有一點(diǎn)需要說明的是objc_removeAssociatedObjects。這個(gè)方法是移除源對(duì)象中所有的關(guān)聯(lián)對(duì)象,并不是其中之一。所以其方法參數(shù)中也沒有傳入指定的key。要?jiǎng)h除指定的關(guān)聯(lián)對(duì)象,使用 objc_setAssociatedObject 方法將對(duì)應(yīng)的 key 設(shè)置成 nil 即可。
objc_setAssociatedObject(self, associatedKey, nil, OBJC_ASSOCIATION_COPY_NONATOMIC);
關(guān)聯(lián)對(duì)象3種使用場景
1.為現(xiàn)有的類添加私有變量
2.為現(xiàn)有的類添加公有屬性
3.為KVO創(chuàng)建一個(gè)關(guān)聯(lián)的觀察者。
2.源碼分析
(一) objc_setAssociatedObject方法
void _object_set_associative_reference(id object, void *key, id value, uintptr_t policy) {
// retain the new value (if any) outside the lock.
ObjcAssociation old_association(0, nil);
id new_value = value ? acquireValue(value, policy) : nil;
{
AssociationsManager manager;
AssociationsHashMap &associations(manager.associations());
disguised_ptr_t disguised_object = DISGUISE(object);
if (new_value) {
// break any existing association.
AssociationsHashMap::iterator i = associations.find(disguised_object);
if (i != associations.end()) {
// secondary table exists
ObjectAssociationMap *refs = i->second;
ObjectAssociationMap::iterator j = refs->find(key);
if (j != refs->end()) {
old_association = j->second;
j->second = ObjcAssociation(policy, new_value);
} else {
(*refs)[key] = ObjcAssociation(policy, new_value);
}
} else {
// create the new association (first time).
ObjectAssociationMap *refs = new ObjectAssociationMap;
associations[disguised_object] = refs;
(*refs)[key] = ObjcAssociation(policy, new_value);
object->setHasAssociatedObjects();
}
} else {
// setting the association to nil breaks the association.
AssociationsHashMap::iterator i = associations.find(disguised_object);
if (i != associations.end()) {
ObjectAssociationMap *refs = i->second;
ObjectAssociationMap::iterator j = refs->find(key);
if (j != refs->end()) {
old_association = j->second;
refs->erase(j);
}
}
}
}
// release the old value (outside of the lock).
if (old_association.hasValue()) ReleaseValue()(old_association);
}
這個(gè)函數(shù)里面主要分為2部分,一部分是if里面對(duì)應(yīng)的new_value不為nil的時(shí)候,另一部分是else里面對(duì)應(yīng)的new_value為nil的情況。
當(dāng)new_value不為nil的時(shí)候,查找時(shí)候,流程如下:

首先在AssociationsManager的結(jié)構(gòu)如下
class AssociationsManager {
static spinlock_t _lock;
static AssociationsHashMap *_map;
public:
AssociationsManager() { _lock.lock(); }
~AssociationsManager() { _lock.unlock(); }
AssociationsHashMap &associations() {
if (_map == NULL)
_map = new AssociationsHashMap();
return *_map;
}
};
在AssociationsManager中有一個(gè)spinlock類型的自旋鎖lock。保證每次只有一個(gè)線程對(duì)AssociationsManager進(jìn)行操作,保證線程安全。AssociationsHashMap對(duì)應(yīng)的是一張哈希表。
AssociationsHashMap哈希表里面key是disguised_ptr_t。
disguised_ptr_t disguised_object = DISGUISE(object);
通過調(diào)用DISGUISE( )方法獲取object地址的指針。拿到disguised_object后,通過這個(gè)key值,在AssociationsHashMap哈希表里面找到對(duì)應(yīng)的value值。而這個(gè)value值ObjcAssociationMap表的首地址。
在ObjcAssociationMap表中,key值是set方法里面?zhèn)鬟^來的形參const void *key,value值是ObjcAssociation對(duì)象。
ObjcAssociation對(duì)象中存儲(chǔ)了set方法最后兩個(gè)參數(shù),policy和value。
所以objc_setAssociatedObject方法中傳的4個(gè)形參在上圖中已經(jīng)標(biāo)出。
現(xiàn)在弄清楚結(jié)構(gòu)之后再來看源碼,就很容易了。objc_setAssociatedObject方法的目的就是在這2張哈希表中存儲(chǔ)對(duì)應(yīng)的鍵值對(duì)。
先初始化一個(gè) AssociationsManager,獲取唯一的保存關(guān)聯(lián)對(duì)象的哈希表 AssociationsHashMap,然后在AssociationsHashMap里面去查找object地址的指針。
如果找到,就找到了第二張表ObjectAssociationMap。在這張表里繼續(xù)查找object的key。
if (i != associations.end()) {
// secondary table exists
ObjectAssociationMap *refs = i->second;
ObjectAssociationMap::iterator j = refs->find(key);
if (j != refs->end()) {
old_association = j->second;
j->second = ObjcAssociation(policy, new_value);
} else {
(*refs)[key] = ObjcAssociation(policy, new_value);
}
}
如果在第二張表ObjectAssociationMap找到對(duì)應(yīng)的ObjcAssociation對(duì)象,那就更新它的值。如果沒有找到,就新建一個(gè)ObjcAssociation對(duì)象,放入第二張表ObjectAssociationMap中。
再回到第一張表AssociationsHashMap中,如果沒有找到對(duì)應(yīng)的鍵值
ObjectAssociationMap *refs = new ObjectAssociationMap;
associations[disguised_object] = refs;
(*refs)[key] = ObjcAssociation(policy, new_value);
object->setHasAssociatedObjects();
此時(shí)就不存在第二張表ObjectAssociationMap了,這時(shí)就需要新建第二張ObjectAssociationMap表,來維護(hù)對(duì)象的所有新增屬性。新建完第二張ObjectAssociationMap表之后,還需要再實(shí)例化 ObjcAssociation對(duì)象添加到 Map 中,調(diào)用setHasAssociatedObjects方法,表明當(dāng)前對(duì)象含有關(guān)聯(lián)對(duì)象。這里的setHasAssociatedObjects方法,改變的是isa_t結(jié)構(gòu)體中的第二個(gè)標(biāo)志位has_assoc的值。(關(guān)于isa_t結(jié)構(gòu)體的結(jié)構(gòu),詳情請(qǐng)看第一天的解析)
// release the old value (outside of the lock).
if (old_association.hasValue()) ReleaseValue()(old_association);
最后如果老的association對(duì)象有值,此時(shí)還會(huì)釋放它。
以上是new_value不為nil的情況。其實(shí)只要記住上面那2張表的結(jié)構(gòu),這個(gè)objc_setAssociatedObject的過程就是更新 / 新建 表中鍵值對(duì)的過程。
再來看看new_value為nil的情況
// setting the association to nil breaks the association.
AssociationsHashMap::iterator i = associations.find(disguised_object);
if (i != associations.end()) {
ObjectAssociationMap *refs = i->second;
ObjectAssociationMap::iterator j = refs->find(key);
if (j != refs->end()) {
old_association = j->second;
refs->erase(j);
}
}
當(dāng)new_value為nil的時(shí)候,就是我們要移除關(guān)聯(lián)對(duì)象的時(shí)候。這個(gè)時(shí)候就是在兩張表中找到對(duì)應(yīng)的鍵值,并調(diào)用erase( )方法,即可刪除對(duì)應(yīng)的關(guān)聯(lián)對(duì)象。
(二) objc_getAssociatedObject方法
id _object_get_associative_reference(id object, void *key) {
id value = nil;
uintptr_t policy = OBJC_ASSOCIATION_ASSIGN;
{
AssociationsManager manager;
AssociationsHashMap &associations(manager.associations());
disguised_ptr_t disguised_object = DISGUISE(object);
AssociationsHashMap::iterator i = associations.find(disguised_object);
if (i != associations.end()) {
ObjectAssociationMap *refs = i->second;
ObjectAssociationMap::iterator j = refs->find(key);
if (j != refs->end()) {
ObjcAssociation &entry = j->second;
value = entry.value();
policy = entry.policy();
if (policy & OBJC_ASSOCIATION_GETTER_RETAIN) ((id(*)(id, SEL))objc_msgSend)(value, SEL_retain);
}
}
}
if (value && (policy & OBJC_ASSOCIATION_GETTER_AUTORELEASE)) {
((id(*)(id, SEL))objc_msgSend)(value, SEL_autorelease);
}
return value;
}
objc_getAssociatedObject方法 很簡單。就是通過遍歷AssociationsHashMap哈希表 和 ObjcAssociationMap表的所有鍵值找到對(duì)應(yīng)的ObjcAssociation對(duì)象,找到了就返回ObjcAssociation對(duì)象,沒有找到就返回nil。
(三) objc_removeAssociatedObjects方法
void objc_removeAssociatedObjects(id object) {
if (object && object->hasAssociatedObjects()) {
_object_remove_assocations(object);
}
}
void _object_remove_assocations(id object) {
vector< ObjcAssociation,ObjcAllocator<ObjcAssociation> > elements;
{
AssociationsManager manager;
AssociationsHashMap &associations(manager.associations());
if (associations.size() == 0) return;
disguised_ptr_t disguised_object = DISGUISE(object);
AssociationsHashMap::iterator i = associations.find(disguised_object);
if (i != associations.end()) {
// copy all of the associations that need to be removed.
ObjectAssociationMap *refs = i->second;
for (ObjectAssociationMap::iterator j = refs->begin(), end = refs->end(); j != end; ++j) {
elements.push_back(j->second);
}
// remove the secondary table.
delete refs;
associations.erase(i);
}
}
// the calls to releaseValue() happen outside of the lock.
for_each(elements.begin(), elements.end(), ReleaseValue());
}
在移除關(guān)聯(lián)對(duì)象object的時(shí)候,會(huì)先去判斷object的isa_t中的第二位has_assoc的值,當(dāng)object 存在并且object->hasAssociatedObjects( )值為1的時(shí)候,才會(huì)去調(diào)用_object_remove_assocations方法。
_object_remove_assocations方法的目的是刪除第二張ObjcAssociationMap表,即刪除所有的關(guān)聯(lián)對(duì)象。刪除第二張表,就需要在第一張AssociationsHashMap表中遍歷查找。這里會(huì)把第二張ObjcAssociationMap表中所有的ObjcAssociation對(duì)象都存到一個(gè)數(shù)組elements里面,然后調(diào)用associations.erase( )刪除第二張表。最后再遍歷elements數(shù)組,把ObjcAssociation對(duì)象依次釋放。
以上就是Associated Object關(guān)聯(lián)對(duì)象3個(gè)函數(shù)的源碼分析。
六.動(dòng)態(tài)的增加方法
在消息發(fā)送階段,如果在父類中也沒有找到相應(yīng)的IMP,就會(huì)執(zhí)行resolveInstanceMethod方法。在這個(gè)方法里面,我們可以動(dòng)態(tài)的給類對(duì)象或者實(shí)例對(duì)象動(dòng)態(tài)的增加方法。
+ (BOOL)resolveInstanceMethod:(SEL)sel {
NSString *selectorString = NSStringFromSelector(sel);
if ([selectorString isEqualToString:@"method1"]) {
class_addMethod(self.class, @selector(method1), (IMP)functionForMethod1, "@:");
}
return [super resolveInstanceMethod:sel];
}
關(guān)于方法操作方面的函數(shù)還有以下這些
// 調(diào)用指定方法的實(shí)現(xiàn)
id method_invoke ( id receiver, Method m, ... );
// 調(diào)用返回一個(gè)數(shù)據(jù)結(jié)構(gòu)的方法的實(shí)現(xiàn)
void method_invoke_stret ( id receiver, Method m, ... );
// 獲取方法名
SEL method_getName ( Method m );
// 返回方法的實(shí)現(xiàn)
IMP method_getImplementation ( Method m );
// 獲取描述方法參數(shù)和返回值類型的字符串
const char * method_getTypeEncoding ( Method m );
// 獲取方法的返回值類型的字符串
char * method_copyReturnType ( Method m );
// 獲取方法的指定位置參數(shù)的類型字符串
char * method_copyArgumentType ( Method m, unsigned int index );
// 通過引用返回方法的返回值類型字符串
void method_getReturnType ( Method m, char *dst, size_t dst_len );
// 返回方法的參數(shù)的個(gè)數(shù)
unsigned int method_getNumberOfArguments ( Method m );
// 通過引用返回方法指定位置參數(shù)的類型字符串
void method_getArgumentType ( Method m, unsigned int index, char *dst, size_t dst_len );
// 返回指定方法的方法描述結(jié)構(gòu)體
struct objc_method_description * method_getDescription ( Method m );
// 設(shè)置方法的實(shí)現(xiàn)
IMP method_setImplementation ( Method m, IMP imp );
// 交換兩個(gè)方法的實(shí)現(xiàn)
void method_exchangeImplementations ( Method m1, Method m2 );
這些方法其實(shí)平時(shí)不需要死記硬背,使用的時(shí)候只要先打出method開頭,后面就會(huì)有補(bǔ)全信息,找到相應(yīng)的方法,傳入對(duì)應(yīng)的方法即可。
七.NSCoding的自動(dòng)歸檔和自動(dòng)解檔

現(xiàn)在雖然手寫歸檔和解檔的時(shí)候不多了,但是自動(dòng)操作還是用Runtime來實(shí)現(xiàn)的。
- (void)encodeWithCoder:(NSCoder *)aCoder{
[aCoder encodeObject:self.name forKey:@"name"];
}
- (id)initWithCoder:(NSCoder *)aDecoder{
if (self = [super init]) {
self.name = [aDecoder decodeObjectForKey:@"name"];
}
return self;
}
手動(dòng)的有一個(gè)缺陷,如果屬性多起來,要寫好多行相似的代碼,雖然功能是可以完美實(shí)現(xiàn),但是看上去不是很優(yōu)雅。
用runtime實(shí)現(xiàn)的思路就比較簡單,我們循環(huán)依次找到每個(gè)成員變量的名稱,然后利用KVC讀取和賦值就可以完成encodeWithCoder和initWithCoder了。
#import "Student.h"
#import <objc/runtime.h>
#import <objc/message.h>
@implementation Student
- (void)encodeWithCoder:(NSCoder *)aCoder{
unsigned int outCount = 0;
Ivar *vars = class_copyIvarList([self class], &outCount);
for (int i = 0; i < outCount; i ++) {
Ivar var = vars[i];
const char *name = ivar_getName(var);
NSString *key = [NSString stringWithUTF8String:name];
id value = [self valueForKey:key];
[aCoder encodeObject:value forKey:key];
}
}
- (nullable __kindof)initWithCoder:(NSCoder *)aDecoder{
if (self = [super init]) {
unsigned int outCount = 0;
Ivar *vars = class_copyIvarList([self class], &outCount);
for (int i = 0; i < outCount; i ++) {
Ivar var = vars[i];
const char *name = ivar_getName(var);
NSString *key = [NSString stringWithUTF8String:name];
id value = [aDecoder decodeObjectForKey:key];
[self setValue:value forKey:key];
}
}
return self;
}
@end
class_copyIvarList方法用來獲取當(dāng)前 Model 的所有成員變量,ivar_getName方法用來獲取每個(gè)成員變量的名稱。
八.字典和模型互相轉(zhuǎn)換
1.字典轉(zhuǎn)模型
1.調(diào)用 class_getProperty 方法獲取當(dāng)前 Model 的所有屬性。
2.調(diào)用 property_copyAttributeList 獲取屬性列表。
3.根據(jù)屬性名稱生成 setter 方法。
4.使用 objc_msgSend 調(diào)用 setter 方法為 Model 的屬性賦值(或者 KVC)
+(id)objectWithKeyValues:(NSDictionary *)aDictionary{
id objc = [[self alloc] init];
for (NSString *key in aDictionary.allKeys) {
id value = aDictionary[key];
/*判斷當(dāng)前屬性是不是Model*/
objc_property_t property = class_getProperty(self, key.UTF8String);
unsigned int outCount = 0;
objc_property_attribute_t *attributeList = property_copyAttributeList(property, &outCount);
objc_property_attribute_t attribute = attributeList[0];
NSString *typeString = [NSString stringWithUTF8String:attribute.value];
if ([typeString isEqualToString:@"@\"Student\""]) {
value = [self objectWithKeyValues:value];
}
//生成setter方法,并用objc_msgSend調(diào)用
NSString *methodName = [NSString stringWithFormat:@"set%@%@:",[key substringToIndex:1].uppercaseString,[key substringFromIndex:1]];
SEL setter = sel_registerName(methodName.UTF8String);
if ([objc respondsToSelector:setter]) {
((void (*) (id,SEL,id)) objc_msgSend) (objc,setter,value);
}
free(attributeList);
}
return objc;
}
這段代碼里面有一處判斷typeString的,這里判斷是防止model嵌套,比如說Student里面還有一層Student,那么這里就需要再次轉(zhuǎn)換一次,當(dāng)然這里有幾層就需要轉(zhuǎn)換幾次。
幾個(gè)出名的開源庫JSONModel、MJExtension等都是通過這種方式實(shí)現(xiàn)的(利用runtime的class_copyIvarList獲取屬性數(shù)組,遍歷模型對(duì)象的所有成員屬性,根據(jù)屬性名找到字典中key值進(jìn)行賦值,當(dāng)然這種方法只能解決NSString、NSNumber等,如果含有NSArray或NSDictionary,還要進(jìn)行第二步轉(zhuǎn)換,如果是字典數(shù)組,需要遍歷數(shù)組中的字典,利用objectWithDict方法將字典轉(zhuǎn)化為模型,在將模型放到數(shù)組中,最后把這個(gè)模型數(shù)組賦值給之前的字典數(shù)組)
2.模型轉(zhuǎn)字典
這里是上一部分字典轉(zhuǎn)模型的逆步驟:
1.調(diào)用 class_copyPropertyList 方法獲取當(dāng)前 Model 的所有屬性。
2.調(diào)用 property_getName 獲取屬性名稱。
3.根據(jù)屬性名稱生成 getter 方法。
4.使用 objc_msgSend 調(diào)用 getter 方法獲取屬性值(或者 KVC)
//模型轉(zhuǎn)字典
-(NSDictionary *)keyValuesWithObject{
unsigned int outCount = 0;
objc_property_t *propertyList = class_copyPropertyList([self class], &outCount);
NSMutableDictionary *dict = [NSMutableDictionary dictionary];
for (int i = 0; i < outCount; i ++) {
objc_property_t property = propertyList[i];
//生成getter方法,并用objc_msgSend調(diào)用
const char *propertyName = property_getName(property);
SEL getter = sel_registerName(propertyName);
if ([self respondsToSelector:getter]) {
id value = ((id (*) (id,SEL)) objc_msgSend) (self,getter);
/*判斷當(dāng)前屬性是不是Model*/
if ([value isKindOfClass:[self class]] && value) {
value = [value keyValuesWithObject];
}
if (value) {
NSString *key = [NSString stringWithUTF8String:propertyName];
[dict setObject:value forKey:key];
}
}
}
free(propertyList);
return dict;
}
中間注釋那里的判斷也是防止model嵌套,如果model里面還有一層model,那么model轉(zhuǎn)字典的時(shí)候還需要再次轉(zhuǎn)換,同樣,有幾層就需要轉(zhuǎn)換幾次。
不過上述的做法是假設(shè)字典里面不再包含二級(jí)字典,如果還包含數(shù)組,數(shù)組里面再包含字典,那還需要多級(jí)轉(zhuǎn)換。這里有一個(gè)關(guān)于字典里面包含數(shù)組的demo.
九.Runtime缺點(diǎn)

看了上面八大點(diǎn)之后,是不是感覺Runtime很神奇,可以迅速解決很多問題,然而,Runtime就像一把瑞士小刀,如果使用得當(dāng),它會(huì)有效地解決問題。但使用不當(dāng),將帶來很多麻煩。在stackoverflow上有人已經(jīng)提出這樣一個(gè)問題:What are the Dangers of Method Swizzling in Objective C?,它的危險(xiǎn)性主要體現(xiàn)以下幾個(gè)方面:
- Method swizzling is not atomic
Method swizzling不是原子性操作。如果在+load方法里面寫,是沒有問題的,但是如果寫在+initialize方法中就會(huì)出現(xiàn)一些奇怪的問題。
- Changes behavior of un-owned code
如果你在一個(gè)類中重寫一個(gè)方法,并且不調(diào)用super方法,你可能會(huì)導(dǎo)致一些問題出現(xiàn)。在大多數(shù)情況下,super方法是期望被調(diào)用的(除非有特殊說明)。如果你使用同樣的思想來進(jìn)行Swizzling,可能就會(huì)引起很多問題。如果你不調(diào)用原始的方法實(shí)現(xiàn),那么你Swizzling改變的太多了,而導(dǎo)致整個(gè)程序變得不安全。
- Possible naming conflicts
命名沖突是程序開發(fā)中經(jīng)常遇到的一個(gè)問題。我們經(jīng)常在類別中的前綴類名稱和方法名稱。不幸的是,命名沖突是在我們程序中的像一種瘟疫。一般我們會(huì)這樣寫Method Swizzling
@interface NSView : NSObject
- (void)setFrame:(NSRect)frame;
@end
@implementation NSView (MyViewAdditions)
- (void)my_setFrame:(NSRect)frame {
// do custom work
[self my_setFrame:frame];
}
+ (void)load {
[self swizzle:@selector(setFrame:) with:@selector(my_setFrame:)];
}
@end
這樣寫看上去是沒有問題的。但是如果在整個(gè)大型程序中還有另外一處定義了my_setFrame:方法呢?那又會(huì)造成命名沖突的問題。我們應(yīng)該把上面的Swizzling改成以下這種樣子:
@implementation NSView (MyViewAdditions)
static void MySetFrame(id self, SEL _cmd, NSRect frame);
static void (*SetFrameIMP)(id self, SEL _cmd, NSRect frame);
static void MySetFrame(id self, SEL _cmd, NSRect frame) {
// do custom work
SetFrameIMP(self, _cmd, frame);
}
+ (void)load {
[self swizzle:@selector(setFrame:) with:(IMP)MySetFrame store:(IMP *)&SetFrameIMP];
}
@end
雖然上面的代碼看上去不是OC(因?yàn)槭褂昧撕瘮?shù)指針),但是這種做法確實(shí)有效的防止了命名沖突的問題。原則上來說,其實(shí)上述做法更加符合標(biāo)準(zhǔn)化的Swizzling。這種做法可能和人們使用方法不同,但是這種做法更好。Swizzling Method 標(biāo)準(zhǔn)定義應(yīng)該是如下的樣子:
typedef IMP *IMPPointer;
BOOL class_swizzleMethodAndStore(Class class, SEL original, IMP replacement, IMPPointer store) {
IMP imp = NULL;
Method method = class_getInstanceMethod(class, original);
if (method) {
const char *type = method_getTypeEncoding(method);
imp = class_replaceMethod(class, original, replacement, type);
if (!imp) {
imp = method_getImplementation(method);
}
}
if (imp && store) { *store = imp; }
return (imp != NULL);
}
@implementation NSObject (FRRuntimeAdditions)
+ (BOOL)swizzle:(SEL)original with:(IMP)replacement store:(IMPPointer)store {
return class_swizzleMethodAndStore(self, original, replacement, store);
}
@end
- Swizzling changes the method's arguments
這一點(diǎn)是這些問題中最大的一個(gè)。標(biāo)準(zhǔn)的Method Swizzling是不會(huì)改變方法參數(shù)的。使用Swizzling中,會(huì)改變傳遞給原來的一個(gè)函數(shù)實(shí)現(xiàn)的參數(shù),例如:
[self my_setFrame:frame];
會(huì)變轉(zhuǎn)換成
objc_msgSend(self, @selector(my_setFrame:), frame);
objc_msgSend會(huì)去查找my_setFrame對(duì)應(yīng)的IMP。一旦IMP找到,會(huì)把相同的參數(shù)傳遞進(jìn)去。這里會(huì)找到最原始的setFrame:方法,調(diào)用執(zhí)行它。但是這里的_cmd參數(shù)并不是setFrame:,現(xiàn)在是my_setFrame:。原始的方法就被一個(gè)它不期待的接收參數(shù)調(diào)用了。這樣并不好。
這里有一個(gè)簡單的解決辦法,上一條里面所說的,用函數(shù)指針去實(shí)現(xiàn)。參數(shù)就不會(huì)變了。
- The order of swizzles matters
調(diào)用順序?qū)τ赟wizzling來說,很重要。假設(shè)setFrame:方法僅僅被定義在NSView類里面。
[NSButton swizzle:@selector(setFrame:) with:@selector(my_buttonSetFrame:)];
[NSControl swizzle:@selector(setFrame:) with:@selector(my_controlSetFrame:)];
[NSView swizzle:@selector(setFrame:) with:@selector(my_viewSetFrame:)];
當(dāng)NSButton被swizzled之后會(huì)發(fā)生什么呢?大多數(shù)的swizzling應(yīng)該保證不會(huì)替換setFrame:方法。因?yàn)橐坏└牧诉@個(gè)方法,會(huì)影響下面所有的View。所以它會(huì)去拉取實(shí)例方法。NSButton會(huì)使用已經(jīng)存在的方法去重新定義setFrame:方法。以至于改變了IMP實(shí)現(xiàn)不會(huì)影響所有的View。相同的事情也會(huì)發(fā)生在對(duì)NSControl進(jìn)行swizzling的時(shí)候,同樣,IMP也是定義在NSView類里面,把NSControl 和 NSButton這上下兩行swizzle順序替換,結(jié)果也是相同的。
當(dāng)調(diào)用NSButton的setFrame:方法,會(huì)去調(diào)用swizzled method,然后會(huì)跳入NSView類里面定義的setFrame:方法。NSControl 和 NSView對(duì)應(yīng)的swizzled method不會(huì)被調(diào)用。
NSButton 和 NSControl各自調(diào)用各自的 swizzling方法,相互不會(huì)影響。
但是我們改變一下調(diào)用順序,把NSView放在第一位調(diào)用。
[NSView swizzle:@selector(setFrame:) with:@selector(my_viewSetFrame:)];
[NSControl swizzle:@selector(setFrame:) with:@selector(my_controlSetFrame:)];
[NSButton swizzle:@selector(setFrame:) with:@selector(my_buttonSetFrame:)];
一旦這里的NSView先進(jìn)行了swizzling了以后,情況就和上面大不相同了。NSControl的swizzling會(huì)去拉取NSView替換后的方法。相應(yīng)的,NSControl在NSButton前面,NSButton也會(huì)去拉取到NSControl替換后的方法。這樣就十分混亂了。但是順序就是這樣排列的。我們開發(fā)中如何能保證不出現(xiàn)這種混亂呢?
再者,在load方法中加載swizzle。如果僅僅是在已經(jīng)加載完成的class中做了swizzle,那么這樣做是安全的。load方法能保證父類會(huì)在其任何子類加載方法之前,加載相應(yīng)的方法。這就保證了我們調(diào)用順序的正確性。
- Difficult to understand (looks recursive)
看著傳統(tǒng)定義的swizzled method,我認(rèn)為很難去預(yù)測會(huì)發(fā)生什么。但是對(duì)比上面標(biāo)準(zhǔn)的swizzling,還是很容易明白。這一點(diǎn)已經(jīng)被解決了。
- Difficult to debug
在調(diào)試中,會(huì)出現(xiàn)奇怪的堆棧調(diào)用信息,尤其是swizzled的命名很混亂,一切方法調(diào)用都是混亂的。對(duì)比標(biāo)準(zhǔn)的swizzled方式,你會(huì)在堆棧中看到清晰的命名方法。swizzling還有一個(gè)比較難調(diào)試的一點(diǎn), 在于你很難記住當(dāng)前確切的哪個(gè)方法已經(jīng)被swizzling了。
在代碼里面寫好文檔注釋,即使你認(rèn)為這段代碼只有你一個(gè)人會(huì)看。遵循這個(gè)方式去實(shí)踐,你的代碼都會(huì)沒問題。它的調(diào)試也沒有多線程的調(diào)試?yán)щy。
最后
經(jīng)過在“神經(jīng)病院”3天的修煉之后,對(duì)OC 的Runtime理解更深了。
關(guān)于黑魔法Method swizzling,我個(gè)人覺得如果使用得當(dāng),還是很安全的。一個(gè)簡單而安全的措施是你僅僅只在load方法中去swizzle。和編程中很多事情一樣,不了解它的時(shí)候會(huì)很危險(xiǎn)可怕,但是一旦明白了它的原理之后,使用它又會(huì)變得非常正確高效。
對(duì)于多人開發(fā),尤其是改動(dòng)過Runtime的地方,文檔記錄一定要完整。如果某人不知道某個(gè)方法被Swizzling了,出現(xiàn)問題調(diào)試起來,十分蛋疼。
如果是SDK開發(fā),某些Swizzling會(huì)改變?nèi)值囊恍┓椒ǖ臅r(shí)候,一定要在文檔里面標(biāo)注清楚,否則使用SDK的人不知道,出現(xiàn)各種奇怪的問題,又要被坑好久。
在合理使用 + 文檔完整齊全 的情況下,解決特定問題,使用Runtime還是非常簡潔安全的。
日??赡苡玫谋容^多的Runtime函數(shù)可能就是下面這些
//獲取cls類對(duì)象所有成員ivar結(jié)構(gòu)體
Ivar *class_copyIvarList(Class cls, unsigned int *outCount)
//獲取cls類對(duì)象name對(duì)應(yīng)的實(shí)例方法結(jié)構(gòu)體
Method class_getInstanceMethod(Class cls, SEL name)
//獲取cls類對(duì)象name對(duì)應(yīng)類方法結(jié)構(gòu)體
Method class_getClassMethod(Class cls, SEL name)
//獲取cls類對(duì)象name對(duì)應(yīng)方法imp實(shí)現(xiàn)
IMP class_getMethodImplementation(Class cls, SEL name)
//測試cls對(duì)應(yīng)的實(shí)例是否響應(yīng)sel對(duì)應(yīng)的方法
BOOL class_respondsToSelector(Class cls, SEL sel)
//獲取cls對(duì)應(yīng)方法列表
Method *class_copyMethodList(Class cls, unsigned int *outCount)
//測試cls是否遵守protocol協(xié)議
BOOL class_conformsToProtocol(Class cls, Protocol *protocol)
//為cls類對(duì)象添加新方法
BOOL class_addMethod(Class cls, SEL name, IMP imp, const char *types)
//替換cls類對(duì)象中name對(duì)應(yīng)方法的實(shí)現(xiàn)
IMP class_replaceMethod(Class cls, SEL name, IMP imp, const char *types)
//為cls添加新成員
BOOL class_addIvar(Class cls, const char *name, size_t size, uint8_t alignment, const char *types)
//為cls添加新屬性
BOOL class_addProperty(Class cls, const char *name, const objc_property_attribute_t *attributes, unsigned int attributeCount)
//獲取m對(duì)應(yīng)的選擇器
SEL method_getName(Method m)
//獲取m對(duì)應(yīng)的方法實(shí)現(xiàn)的imp指針
IMP method_getImplementation(Method m)
//獲取m方法的對(duì)應(yīng)編碼
const char *method_getTypeEncoding(Method m)
//獲取m方法參數(shù)的個(gè)數(shù)
unsigned int method_getNumberOfArguments(Method m)
//copy方法返回值類型
char *method_copyReturnType(Method m)
//獲取m方法index索引參數(shù)的類型
char *method_copyArgumentType(Method m, unsigned int index)
//獲取m方法返回值類型
void method_getReturnType(Method m, char *dst, size_t dst_len)
//獲取方法的參數(shù)類型
void method_getArgumentType(Method m, unsigned int index, char *dst, size_t dst_len)
//設(shè)置m方法的具體實(shí)現(xiàn)指針
IMP method_setImplementation(Method m, IMP imp)
//交換m1,m2方法對(duì)應(yīng)具體實(shí)現(xiàn)的函數(shù)指針
void method_exchangeImplementations(Method m1, Method m2)
//獲取v的名稱
const char *ivar_getName(Ivar v)
//獲取v的類型編碼
const char *ivar_getTypeEncoding(Ivar v)
//設(shè)置object對(duì)象關(guān)聯(lián)的對(duì)象
void objc_setAssociatedObject(id object, const void *key, id value, objc_AssociationPolicy policy)
//獲取object關(guān)聯(lián)的對(duì)象
id objc_getAssociatedObject(id object, const void *key)
//移除object關(guān)聯(lián)的對(duì)象
void objc_removeAssociatedObjects(id object)
這些API看上去不好記,其實(shí)使用的時(shí)候不難,關(guān)于方法操作的,一般都是method開頭,關(guān)于類的,一般都是class開頭的,其他的基本都是objc開頭的,剩下的就看代碼補(bǔ)全的提示,看方法名基本就能找到想要的方法了。當(dāng)然很熟悉的話,可以直接打出指定方法,也不會(huì)依賴代碼補(bǔ)全。
還有一些關(guān)于協(xié)議相關(guān)的API以及其他一些不常用,但是也可能用到的,就需要查看Objective-C Runtime官方API文檔,這個(gè)官方文檔里面詳細(xì)說明,平時(shí)不懂的多看看文檔。
最后請(qǐng)大家多多指教。
Ps.這篇干貨有點(diǎn)多,簡書提示文章字?jǐn)?shù)快到上限了,還好都寫完了。順利出院了!
