前言
到了今天終于要"出院"了,要總結一下住院幾天的收獲牺蹄,談談Runtime到底能為我們開發(fā)帶來些什么好處。當然它也是把雙刃劍薄翅,使用不當?shù)脑捝忱迹矔蔀殚_發(fā)路上的一個大坑。
目錄
- 1.Runtime的優(yōu)點
- (1) 實現(xiàn)多繼承Multiple Inheritance
- (2) Method Swizzling
- (3) Aspect Oriented Programming
- (4) Isa Swizzling
- (5) Associated Object關聯(lián)對象
- (6) 動態(tài)的增加方法
- (7) NSCoding的自動歸檔和自動解檔
- (8) 字典和模型互相轉換
- 2.Runtime的缺點
一. 實現(xiàn)多繼承Multiple Inheritance
在上一篇文章里面講到的forwardingTargetForSelector:方法就能知道翘魄,一個類可以做到繼承多個類的效果鼎天,只需要在這一步將消息轉發(fā)給正確的類對象就可以模擬多繼承的效果。
在官方文檔上記錄了這樣一段例子熟丸。
在OC程序中可以借用消息轉發(fā)機制來實現(xiàn)多繼承的功能训措。 在上圖中,一個對象對一個消息做出回應光羞,類似于另一個對象中的方法借過來或是“繼承”過來一樣绩鸣。 在圖中,warrior實例轉發(fā)了一個negotiate消息到Diplomat實例中纱兑,執(zhí)行Diplomat中的negotiate方法呀闻,結果看起來像是warrior實例執(zhí)行了一個和Diplomat實例一樣的negotiate方法,其實執(zhí)行者還是Diplomat實例潜慎。
這使得不同繼承體系分支下的兩個類可以“繼承”對方的方法捡多,這樣一個類可以響應自己繼承分支里面的方法,同時也能響應其他不相干類發(fā)過來的消息铐炫。在上圖中Warrior和Diplomat沒有繼承關系垒手,但是Warrior將negotiate消息轉發(fā)給了Diplomat后,就好似Diplomat是Warrior的超類一樣倒信。
消息轉發(fā)提供了許多類似于多繼承的特性科贬,但是他們之間有一個很大的不同:
多繼承:合并了不同的行為特征在一個單獨的對象中,會得到一個重量級多層面的對象鳖悠。
消息轉發(fā):將各個功能分散到不同的對象中榜掌,得到的一些輕量級的對象,這些對象通過消息通過消息轉發(fā)聯(lián)合起來乘综。
這里值得說明的一點是憎账,即使我們利用轉發(fā)消息來實現(xiàn)了“假”繼承,但是NSObject類還是會將兩者區(qū)分開卡辰。像respondsToSelector:和 isKindOfClass:這類方法只會考慮繼承體系胞皱,不會考慮轉發(fā)鏈邪意。比如上圖中一個Warrior對象如果被問到是否能響應negotiate消息:
if ( [aWarrior respondsToSelector:@selector(negotiate)] )
結果是NO,雖然它能夠響應negotiate消息而不報錯反砌,但是它是靠轉發(fā)消息給Diplomat類來響應消息的抄罕。
如果非要制造假象,反應出這種“假”的繼承關系于颖,那么需要重新實現(xiàn) respondsToSelector:和 isKindOfClass:來加入你的轉發(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:中也應該寫一份轉發(fā)算法嚷兔。如果使用了協(xié)議森渐,conformsToProtocol:也一樣需要重寫。類似地冒晰,如果一個對象轉發(fā)它接受的任何遠程消息同衣,它得給出一個methodSignatureForSelector:來返回準確的方法描述,這個方法會最終響應被轉發(fā)的消息壶运。比如一個對象能給它的替代者對象轉發(fā)消息耐齐,它需要像下面這樣實現(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.
需要引起注意的一點,實現(xiàn)methodSignatureForSelector方法是一種先進的技術蒋情,只適用于沒有其他解決方案的情況下埠况。它不會作為繼承的替代。如果您必須使用這種技術棵癣,請確保您完全理解類做的轉發(fā)和您轉發(fā)的類的行為辕翰。請勿濫用!
二.Method Swizzling
提到Objective-C 中的 Runtime狈谊,大多數(shù)人第一個想到的可能就是黑魔法Method Swizzling喜命。畢竟這是Runtime里面很強大的一部分,它可以通過Runtime的API實現(xiàn)更改任意的方法河劝,理論上可以在運行時通過類名/方法名hook到任何 OC 方法壁榕,替換任何類的實現(xiàn)以及新增任意類。
舉的最多的例子應該就是埋點統(tǒng)計用戶信息的例子赎瞎。
假設我們需要在頁面上不同的地方統(tǒng)計用戶信息牌里,常見做法有兩種:
- 傻瓜式的在所有需要統(tǒng)計的頁面都加上代碼。這樣做簡單煎娇,但是重復的代碼太多二庵。
- 把統(tǒng)計的代碼寫入基類中,比如說BaseViewController缓呛。這樣雖然代碼只需要寫一次催享,但是UITableViewController,UICollectionViewcontroller都需要寫一遍哟绊,這樣重復的代碼依舊不少因妙。
基于這兩點,我們這時候選用Method Swizzling來解決這個事情最優(yōu)雅。
1. Method Swizzling原理
Method Swizzing是發(fā)生在運行時的攀涵,主要用于在運行時將兩個Method進行交換铣耘,我們可以將Method Swizzling代碼寫到任何地方,但是只有在這段Method Swilzzling代碼執(zhí)行完畢之后互換才起作用以故。而且Method Swizzling也是iOS中AOP(面相切面編程)的一種實現(xiàn)方式蜗细,我們可以利用蘋果這一特性來實現(xiàn)AOP編程。
Method Swizzling本質(zhì)上就是對IMP和SEL進行交換怒详。
2.Method Swizzling使用
一般我們使用都是新建一個分類炉媒,在分類中進行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可以在運行時通過修改類的方法列表中selector對應的函數(shù)或者設置交換方法實現(xiàn)昆烁,來動態(tài)修改方法吊骤。可以重寫某個方法而不用繼承静尼,同時還可以調(diào)用原先的實現(xiàn)白粉。所以通常應用于在category中添加一個方法。
3.Method Swizzling注意點
1.Swizzling應該總在+load中執(zhí)行
Objective-C在運行時會自動調(diào)用類的兩個方法+load和+initialize鼠渺。+load會在類初始加載時調(diào)用鸭巴, +initialize方法是以懶加載的方式被調(diào)用的,如果程序一直沒有給某個類或它的子類發(fā)送消息拦盹,那么這個類的 +initialize方法是永遠不會被調(diào)用的奕扣。所以Swizzling要是寫在+initialize方法中,是有可能永遠都不被執(zhí)行掌敬。
和+initialize比較+load能保證在類的初始化過程中被加載惯豆。
關于+load和+initialize的比較可以參看這篇文章《Objective-C +load vs +initialize》
2.Swizzling應該總是在dispatch_once中執(zhí)行
Swizzling會改變?nèi)譅顟B(tài),所以在運行時采取一些預防措施奔害,使用dispatch_once就能夠確保代碼不管有多少線程都只被執(zhí)行一次楷兽。這將成為Method Swizzling的最佳實踐。
這里有一個很容易犯的錯誤华临,那就是繼承中用了Swizzling芯杀。如果不寫dispatch_once就會導致Swizzling失效!
舉個例子雅潭,比如同時對NSArray和NSMutableArray中的objectAtIndex:方法都進行了Swizzling揭厚,這樣可能會導致NSArray中的Swizzling失效的。
可是為什么會這樣呢扶供?
原因是筛圆,我們沒有用dispatch_once控制Swizzling只執(zhí)行一次。如果這段Swizzling被執(zhí)行多次椿浓,經(jīng)過多次的交換IMP和SEL之后太援,結果可能就是未交換之前的狀態(tài)闽晦。
比如說父類A的B方法和子類C的D方法進行交換,交換一次后提岔,父類A持有D方法的IMP仙蛉,子類C持有B方法的IMP,但是再次交換一次碱蒙,就又還原了荠瘪。父類A還是持有B方法的IMP,子類C還是持有D方法的IMP赛惩,這樣就相當于咩有交換巧还。可以看出坊秸,如果不寫dispatch_once,偶數(shù)次交換以后澎怒,相當于沒有交換褒搔,Swizzling失效!
3.Swizzling在+load中執(zhí)行時喷面,不要調(diào)用[super load]
原因同注意點二星瘾,如果是多繼承,并且對同一個方法都進行了Swizzling惧辈,那么調(diào)用[super load]以后琳状,父類的Swizzling就失效了。
4.上述模板中沒有錯誤
有些人懷疑我上述給的模板可能有錯誤盒齿。在這里需要講解一下念逞。
在進行Swizzling的時候,我們需要用class_addMethod先進行判斷一下原有類中是否有要替換的方法的實現(xiàn)边翁。
如果class_addMethod返回NO翎承,說明當前類中有要替換方法的實現(xiàn),所以可以直接進行替換符匾,調(diào)用method_exchangeImplementations即可實現(xiàn)Swizzling叨咖。
如果class_addMethod返回YES,說明當前類中沒有要替換方法的實現(xiàn)啊胶,我們需要在父類中去尋找甸各。這個時候就需要用到method_getImplementation去獲取class_getInstanceMethod里面的方法實現(xiàn)。然后再進行class_replaceMethod來實現(xiàn)Swizzling焰坪。
這是Swizzling需要判斷的一點趣倾。
還有一點需要注意的是,在我們替換的方法- (void)xxx_viewWillAppear:(BOOL)animated中某饰,調(diào)用了[self xxx_viewWillAppear:animated];這不是死循環(huán)了么誊酌?
其實這里并不會死循環(huán)部凑。
由于我們進行了Swizzling,所以其實在原來的- (void)viewWillAppear:(BOOL)animated方法中碧浊,調(diào)用的是- (void)xxx_viewWillAppear:(BOOL)animated方法的實現(xiàn)涂邀。所以不會造成死循環(huán)。相反的箱锐,如果這里把[self xxx_viewWillAppear:animated];改成[self viewWillAppear:animated];就會造成死循環(huán)比勉。因為外面調(diào)用[self viewWillAppear:animated];的時候,會交換方法走到[self xxx_viewWillAppear:animated];這個方法實現(xiàn)中來驹止,然后這里又去調(diào)用[self viewWillAppear:animated]浩聋,就會造成死循環(huán)了。
所以按照上述Swizzling的模板來寫臊恋,就不會遇到這4點需要注意的問題啦衣洁。
4.Method Swizzling使用場景
Method Swizzling使用場景其實有很多很多,在一些特殊的開發(fā)需求中適時的使用黑魔法抖仅,可以做法神來之筆的效果坊夫。這里就舉3種常見的場景。
1.實現(xiàn)AOP
AOP的例子在上一篇文章中舉了一個例子撤卢,在下一章中也打算詳細分析一下其實現(xiàn)原理环凿,這里就一筆帶過。
2.實現(xiàn)埋點統(tǒng)計
如果app有埋點需求放吩,并且要自己實現(xiàn)一套埋點邏輯智听,那么這里用到Swizzling是很合適的選擇。優(yōu)點在開頭已經(jīng)分析了渡紫,這里不再贅述到推。看到一篇分析的挺精彩的埋點的文章惕澎,推薦大家閱讀环肘。
iOS動態(tài)性(二)可復用而且高度解耦的用戶統(tǒng)計埋點實現(xiàn)
3.實現(xiàn)異常保護
日常開發(fā)我們經(jīng)常會遇到NSArray數(shù)組越界的情況,蘋果的API也沒有對異常保護集灌,所以需要我們開發(fā)者開發(fā)時候多多留意悔雹。關于Index有好多方法,objectAtIndex欣喧,removeObjectAtIndex腌零,replaceObjectAtIndex,exchangeObjectAtIndex等等唆阿,這些設計到Index都需要判斷是否越界益涧。
常見做法是給NSArray,NSMutableArray增加分類驯鳖,增加這些異常保護的方法闲询,不過如果原有工程里面已經(jīng)寫了大量的AtIndex系列的方法久免,去替換成新的分類的方法,效率會比較低扭弧。這里可以考慮用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)用這個objc_getClass方法的時候鸽捻,要先知道類對應的真實的類名才行呼巴,NSArray其實在Runtime中對應著__NSArrayI,NSMutableArray對應著__NSArrayM御蒲,NSDictionary對應著__NSDictionaryI衣赶,NSMutableDictionary對應著__NSDictionaryM。
三. Aspect Oriented Programming
Wikipedia 里對 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è)務邏輯無關遵馆,很多地方都有,又很難抽象出一個模塊敲街,這種程序設計問題,業(yè)界給它們起了一個名字叫橫向關注點(Cross-cutting concern)严望,AOP作用就是分離橫向關注點(Cross-cutting concern)來提高模塊復用性多艇,它可以在既有的代碼添加一些額外的行為(記錄日志削解、身份驗證醉者、緩存)而無需修改代碼。
接下來分析分析AOP的工作原理晃琳。
在上一篇中我們分析過了拨匆,在objc_msgSend函數(shù)查找IMP的過程中姆涩,如果在父類也沒有找到相應的IMP,那么就會開始執(zhí)行_class_resolveMethod方法惭每,如果不是元類骨饿,就執(zhí)行_class_resolveInstanceMethod,如果是元類台腥,執(zhí)行_class_resolveClassMethod宏赘。在這個方法中,允許開發(fā)者動態(tài)增加方法實現(xiàn)黎侈。這個階段一般是給@dynamic屬性變量提供動態(tài)方法的察署。
如果_class_resolveMethod無法處理,會開始選擇備援接受者接受消息峻汉,這個時候就到了forwardingTargetForSelector方法贴汪。如果該方法返回非nil的對象脐往,則使用該對象作為新的消息接收者。
- (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];
}
替換類方法返回值就是一個類對象扳埂。
forwardingTargetForSelector這種方法屬于單純的轉發(fā)业簿,無法對消息的參數(shù)和返回值進行處理。
最后到了完整轉發(fā)階段聂喇。
Runtime系統(tǒng)會向?qū)ο蟀l(fā)送methodSignatureForSelector:消息辖源,并取到返回的方法簽名用于生成NSInvocation對象。為接下來的完整的消息轉發(fā)生成一個 NSMethodSignature對象希太。NSMethodSignature 對象會被包裝成 NSInvocation 對象克饶,forwardInvocation: 方法里就可以對 NSInvocation 進行處理了。
// 為目標對象中被調(diào)用的方法返回一個NSMethodSignature實例
#warning 運行時系統(tǒng)要求在執(zhí)行標準轉發(fā)時實現(xiàn)這個方法
- (NSMethodSignature *)methodSignatureForSelector:(SEL)sel{
return [self.proxyTarget methodSignatureForSelector:sel];
}
對象需要創(chuàng)建一個NSInvocation對象誊辉,把消息調(diào)用的全部細節(jié)封裝進去矾湃,包括selector, target, arguments 等參數(shù),還能夠?qū)Ψ祷亟Y果進行處理堕澄。
AOP的多數(shù)操作就是在forwardInvocation中完成的邀跃。一般會分為2個階段,一個是Intercepter注冊階段蛙紫,一個是Intercepter執(zhí)行階段拍屑。
1. Intercepter注冊
首先會把類里面的某個要切片的方法的IMP加入到Aspect中,類方法里面如果有forwardingTargetForSelector:的IMP坑傅,也要加入到Aspect中僵驰。
然后對類的切片方法和forwardingTargetForSelector:的IMP進行替換。兩者的IMP相應的替換為objc_msgForward()方法和hook過的forwardingTargetForSelector:唁毒。這樣主要的Intercepter注冊就完成了蒜茴。
2. Intercepter執(zhí)行
當執(zhí)行func()方法的時候,會去查找它的IMP浆西,現(xiàn)在它的IMP已經(jīng)被我們替換為了objc_msgForward()方法粉私,于是開始查找備援轉發(fā)對象。
查找備援接受者調(diào)用forwardingTargetForSelector:這個方法近零,由于這里是被我們hook過的诺核,所以IMP指向的是hook過的forwardingTargetForSelector:方法。這里我們會返回Aspect的target久信,即選取Aspect作為備援接受者猪瞬。
有了備援接受者之后,就會重新objc_msgSend入篮,從消息發(fā)送階段重頭開始陈瘦。
objc_msgSend找不到指定的IMP,再進行_class_resolveMethod,這里也沒有找到痊项,forwardingTargetForSelector:這里也不做處理锅风,接著就會methodSignatureForSelector。在methodSignatureForSelector方法中創(chuàng)建一個NSInvocation對象鞍泉,傳遞給最終的forwardInvocation方法皱埠。
Aspect里面的forwardInvocation方法會干所有切面的事情。這里轉發(fā)邏輯就完全由我們自定義了咖驮。Intercepter注冊的時候我們也加入了原來方法中的method()和forwardingTargetForSelector:方法的IMP边器,這里我們可以在forwardInvocation方法中去執(zhí)行這些IMP。在執(zhí)行這些IMP的前后都可以任意的插入任何IMP以達到切面的目的托修。
以上就是AOP的原理忘巧。
四. Isa Swizzling
前面第二點談到了黑魔法Method Swizzling,本質(zhì)上就是對IMP和SEL進行交換睦刃。其實接下來要說的Isa Swizzling砚嘴,和它類似,本質(zhì)上也是交換涩拙,不過交換的是Isa际长。
在蘋果的官方庫里面有一個很有名的技術就用到了這個Isa Swizzling,那就是KVO——Key-Value Observing兴泥。
官方文檔上對于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.
官方給的就這么多工育,具體實現(xiàn)也沒有說的很清楚。那只能我們自己來實驗一下搓彻。
KVO是為了監(jiān)聽一個對象的某個屬性值是否發(fā)生變化如绸。在屬性值發(fā)生變化的時候,肯定會調(diào)用其setter方法好唯。所以KVO的本質(zhì)就是監(jiān)聽對象有沒有調(diào)用被監(jiān)聽屬性對應的setter方法竭沫。具體實現(xiàn)應該是重寫其setter方法即可燥翅。
官方是如何優(yōu)雅的實現(xiàn)重寫監(jiān)聽類的setter方法的呢骑篙?實驗代碼如下:
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
通過打印,我們可以很明顯的看到森书,被觀察的對象的isa變了靶端,變成了NSKVONotifying_Student這個類了。
在@interface NSObject(NSKeyValueObserverRegistration) 這個分類里面凛膏,蘋果定義了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指向到另外一個類去台谍。
在這個新類里面重寫被觀察的對象四個方法。class吁断,setter趁蕊,dealloc坞生,_isKVOA。
1. 重寫class方法
重寫class方法是為了我們調(diào)用它的時候返回跟重寫繼承類之前同樣的內(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)));
}
打印結果
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 方法打印出來結果不同卒废。
- (Class)class {
return object_getClass(self);
}
Class object_getClass(id obj)
{
if (obj) return obj->getIsa();
else return Nil;
}
從實現(xiàn)上看,兩個方法的實現(xiàn)都一樣的宙地,按道理來說摔认,打印結果應該相同,可是為何在加了 KVO 以后會出現(xiàn)打印結果不同呢绸栅?
** 根本原因:對于KVO级野,底層交換了 NSKVONotifying_Student 的 class 方法,讓其返回 Student粹胯。**
打印這句話 object_getClass(stu) 的時候蓖柔,isa 當然是 NSKVONotifying_Student。
+ (BOOL)respondsToSelector:(SEL)sel {
if (!sel) return NO;
return class_respondsToSelector_inst(object_getClass(self), sel, self);
}
當我們執(zhí)行 NSLog 的時候风纠,會執(zhí)行上面這個方法况鸣,這個方法的 sel 是encodeWithOSLogCoder:options:maxLength:
,這個時候竹观,self
是 NSKVONotifying_Student镐捧,上面那個 respondsToSelector 方法里面 return 的 object_getClass(self)
結果還是NSKVONotifying_Student。
打印 [stu class] 的時候臭增,isa 當然還是 NSKVONotifying_Student懂酱。當執(zhí)行到 NSLog 的時候,+ (BOOL)respondsToSelector:(SEL)sel
誊抛,又會執(zhí)行到這個方法列牺,這個時候的 self 變成了 Student,這個時候 respondsToSelector 方法里面的 object_getClass(self) 輸出當然就是 Student 了拗窃。
2. 重寫setter方法
在新的類中會重寫對應的set方法瞎领,是為了在set方法中增加另外兩個方法的調(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
如果有訪問器方法,則運行時會在setter方法中調(diào)用will/didChangeValueForKey:方法随夸;
如果沒用訪問器方法九默,運行時會在setValue:forKey方法中調(diào)用will/didChangeValueForKey:方法。
所以這種情況下宾毒,KVO是奏效的驼修。
2)有訪問器方法
運行時會重寫訪問器方法調(diào)用will/didChangeValueForKey:方法。
因此,直接調(diào)用訪問器方法改變屬性值時乙各,KVO也能監(jiān)聽到勉躺。
3)直接調(diào)用will/didChangeValueForKey:方法。
綜上所述觅丰,只要setter中重寫will/didChangeValueForKey:方法就可以使用KVO了饵溅。
3. 重寫dealloc方法
銷毀新生成的NSKVONotifying_類。
4. 重寫_isKVOA方法
這個私有方法估計可能是用來標示該類是一個 KVO 機制聲稱的類妇萄。
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 提供了大部分基礎數(shù)據(jù)類型的輔助函數(shù)(Objective C中的 Boolean 只是 unsigned char 的 typedef冠句,所以包括了轻掩,但沒有 C++中的 bool),此外還包括一些常見的結構體如 Point, Range, Rect, Size懦底,這表明這些結構體也可以用于自動鍵值觀察唇牧,但要注意除此之外的結構體就不能用于自動鍵值觀察了。對于所有 Objective C 對象對應的是 __NSSetObjectValueAndNotify 方法聚唐。
KVO即使是蘋果官方的實現(xiàn)丐重,也是有缺陷的,這里有一篇文章詳細了分析了KVO中的缺陷杆查,主要問題在KVO的回調(diào)機制扮惦,不能傳一個selector或者block作為回調(diào),而必須重寫-addObserver:forKeyPath:options:context:方法所引發(fā)的一系列問題亲桦。而且只監(jiān)聽一兩個屬性值還好崖蜜,如果監(jiān)聽的屬性多了, 或者監(jiān)聽了多個對象的屬性, 那有點麻煩,需要在方法里面寫很多的if-else的判斷客峭。
最后豫领,官方文檔上對于KVO的實現(xiàn)的最后,給出了需要我們注意的一點是舔琅,永遠不要用用isa來判斷一個類的繼承關系等恐,而是應該用class方法來判斷類的實例。
五. Associated Object 關聯(lián)對象
Associated Objects是Objective-C 2.0中Runtime的特性之一搏明。眾所周知鼠锈,在 Category 中闪檬,我們無法添加@property星著,因為添加了@property之后并不會自動幫我們生成實例變量以及存取方法。那么粗悯,我們現(xiàn)在就可以通過關聯(lián)對象來實現(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個函數(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 設置關聯(lián)對象的實例對象
2.const void *key 區(qū)分不同的關聯(lián)對象的 key。這里會有3種寫法横缔。
使用 &AssociatedObjectKey 作為key值
static char AssociatedObjectKey = "AssociatedKey";
使用AssociatedKey 作為key值
static const void *AssociatedKey = "AssociatedKey";
使用@selector
@selector(associatedKey)
3種方法都可以铺遂,不過推薦使用更加簡潔的第三種方式。
3.id value 關聯(lián)的對象
4.objc_AssociationPolicy policy 關聯(lián)對象的存儲策略茎刚,它是一個枚舉襟锐,與property的attribute 相對應。
Behavior | @property Equivalent | Description |
---|---|---|
OBJC_ASSOCIATION_ASSIGN | @property (assign) / @property (unsafe_unretained) | 弱引用關聯(lián)對象 |
OBJC_ASSOCIATION_RETAIN_NONATOMIC | @property (nonatomic, strong) | 強引用關聯(lián)對象膛锭,且為非原子操 |
OBJC_ASSOCIATION_COPY_NONATOMIC | @property (nonatomic, copy) | 復制關聯(lián)對象粮坞,且為非原子操作 |
OBJC_ASSOCIATION_RETAIN | @property (atomic, strong) | 強引用關聯(lián)對象,且為原子操作 |
OBJC_ASSOCIATION_COPY | @property (atomic, copy) | 復制關聯(lián)對象初狰,且為原子操作 |
這里需要注意的是標記成OBJC_ASSOCIATION_ASSIGN的關聯(lián)對象和
@property (weak) 是不一樣的莫杈,上面表格中等價定義寫的是 @property (unsafe_unretained),對象被銷毀時奢入,屬性值仍然還在筝闹。如果之后再次使用該對象就會導致程序閃退。所以我們在使用OBJC_ASSOCIATION_ASSIGN時腥光,要格外注意关顷。
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.
關于關聯(lián)對象還有一點需要說明的是objc_removeAssociatedObjects。這個方法是移除源對象中所有的關聯(lián)對象武福,并不是其中之一解寝。所以其方法參數(shù)中也沒有傳入指定的key。要刪除指定的關聯(lián)對象艘儒,使用 objc_setAssociatedObject 方法將對應的 key 設置成 nil 即可聋伦。
objc_setAssociatedObject(self, associatedKey, nil, OBJC_ASSOCIATION_COPY_NONATOMIC);
關聯(lián)對象3種使用場景
1.為現(xiàn)有的類添加私有變量
2.為現(xiàn)有的類添加公有屬性
3.為KVO創(chuàng)建一個關聯(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);
}
這個函數(shù)里面主要分為2部分界睁,一部分是if里面對應的new_value不為nil的時候觉增,另一部分是else里面對應的new_value為nil的情況。
當new_value不為nil的時候翻斟,查找時候逾礁,流程如下:
首先在AssociationsManager的結構如下
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中有一個spinlock類型的自旋鎖lock。保證每次只有一個線程對AssociationsManager進行操作访惜,保證線程安全嘹履。AssociationsHashMap對應的是一張哈希表。
AssociationsHashMap哈希表里面key是disguised_ptr_t债热。
disguised_ptr_t disguised_object = DISGUISE(object);
通過調(diào)用DISGUISE( )方法獲取object地址的指針砾嫉。拿到disguised_object后,通過這個key值窒篱,在AssociationsHashMap哈希表里面找到對應的value值焕刮。而這個value值ObjcAssociationMap表的首地址舶沿。
在ObjcAssociationMap表中,key值是set方法里面?zhèn)鬟^來的形參const void *key配并,value值是ObjcAssociation對象括荡。
ObjcAssociation對象中存儲了set方法最后兩個參數(shù),policy和value溉旋。
所以objc_setAssociatedObject方法中傳的4個形參在上圖中已經(jīng)標出畸冲。
現(xiàn)在弄清楚結構之后再來看源碼,就很容易了观腊。objc_setAssociatedObject方法的目的就是在這2張哈希表中存儲對應的鍵值對召夹。
先初始化一個 AssociationsManager,獲取唯一的保存關聯(lián)對象的哈希表 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找到對應的ObjcAssociation對象,那就更新它的值迄委。如果沒有找到褐筛,就新建一個ObjcAssociation對象,放入第二張表ObjectAssociationMap中叙身。
再回到第一張表AssociationsHashMap中渔扎,如果沒有找到對應的鍵值
ObjectAssociationMap *refs = new ObjectAssociationMap;
associations[disguised_object] = refs;
(*refs)[key] = ObjcAssociation(policy, new_value);
object->setHasAssociatedObjects();
此時就不存在第二張表ObjectAssociationMap了,這時就需要新建第二張ObjectAssociationMap表信轿,來維護對象的所有新增屬性晃痴。新建完第二張ObjectAssociationMap表之后,還需要再實例化 ObjcAssociation對象添加到 Map 中财忽,調(diào)用setHasAssociatedObjects方法倘核,表明當前對象含有關聯(lián)對象。這里的setHasAssociatedObjects方法即彪,改變的是isa_t結構體中的第二個標志位has_assoc的值紧唱。(關于isa_t結構體的結構,詳情請看第一天的解析)
// release the old value (outside of the lock).
if (old_association.hasValue()) ReleaseValue()(old_association);
最后如果老的association對象有值隶校,此時還會釋放它漏益。
以上是new_value不為nil的情況。其實只要記住上面那2張表的結構深胳,這個objc_setAssociatedObject的過程就是更新 / 新建 表中鍵值對的過程绰疤。
再來看看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);
}
}
當new_value為nil的時候,就是我們要移除關聯(lián)對象的時候稠屠。這個時候就是在兩張表中找到對應的鍵值峦睡,并調(diào)用erase( )方法,即可刪除對應的關聯(lián)對象权埠。
(二) 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表的所有鍵值找到對應的ObjcAssociation對象,找到了就返回ObjcAssociation對象攘蔽,沒有找到就返回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());
}
在移除關聯(lián)對象object的時候,會先去判斷object的isa_t中的第二位has_assoc的值满俗,當object 存在并且object->hasAssociatedObjects( )值為1的時候转捕,才會去調(diào)用_object_remove_assocations方法。
_object_remove_assocations方法的目的是刪除第二張ObjcAssociationMap表唆垃,即刪除所有的關聯(lián)對象五芝。刪除第二張表,就需要在第一張AssociationsHashMap表中遍歷查找辕万。這里會把第二張ObjcAssociationMap表中所有的ObjcAssociation對象都存到一個數(shù)組elements里面枢步,然后調(diào)用associations.erase( )刪除第二張表。最后再遍歷elements數(shù)組渐尿,把ObjcAssociation對象依次釋放醉途。
以上就是Associated Object關聯(lián)對象3個函數(shù)的源碼分析。
六.動態(tài)的增加方法
在消息發(fā)送階段砖茸,如果在父類中也沒有找到相應的IMP隘擎,就會執(zhí)行resolveInstanceMethod方法。在這個方法里面凉夯,我們可以動態(tài)的給類對象或者實例對象動態(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];
}
關于方法操作方面的函數(shù)還有以下這些
// 調(diào)用指定方法的實現(xiàn)
id method_invoke ( id receiver, Method m, ... );
// 調(diào)用返回一個數(shù)據(jù)結構的方法的實現(xiàn)
void method_invoke_stret ( id receiver, Method m, ... );
// 獲取方法名
SEL method_getName ( Method m );
// 返回方法的實現(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ù)的個數(shù)
unsigned int method_getNumberOfArguments ( Method m );
// 通過引用返回方法指定位置參數(shù)的類型字符串
void method_getArgumentType ( Method m, unsigned int index, char *dst, size_t dst_len );
// 返回指定方法的方法描述結構體
struct objc_method_description * method_getDescription ( Method m );
// 設置方法的實現(xiàn)
IMP method_setImplementation ( Method m, IMP imp );
// 交換兩個方法的實現(xiàn)
void method_exchangeImplementations ( Method m1, Method m2 );
這些方法其實平時不需要死記硬背,使用的時候只要先打出method開頭劲够,后面就會有補全信息宝惰,找到相應的方法,傳入對應的方法即可再沧。
七.NSCoding的自動歸檔和自動解檔
現(xiàn)在雖然手寫歸檔和解檔的時候不多了尼夺,但是自動操作還是用Runtime來實現(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;
}
手動的有一個缺陷炒瘸,如果屬性多起來淤堵,要寫好多行相似的代碼,雖然功能是可以完美實現(xiàn)顷扩,但是看上去不是很優(yōu)雅拐邪。
用runtime實現(xiàn)的思路就比較簡單,我們循環(huán)依次找到每個成員變量的名稱隘截,然后利用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方法用來獲取當前 Model 的所有成員變量汹胃,ivar_getName方法用來獲取每個成員變量的名稱。
八.字典和模型互相轉換
1.字典轉模型
1.調(diào)用 class_getProperty 方法獲取當前 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];
/*判斷當前屬性是不是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赁濒,那么這里就需要再次轉換一次轨奄,當然這里有幾層就需要轉換幾次。
幾個出名的開源庫JSONModel拒炎、MJExtension等都是通過這種方式實現(xiàn)的(利用runtime的class_copyIvarList獲取屬性數(shù)組挪拟,遍歷模型對象的所有成員屬性,根據(jù)屬性名找到字典中key值進行賦值击你,當然這種方法只能解決NSString舞丛、NSNumber等,如果含有NSArray或NSDictionary果漾,還要進行第二步轉換球切,如果是字典數(shù)組,需要遍歷數(shù)組中的字典绒障,利用objectWithDict方法將字典轉化為模型吨凑,在將模型放到數(shù)組中,最后把這個模型數(shù)組賦值給之前的字典數(shù)組)
2.模型轉字典
這里是上一部分字典轉模型的逆步驟:
1.調(diào)用 class_copyPropertyList 方法獲取當前 Model 的所有屬性户辱。
2.調(diào)用 property_getName 獲取屬性名稱鸵钝。
3.根據(jù)屬性名稱生成 getter 方法。
4.使用 objc_msgSend 調(diào)用 getter 方法獲取屬性值(或者 KVC)
//模型轉字典
-(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);
/*判斷當前屬性是不是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轉字典的時候還需要再次轉換必逆,同樣怠堪,有幾層就需要轉換幾次。
不過上述的做法是假設字典里面不再包含二級字典名眉,如果還包含數(shù)組粟矿,數(shù)組里面再包含字典,那還需要多級轉換损拢。這里有一個關于字典里面包含數(shù)組的demo.
九.Runtime缺點
看了上面八大點之后陌粹,是不是感覺Runtime很神奇,可以迅速解決很多問題福压,然而掏秩,Runtime就像一把瑞士小刀或舞,如果使用得當,它會有效地解決問題蒙幻。但使用不當映凳,將帶來很多麻煩。在stackoverflow上有人已經(jīng)提出這樣一個問題:What are the Dangers of Method Swizzling in Objective C?杆煞,它的危險性主要體現(xiàn)以下幾個方面:
- Method swizzling is not atomic
Method swizzling不是原子性操作魏宽。如果在+load方法里面寫腐泻,是沒有問題的决乎,但是如果寫在+initialize方法中就會出現(xiàn)一些奇怪的問題。
- Changes behavior of un-owned code
如果你在一個類中重寫一個方法派桩,并且不調(diào)用super方法构诚,你可能會導致一些問題出現(xiàn)。在大多數(shù)情況下棉圈,super方法是期望被調(diào)用的(除非有特殊說明)薄霜。如果你使用同樣的思想來進行Swizzling糟红,可能就會引起很多問題。如果你不調(diào)用原始的方法實現(xiàn)丑蛤,那么你Swizzling改變的太多了,而導致整個程序變得不安全撕阎。
- Possible naming conflicts
命名沖突是程序開發(fā)中經(jīng)常遇到的一個問題受裹。我們經(jīng)常在類別中的前綴類名稱和方法名稱。不幸的是虏束,命名沖突是在我們程序中的像一種瘟疫棉饶。一般我們會這樣寫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
這樣寫看上去是沒有問題的。但是如果在整個大型程序中還有另外一處定義了my_setFrame:方法呢镇匀?那又會造成命名沖突的問題照藻。我們應該把上面的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(因為使用了函數(shù)指針),但是這種做法確實有效的防止了命名沖突的問題汗侵。原則上來說幸缕,其實上述做法更加符合標準化的Swizzling。這種做法可能和人們使用方法不同晰韵,但是這種做法更好冀值。Swizzling Method 標準定義應該是如下的樣子:
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
這一點是這些問題中最大的一個。標準的Method Swizzling是不會改變方法參數(shù)的宫屠。使用Swizzling中列疗,會改變傳遞給原來的一個函數(shù)實現(xiàn)的參數(shù),例如:
[self my_setFrame:frame];
會變轉換成
objc_msgSend(self, @selector(my_setFrame:), frame);
objc_msgSend會去查找my_setFrame對應的IMP浪蹂。一旦IMP找到抵栈,會把相同的參數(shù)傳遞進去告材。這里會找到最原始的setFrame:方法,調(diào)用執(zhí)行它古劲。但是這里的_cmd參數(shù)并不是setFrame:斥赋,現(xiàn)在是my_setFrame:。原始的方法就被一個它不期待的接收參數(shù)調(diào)用了产艾。這樣并不好疤剑。
這里有一個簡單的解決辦法,上一條里面所說的闷堡,用函數(shù)指針去實現(xiàn)隘膘。參數(shù)就不會變了。
- The order of swizzles matters
調(diào)用順序?qū)τ赟wizzling來說杠览,很重要弯菊。假設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:)];
當NSButton被swizzled之后會發(fā)生什么呢踱阿?大多數(shù)的swizzling應該保證不會替換setFrame:方法管钳。因為一旦改了這個方法,會影響下面所有的View软舌。所以它會去拉取實例方法才漆。NSButton會使用已經(jīng)存在的方法去重新定義setFrame:方法。以至于改變了IMP實現(xiàn)不會影響所有的View佛点。相同的事情也會發(fā)生在對NSControl進行swizzling的時候醇滥,同樣,IMP也是定義在NSView類里面恋脚,把NSControl 和 NSButton這上下兩行swizzle順序替換腺办,結果也是相同的。
當調(diào)用NSButton的setFrame:方法糟描,會去調(diào)用swizzled method怀喉,然后會跳入NSView類里面定義的setFrame:方法。NSControl 和 NSView對應的swizzled method不會被調(diào)用船响。
NSButton 和 NSControl各自調(diào)用各自的 swizzling方法躬拢,相互不會影響。
但是我們改變一下調(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先進行了swizzling了以后,情況就和上面大不相同了米诉。NSControl的swizzling會去拉取NSView替換后的方法菱蔬。相應的,NSControl在NSButton前面,NSButton也會去拉取到NSControl替換后的方法拴泌。這樣就十分混亂了魏身。但是順序就是這樣排列的。我們開發(fā)中如何能保證不出現(xiàn)這種混亂呢蚪腐?
再者箭昵,在load方法中加載swizzle。如果僅僅是在已經(jīng)加載完成的class中做了swizzle回季,那么這樣做是安全的家制。load方法能保證父類會在其任何子類加載方法之前,加載相應的方法泡一。這就保證了我們調(diào)用順序的正確性颤殴。
- Difficult to understand (looks recursive)
看著傳統(tǒng)定義的swizzled method,我認為很難去預測會發(fā)生什么瘾杭。但是對比上面標準的swizzling诅病,還是很容易明白哪亿。這一點已經(jīng)被解決了粥烁。
- Difficult to debug
在調(diào)試中,會出現(xiàn)奇怪的堆棧調(diào)用信息蝇棉,尤其是swizzled的命名很混亂讨阻,一切方法調(diào)用都是混亂的。對比標準的swizzled方式篡殷,你會在堆棧中看到清晰的命名方法钝吮。swizzling還有一個比較難調(diào)試的一點, 在于你很難記住當前確切的哪個方法已經(jīng)被swizzling了板辽。
在代碼里面寫好文檔注釋奇瘦,即使你認為這段代碼只有你一個人會看。遵循這個方式去實踐劲弦,你的代碼都會沒問題耳标。它的調(diào)試也沒有多線程的調(diào)試困難。
最后
經(jīng)過在“神經(jīng)病院”3天的修煉之后邑跪,對OC 的Runtime理解更深了次坡。
關于黑魔法Method swizzling,我個人覺得如果使用得當画畅,還是很安全的砸琅。一個簡單而安全的措施是你僅僅只在load方法中去swizzle。和編程中很多事情一樣轴踱,不了解它的時候會很危險可怕症脂,但是一旦明白了它的原理之后,使用它又會變得非常正確高效。
對于多人開發(fā)诱篷,尤其是改動過Runtime的地方沸版,文檔記錄一定要完整。如果某人不知道某個方法被Swizzling了兴蒸,出現(xiàn)問題調(diào)試起來视粮,十分蛋疼。
如果是SDK開發(fā)橙凳,某些Swizzling會改變?nèi)值囊恍┓椒ǖ臅r候蕾殴,一定要在文檔里面標注清楚,否則使用SDK的人不知道岛啸,出現(xiàn)各種奇怪的問題钓觉,又要被坑好久。
在合理使用 + 文檔完整齊全 的情況下坚踩,解決特定問題荡灾,使用Runtime還是非常簡潔安全的。
日乘仓可能用的比較多的Runtime函數(shù)可能就是下面這些
//獲取cls類對象所有成員ivar結構體
Ivar *class_copyIvarList(Class cls, unsigned int *outCount)
//獲取cls類對象name對應的實例方法結構體
Method class_getInstanceMethod(Class cls, SEL name)
//獲取cls類對象name對應類方法結構體
Method class_getClassMethod(Class cls, SEL name)
//獲取cls類對象name對應方法imp實現(xiàn)
IMP class_getMethodImplementation(Class cls, SEL name)
//測試cls對應的實例是否響應sel對應的方法
BOOL class_respondsToSelector(Class cls, SEL sel)
//獲取cls對應方法列表
Method *class_copyMethodList(Class cls, unsigned int *outCount)
//測試cls是否遵守protocol協(xié)議
BOOL class_conformsToProtocol(Class cls, Protocol *protocol)
//為cls類對象添加新方法
BOOL class_addMethod(Class cls, SEL name, IMP imp, const char *types)
//替換cls類對象中name對應方法的實現(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對應的選擇器
SEL method_getName(Method m)
//獲取m對應的方法實現(xiàn)的imp指針
IMP method_getImplementation(Method m)
//獲取m方法的對應編碼
const char *method_getTypeEncoding(Method m)
//獲取m方法參數(shù)的個數(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)
//設置m方法的具體實現(xiàn)指針
IMP method_setImplementation(Method m, IMP imp)
//交換m1批幌,m2方法對應具體實現(xiàn)的函數(shù)指針
void method_exchangeImplementations(Method m1, Method m2)
//獲取v的名稱
const char *ivar_getName(Ivar v)
//獲取v的類型編碼
const char *ivar_getTypeEncoding(Ivar v)
//設置object對象關聯(lián)的對象
void objc_setAssociatedObject(id object, const void *key, id value, objc_AssociationPolicy policy)
//獲取object關聯(lián)的對象
id objc_getAssociatedObject(id object, const void *key)
//移除object關聯(lián)的對象
void objc_removeAssociatedObjects(id object)
這些API看上去不好記,其實使用的時候不難嗓节,關于方法操作的荧缘,一般都是method開頭,關于類的拦宣,一般都是class開頭的截粗,其他的基本都是objc開頭的,剩下的就看代碼補全的提示鸵隧,看方法名基本就能找到想要的方法了绸罗。當然很熟悉的話,可以直接打出指定方法豆瘫,也不會依賴代碼補全珊蟀。
還有一些關于協(xié)議相關的API以及其他一些不常用,但是也可能用到的靡羡,就需要查看Objective-C Runtime官方API文檔系洛,這個官方文檔里面詳細說明,平時不懂的多看看文檔略步。
最后請大家多多指教描扯。
Ps.這篇干貨有點多,簡書提示文章字數(shù)快到上限了趟薄,還好都寫完了绽诚。順利出院了!