前言
到了今天終于要"出院"了,要總結一下住院幾天的收獲,談談Runtime到底能為我們開發帶來些什么好處。當然它也是把雙刃劍,使用不當的話,也會成為開發路上的一個大坑。
目錄
- 1.Runtime的優點
- (1) 實現多繼承Multiple Inheritance
- (2) Method Swizzling
- (3) Aspect Oriented Programming
- (4) Isa Swizzling
- (5) Associated Object關聯對象
- (6) 動態的增加方法
- (7) NSCoding的自動歸檔和自動解檔
- (8) 字典和模型互相轉換
- 2.Runtime的缺點
一. 實現多繼承Multiple Inheritance
在上一篇文章里面講到的forwardingTargetForSelector:方法就能知道,一個類可以做到繼承多個類的效果,只需要在這一步將消息轉發給正確的類對象就可以模擬多繼承的效果。
在官方文檔上記錄了這樣一段例子。
在OC程序中可以借用消息轉發機制來實現多繼承的功能。 在上圖中,一個對象對一個消息做出回應,類似于另一個對象中的方法借過來或是“繼承”過來一樣。 在圖中,warrior實例轉發了一個negotiate消息到Diplomat實例中,執行Diplomat中的negotiate方法,結果看起來像是warrior實例執行了一個和Diplomat實例一樣的negotiate方法,其實執行者還是Diplomat實例。
這使得不同繼承體系分支下的兩個類可以“繼承”對方的方法,這樣一個類可以響應自己繼承分支里面的方法,同時也能響應其他不相干類發過來的消息。在上圖中Warrior和Diplomat沒有繼承關系,但是Warrior將negotiate消息轉發給了Diplomat后,就好似Diplomat是Warrior的超類一樣。
消息轉發提供了許多類似于多繼承的特性,但是他們之間有一個很大的不同:
多繼承:合并了不同的行為特征在一個單獨的對象中,會得到一個重量級多層面的對象。
消息轉發:將各個功能分散到不同的對象中,得到的一些輕量級的對象,這些對象通過消息通過消息轉發聯合起來。
這里值得說明的一點是,即使我們利用轉發消息來實現了“假”繼承,但是NSObject類還是會將兩者區分開。像respondsToSelector:和 isKindOfClass:這類方法只會考慮繼承體系,不會考慮轉發鏈。比如上圖中一個Warrior對象如果被問到是否能響應negotiate消息:
if ( [aWarrior respondsToSelector:@selector(negotiate)] )
結果是NO,雖然它能夠響應negotiate消息而不報錯,但是它是靠轉發消息給Diplomat類來響應消息的。
如果非要制造假象,反應出這種“假”的繼承關系,那么需要重新實現 respondsToSelector:和 isKindOfClass:來加入你的轉發算法:
- (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:中也應該寫一份轉發算法。如果使用了協議,conformsToProtocol:也一樣需要重寫。類似地,如果一個對象轉發它接受的任何遠程消息,它得給出一個methodSignatureForSelector:來返回準確的方法描述,這個方法會最終響應被轉發的消息。比如一個對象能給它的替代者對象轉發消息,它需要像下面這樣實現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.
需要引起注意的一點,實現methodSignatureForSelector方法是一種先進的技術,只適用于沒有其他解決方案的情況下。它不會作為繼承的替代。如果您必須使用這種技術,請確保您完全理解類做的轉發和您轉發的類的行為。請勿濫用!
二.Method Swizzling
提到Objective-C 中的 Runtime,大多數人第一個想到的可能就是黑魔法Method Swizzling。畢竟這是Runtime里面很強大的一部分,它可以通過Runtime的API實現更改任意的方法,理論上可以在運行時通過類名/方法名hook到任何 OC 方法,替換任何類的實現以及新增任意類。
舉的最多的例子應該就是埋點統計用戶信息的例子。
假設我們需要在頁面上不同的地方統計用戶信息,常見做法有兩種:
- 傻瓜式的在所有需要統計的頁面都加上代碼。這樣做簡單,但是重復的代碼太多。
- 把統計的代碼寫入基類中,比如說BaseViewController。這樣雖然代碼只需要寫一次,但是UITableViewController,UICollectionViewcontroller都需要寫一遍,這樣重復的代碼依舊不少。
基于這兩點,我們這時候選用Method Swizzling來解決這個事情最優雅。
1. Method Swizzling原理
Method Swizzing是發生在運行時的,主要用于在運行時將兩個Method進行交換,我們可以將Method Swizzling代碼寫到任何地方,但是只有在這段Method Swilzzling代碼執行完畢之后互換才起作用。而且Method Swizzling也是iOS中AOP(面相切面編程)的一種實現方式,我們可以利用蘋果這一特性來實現AOP編程。
Method Swizzling本質上就是對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對應的函數或者設置交換方法實現,來動態修改方法。可以重寫某個方法而不用繼承,同時還可以調用原先的實現。所以通常應用于在category中添加一個方法。
3.Method Swizzling注意點
1.Swizzling應該總在+load中執行
Objective-C在運行時會自動調用類的兩個方法+load和+initialize。+load會在類初始加載時調用, +initialize方法是以懶加載的方式被調用的,如果程序一直沒有給某個類或它的子類發送消息,那么這個類的 +initialize方法是永遠不會被調用的。所以Swizzling要是寫在+initialize方法中,是有可能永遠都不被執行。
和+initialize比較+load能保證在類的初始化過程中被加載。
關于+load和+initialize的比較可以參看這篇文章《Objective-C +load vs +initialize》
2.Swizzling應該總是在dispatch_once中執行
Swizzling會改變全局狀態,所以在運行時采取一些預防措施,使用dispatch_once就能夠確保代碼不管有多少線程都只被執行一次。這將成為Method Swizzling的最佳實踐。
這里有一個很容易犯的錯誤,那就是繼承中用了Swizzling。如果不寫dispatch_once就會導致Swizzling失效!
舉個例子,比如同時對NSArray和NSMutableArray中的objectAtIndex:方法都進行了Swizzling,這樣可能會導致NSArray中的Swizzling失效的。
可是為什么會這樣呢?
原因是,我們沒有用dispatch_once控制Swizzling只執行一次。如果這段Swizzling被執行多次,經過多次的交換IMP和SEL之后,結果可能就是未交換之前的狀態。
比如說父類A的B方法和子類C的D方法進行交換,交換一次后,父類A持有D方法的IMP,子類C持有B方法的IMP,但是再次交換一次,就又還原了。父類A還是持有B方法的IMP,子類C還是持有D方法的IMP,這樣就相當于咩有交換。可以看出,如果不寫dispatch_once,偶數次交換以后,相當于沒有交換,Swizzling失效!
3.Swizzling在+load中執行時,不要調用[super load]
原因同注意點二,如果是多繼承,并且對同一個方法都進行了Swizzling,那么調用[super load]以后,父類的Swizzling就失效了。
4.上述模板中沒有錯誤
有些人懷疑我上述給的模板可能有錯誤。在這里需要講解一下。
在進行Swizzling的時候,我們需要用class_addMethod先進行判斷一下原有類中是否有要替換的方法的實現。
如果class_addMethod返回NO,說明當前類中有要替換方法的實現,所以可以直接進行替換,調用method_exchangeImplementations即可實現Swizzling。
如果class_addMethod返回YES,說明當前類中沒有要替換方法的實現,我們需要在父類中去尋找。這個時候就需要用到method_getImplementation去獲取class_getInstanceMethod里面的方法實現。然后再進行class_replaceMethod來實現Swizzling。
這是Swizzling需要判斷的一點。
還有一點需要注意的是,在我們替換的方法- (void)xxx_viewWillAppear:(BOOL)animated中,調用了[self xxx_viewWillAppear:animated];這不是死循環了么?
其實這里并不會死循環。
由于我們進行了Swizzling,所以其實在原來的- (void)viewWillAppear:(BOOL)animated方法中,調用的是- (void)xxx_viewWillAppear:(BOOL)animated方法的實現。所以不會造成死循環。相反的,如果這里把[self xxx_viewWillAppear:animated];改成[self viewWillAppear:animated];就會造成死循環。因為外面調用[self viewWillAppear:animated];的時候,會交換方法走到[self xxx_viewWillAppear:animated];這個方法實現中來,然后這里又去調用[self viewWillAppear:animated],就會造成死循環了。
所以按照上述Swizzling的模板來寫,就不會遇到這4點需要注意的問題啦。
4.Method Swizzling使用場景
Method Swizzling使用場景其實有很多很多,在一些特殊的開發需求中適時的使用黑魔法,可以做法神來之筆的效果。這里就舉3種常見的場景。
1.實現AOP
AOP的例子在上一篇文章中舉了一個例子,在下一章中也打算詳細分析一下其實現原理,這里就一筆帶過。
2.實現埋點統計
如果app有埋點需求,并且要自己實現一套埋點邏輯,那么這里用到Swizzling是很合適的選擇。優點在開頭已經分析了,這里不再贅述。看到一篇分析的挺精彩的埋點的文章,推薦大家閱讀。
iOS動態性(二)可復用而且高度解耦的用戶統計埋點實現
3.實現異常保護
日常開發我們經常會遇到NSArray數組越界的情況,蘋果的API也沒有對異常保護,所以需要我們開發者開發時候多多留意。關于Index有好多方法,objectAtIndex,removeObjectAtIndex,replaceObjectAtIndex,exchangeObjectAtIndex等等,這些設計到Index都需要判斷是否越界。
常見做法是給NSArray,NSMutableArray增加分類,增加這些異常保護的方法,不過如果原有工程里面已經寫了大量的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
注意,調用這個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).
類似記錄日志、身份驗證、緩存等事務非常瑣碎,與業務邏輯無關,很多地方都有,又很難抽象出一個模塊,這種程序設計問題,業界給它們起了一個名字叫橫向關注點(Cross-cutting concern),AOP作用就是分離橫向關注點(Cross-cutting concern)來提高模塊復用性,它可以在既有的代碼添加一些額外的行為(記錄日志、身份驗證、緩存)而無需修改代碼。
接下來分析分析AOP的工作原理。
在上一篇中我們分析過了,在objc_msgSend函數查找IMP的過程中,如果在父類也沒有找到相應的IMP,那么就會開始執行_class_resolveMethod方法,如果不是元類,就執行_class_resolveInstanceMethod,如果是元類,執行_class_resolveClassMethod。在這個方法中,允許開發者動態增加方法實現。這個階段一般是給@dynamic屬性變量提供動態方法的。
如果_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這種方法屬于單純的轉發,無法對消息的參數和返回值進行處理。
最后到了完整轉發階段。
Runtime系統會向對象發送methodSignatureForSelector:消息,并取到返回的方法簽名用于生成NSInvocation對象。為接下來的完整的消息轉發生成一個 NSMethodSignature對象。NSMethodSignature 對象會被包裝成 NSInvocation 對象,forwardInvocation: 方法里就可以對 NSInvocation 進行處理了。
// 為目標對象中被調用的方法返回一個NSMethodSignature實例
#warning 運行時系統要求在執行標準轉發時實現這個方法
- (NSMethodSignature *)methodSignatureForSelector:(SEL)sel{
return [self.proxyTarget methodSignatureForSelector:sel];
}
對象需要創建一個NSInvocation對象,把消息調用的全部細節封裝進去,包括selector, target, arguments 等參數,還能夠對返回結果進行處理。
AOP的多數操作就是在forwardInvocation中完成的。一般會分為2個階段,一個是Intercepter注冊階段,一個是Intercepter執行階段。
1. Intercepter注冊
首先會把類里面的某個要切片的方法的IMP加入到Aspect中,類方法里面如果有forwardingTargetForSelector:的IMP,也要加入到Aspect中。
然后對類的切片方法和forwardingTargetForSelector:的IMP進行替換。兩者的IMP相應的替換為objc_msgForward()方法和hook過的forwardingTargetForSelector:。這樣主要的Intercepter注冊就完成了。
2. Intercepter執行
當執行func()方法的時候,會去查找它的IMP,現在它的IMP已經被我們替換為了objc_msgForward()方法,于是開始查找備援轉發對象。
查找備援接受者調用forwardingTargetForSelector:這個方法,由于這里是被我們hook過的,所以IMP指向的是hook過的forwardingTargetForSelector:方法。這里我們會返回Aspect的target,即選取Aspect作為備援接受者。
有了備援接受者之后,就會重新objc_msgSend,從消息發送階段重頭開始。
objc_msgSend找不到指定的IMP,再進行_class_resolveMethod,這里也沒有找到,forwardingTargetForSelector:這里也不做處理,接著就會methodSignatureForSelector。在methodSignatureForSelector方法中創建一個NSInvocation對象,傳遞給最終的forwardInvocation方法。
Aspect里面的forwardInvocation方法會干所有切面的事情。這里轉發邏輯就完全由我們自定義了。Intercepter注冊的時候我們也加入了原來方法中的method()和forwardingTargetForSelector:方法的IMP,這里我們可以在forwardInvocation方法中去執行這些IMP。在執行這些IMP的前后都可以任意的插入任何IMP以達到切面的目的。
以上就是AOP的原理。
四. Isa Swizzling
前面第二點談到了黑魔法Method Swizzling,本質上就是對IMP和SEL進行交換。其實接下來要說的Isa Swizzling,和它類似,本質上也是交換,不過交換的是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.
官方給的就這么多,具體實現也沒有說的很清楚。那只能我們自己來實驗一下。
KVO是為了監聽一個對象的某個屬性值是否發生變化。在屬性值發生變化的時候,肯定會調用其setter方法。所以KVO的本質就是監聽對象有沒有調用被監聽屬性對應的setter方法。具體實現應該是重寫其setter方法即可。
官方是如何優雅的實現重寫監聽類的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在調用addObserver方法之后,蘋果的做法是在執行完addObserver: forKeyPath: options: context: 方法之后,把isa指向到另外一個類去。
在這個新類里面重寫被觀察的對象四個方法。class,setter,dealloc,_isKVOA。
1. 重寫class方法
重寫class方法是為了我們調用它的時候返回跟重寫繼承類之前同樣的內容。
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方法的區別。
這里要特別說明一下,為何打印 object_getClass 方法和 class 方法打印出來結果不同。
- (Class)class {
return object_getClass(self);
}
Class object_getClass(id obj)
{
if (obj) return obj->getIsa();
else return Nil;
}
從實現上看,兩個方法的實現都一樣的,按道理來說,打印結果應該相同,可是為何在加了 KVO 以后會出現打印結果不同呢?
** 根本原因:對于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);
}
當我們執行 NSLog 的時候,會執行上面這個方法,這個方法的 sel 是encodeWithOSLogCoder:options:maxLength:
,這個時候,self
是 NSKVONotifying_Student,上面那個 respondsToSelector 方法里面 return 的 object_getClass(self)
結果還是NSKVONotifying_Student。
打印 [stu class] 的時候,isa 當然還是 NSKVONotifying_Student。當執行到 NSLog 的時候,+ (BOOL)respondsToSelector:(SEL)sel
,又會執行到這個方法,這個時候的 self 變成了 Student,這個時候 respondsToSelector 方法里面的 object_getClass(self) 輸出當然就是 Student 了。
2. 重寫setter方法
在新的類中會重寫對應的set方法,是為了在set方法中增加另外兩個方法的調用:
- (void)willChangeValueForKey:(NSString *)key
- (void)didChangeValueForKey:(NSString *)key
在didChangeValueForKey:方法再調用
- (void)observeValueForKeyPath:(NSString *)keyPath
ofObject:(id)object
change:(NSDictionary *)change
context:(void *)context
這里有幾種情況需要說明一下:
1)如果使用了KVC
如果有訪問器方法,則運行時會在setter方法中調用will/didChangeValueForKey:方法;
如果沒用訪問器方法,運行時會在setValue:forKey方法中調用will/didChangeValueForKey:方法。
所以這種情況下,KVO是奏效的。
2)有訪問器方法
運行時會重寫訪問器方法調用will/didChangeValueForKey:方法。
因此,直接調用訪問器方法改變屬性值時,KVO也能監聽到。
3)直接調用will/didChangeValueForKey:方法。
綜上所述,只要setter中重寫will/didChangeValueForKey:方法就可以使用KVO了。
3. 重寫dealloc方法
銷毀新生成的NSKVONotifying_類。
4. 重寫_isKVOA方法
這個私有方法估計可能是用來標示該類是一個 KVO 機制聲稱的類。
Foundation 到底為我們提供了哪些用于 KVO 的輔助函數。打開 terminal,使用 nm -a 命令查看 Foundation 中的信息:
nm -a /System/Library/Frameworks/Foundation.framework/Versions/C/Foundation
里面包含了以下這些KVO中可能用到的函數:
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 提供了大部分基礎數據類型的輔助函數(Objective C中的 Boolean 只是 unsigned char 的 typedef,所以包括了,但沒有 C++中的 bool),此外還包括一些常見的結構體如 Point, Range, Rect, Size,這表明這些結構體也可以用于自動鍵值觀察,但要注意除此之外的結構體就不能用于自動鍵值觀察了。對于所有 Objective C 對象對應的是 __NSSetObjectValueAndNotify 方法。
KVO即使是蘋果官方的實現,也是有缺陷的,這里有一篇文章詳細了分析了KVO中的缺陷,主要問題在KVO的回調機制,不能傳一個selector或者block作為回調,而必須重寫-addObserver:forKeyPath:options:context:方法所引發的一系列問題。而且只監聽一兩個屬性值還好,如果監聽的屬性多了, 或者監聽了多個對象的屬性, 那有點麻煩,需要在方法里面寫很多的if-else的判斷。
最后,官方文檔上對于KVO的實現的最后,給出了需要我們注意的一點是,永遠不要用用isa來判斷一個類的繼承關系,而是應該用class方法來判斷類的實例。
五. Associated Object 關聯對象
Associated Objects是Objective-C 2.0中Runtime的特性之一。眾所周知,在 Category 中,我們無法添加@property,因為添加了@property之后并不會自動幫我們生成實例變量以及存取方法。那么,我們現在就可以通過關聯對象來實現在 Category 中添加屬性的功能了。
1. 用法
借用這篇經典文章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個函數:
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);
來說明一下這些參數的意義:
1.id object 設置關聯對象的實例對象
2.const void *key 區分不同的關聯對象的 key。這里會有3種寫法。
使用 &AssociatedObjectKey 作為key值
static char AssociatedObjectKey = "AssociatedKey";
使用AssociatedKey 作為key值
static const void *AssociatedKey = "AssociatedKey";
使用@selector
@selector(associatedKey)
3種方法都可以,不過推薦使用更加簡潔的第三種方式。
3.id value 關聯的對象
4.objc_AssociationPolicy policy 關聯對象的存儲策略,它是一個枚舉,與property的attribute 相對應。
Behavior | @property Equivalent | Description |
---|---|---|
OBJC_ASSOCIATION_ASSIGN | @property (assign) / @property (unsafe_unretained) | 弱引用關聯對象 |
OBJC_ASSOCIATION_RETAIN_NONATOMIC | @property (nonatomic, strong) | 強引用關聯對象,且為非原子操 |
OBJC_ASSOCIATION_COPY_NONATOMIC | @property (nonatomic, copy) | 復制關聯對象,且為非原子操作 |
OBJC_ASSOCIATION_RETAIN | @property (atomic, strong) | 強引用關聯對象,且為原子操作 |
OBJC_ASSOCIATION_COPY | @property (atomic, copy) | 復制關聯對象,且為原子操作 |
這里需要注意的是標記成OBJC_ASSOCIATION_ASSIGN的關聯對象和
@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.
關于關聯對象還有一點需要說明的是objc_removeAssociatedObjects。這個方法是移除源對象中所有的關聯對象,并不是其中之一。所以其方法參數中也沒有傳入指定的key。要刪除指定的關聯對象,使用 objc_setAssociatedObject 方法將對應的 key 設置成 nil 即可。
objc_setAssociatedObject(self, associatedKey, nil, OBJC_ASSOCIATION_COPY_NONATOMIC);
關聯對象3種使用場景
1.為現有的類添加私有變量
2.為現有的類添加公有屬性
3.為KVO創建一個關聯的觀察者。
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);
}
這個函數里面主要分為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);
通過調用DISGUISE( )方法獲取object地址的指針。拿到disguised_object后,通過這個key值,在AssociationsHashMap哈希表里面找到對應的value值。而這個value值ObjcAssociationMap表的首地址。
在ObjcAssociationMap表中,key值是set方法里面傳過來的形參const void *key,value值是ObjcAssociation對象。
ObjcAssociation對象中存儲了set方法最后兩個參數,policy和value。
所以objc_setAssociatedObject方法中傳的4個形參在上圖中已經標出。
現在弄清楚結構之后再來看源碼,就很容易了。objc_setAssociatedObject方法的目的就是在這2張哈希表中存儲對應的鍵值對。
先初始化一個 AssociationsManager,獲取唯一的保存關聯對象的哈希表 AssociationsHashMap,然后在AssociationsHashMap里面去查找object地址的指針。
如果找到,就找到了第二張表ObjectAssociationMap。在這張表里繼續查找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 中,調用setHasAssociatedObjects方法,表明當前對象含有關聯對象。這里的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的時候,就是我們要移除關聯對象的時候。這個時候就是在兩張表中找到對應的鍵值,并調用erase( )方法,即可刪除對應的關聯對象。
(二) 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());
}
在移除關聯對象object的時候,會先去判斷object的isa_t中的第二位has_assoc的值,當object 存在并且object->hasAssociatedObjects( )值為1的時候,才會去調用_object_remove_assocations方法。
_object_remove_assocations方法的目的是刪除第二張ObjcAssociationMap表,即刪除所有的關聯對象。刪除第二張表,就需要在第一張AssociationsHashMap表中遍歷查找。這里會把第二張ObjcAssociationMap表中所有的ObjcAssociation對象都存到一個數組elements里面,然后調用associations.erase( )刪除第二張表。最后再遍歷elements數組,把ObjcAssociation對象依次釋放。
以上就是Associated Object關聯對象3個函數的源碼分析。
六.動態的增加方法
在消息發送階段,如果在父類中也沒有找到相應的IMP,就會執行resolveInstanceMethod方法。在這個方法里面,我們可以動態的給類對象或者實例對象動態的增加方法。
+ (BOOL)resolveInstanceMethod:(SEL)sel {
NSString *selectorString = NSStringFromSelector(sel);
if ([selectorString isEqualToString:@"method1"]) {
class_addMethod(self.class, @selector(method1), (IMP)functionForMethod1, "@:");
}
return [super resolveInstanceMethod:sel];
}
關于方法操作方面的函數還有以下這些
// 調用指定方法的實現
id method_invoke ( id receiver, Method m, ... );
// 調用返回一個數據結構的方法的實現
void method_invoke_stret ( id receiver, Method m, ... );
// 獲取方法名
SEL method_getName ( Method m );
// 返回方法的實現
IMP method_getImplementation ( Method m );
// 獲取描述方法參數和返回值類型的字符串
const char * method_getTypeEncoding ( Method m );
// 獲取方法的返回值類型的字符串
char * method_copyReturnType ( Method m );
// 獲取方法的指定位置參數的類型字符串
char * method_copyArgumentType ( Method m, unsigned int index );
// 通過引用返回方法的返回值類型字符串
void method_getReturnType ( Method m, char *dst, size_t dst_len );
// 返回方法的參數的個數
unsigned int method_getNumberOfArguments ( Method m );
// 通過引用返回方法指定位置參數的類型字符串
void method_getArgumentType ( Method m, unsigned int index, char *dst, size_t dst_len );
// 返回指定方法的方法描述結構體
struct objc_method_description * method_getDescription ( Method m );
// 設置方法的實現
IMP method_setImplementation ( Method m, IMP imp );
// 交換兩個方法的實現
void method_exchangeImplementations ( Method m1, Method m2 );
這些方法其實平時不需要死記硬背,使用的時候只要先打出method開頭,后面就會有補全信息,找到相應的方法,傳入對應的方法即可。
七.NSCoding的自動歸檔和自動解檔
現在雖然手寫歸檔和解檔的時候不多了,但是自動操作還是用Runtime來實現的。
- (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;
}
手動的有一個缺陷,如果屬性多起來,要寫好多行相似的代碼,雖然功能是可以完美實現,但是看上去不是很優雅。
用runtime實現的思路就比較簡單,我們循環依次找到每個成員變量的名稱,然后利用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.調用 class_getProperty 方法獲取當前 Model 的所有屬性。
2.調用 property_copyAttributeList 獲取屬性列表。
3.根據屬性名稱生成 setter 方法。
4.使用 objc_msgSend 調用 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調用
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等都是通過這種方式實現的(利用runtime的class_copyIvarList獲取屬性數組,遍歷模型對象的所有成員屬性,根據屬性名找到字典中key值進行賦值,當然這種方法只能解決NSString、NSNumber等,如果含有NSArray或NSDictionary,還要進行第二步轉換,如果是字典數組,需要遍歷數組中的字典,利用objectWithDict方法將字典轉化為模型,在將模型放到數組中,最后把這個模型數組賦值給之前的字典數組)
2.模型轉字典
這里是上一部分字典轉模型的逆步驟:
1.調用 class_copyPropertyList 方法獲取當前 Model 的所有屬性。
2.調用 property_getName 獲取屬性名稱。
3.根據屬性名稱生成 getter 方法。
4.使用 objc_msgSend 調用 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調用
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轉字典的時候還需要再次轉換,同樣,有幾層就需要轉換幾次。
不過上述的做法是假設字典里面不再包含二級字典,如果還包含數組,數組里面再包含字典,那還需要多級轉換。這里有一個關于字典里面包含數組的demo.
九.Runtime缺點
看了上面八大點之后,是不是感覺Runtime很神奇,可以迅速解決很多問題,然而,Runtime就像一把瑞士小刀,如果使用得當,它會有效地解決問題。但使用不當,將帶來很多麻煩。在stackoverflow上有人已經提出這樣一個問題:What are the Dangers of Method Swizzling in Objective C?,它的危險性主要體現以下幾個方面:
- Method swizzling is not atomic
Method swizzling不是原子性操作。如果在+load方法里面寫,是沒有問題的,但是如果寫在+initialize方法中就會出現一些奇怪的問題。
- Changes behavior of un-owned code
如果你在一個類中重寫一個方法,并且不調用super方法,你可能會導致一些問題出現。在大多數情況下,super方法是期望被調用的(除非有特殊說明)。如果你使用同樣的思想來進行Swizzling,可能就會引起很多問題。如果你不調用原始的方法實現,那么你Swizzling改變的太多了,而導致整個程序變得不安全。
- Possible naming conflicts
命名沖突是程序開發中經常遇到的一個問題。我們經常在類別中的前綴類名稱和方法名稱。不幸的是,命名沖突是在我們程序中的像一種瘟疫。一般我們會這樣寫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(因為使用了函數指針),但是這種做法確實有效的防止了命名沖突的問題。原則上來說,其實上述做法更加符合標準化的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是不會改變方法參數的。使用Swizzling中,會改變傳遞給原來的一個函數實現的參數,例如:
[self my_setFrame:frame];
會變轉換成
objc_msgSend(self, @selector(my_setFrame:), frame);
objc_msgSend會去查找my_setFrame對應的IMP。一旦IMP找到,會把相同的參數傳遞進去。這里會找到最原始的setFrame:方法,調用執行它。但是這里的_cmd參數并不是setFrame:,現在是my_setFrame:。原始的方法就被一個它不期待的接收參數調用了。這樣并不好。
這里有一個簡單的解決辦法,上一條里面所說的,用函數指針去實現。參數就不會變了。
- The order of swizzles matters
調用順序對于Swizzling來說,很重要。假設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之后會發生什么呢?大多數的swizzling應該保證不會替換setFrame:方法。因為一旦改了這個方法,會影響下面所有的View。所以它會去拉取實例方法。NSButton會使用已經存在的方法去重新定義setFrame:方法。以至于改變了IMP實現不會影響所有的View。相同的事情也會發生在對NSControl進行swizzling的時候,同樣,IMP也是定義在NSView類里面,把NSControl 和 NSButton這上下兩行swizzle順序替換,結果也是相同的。
當調用NSButton的setFrame:方法,會去調用swizzled method,然后會跳入NSView類里面定義的setFrame:方法。NSControl 和 NSView對應的swizzled method不會被調用。
NSButton 和 NSControl各自調用各自的 swizzling方法,相互不會影響。
但是我們改變一下調用順序,把NSView放在第一位調用。
[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替換后的方法。這樣就十分混亂了。但是順序就是這樣排列的。我們開發中如何能保證不出現這種混亂呢?
再者,在load方法中加載swizzle。如果僅僅是在已經加載完成的class中做了swizzle,那么這樣做是安全的。load方法能保證父類會在其任何子類加載方法之前,加載相應的方法。這就保證了我們調用順序的正確性。
- Difficult to understand (looks recursive)
看著傳統定義的swizzled method,我認為很難去預測會發生什么。但是對比上面標準的swizzling,還是很容易明白。這一點已經被解決了。
- Difficult to debug
在調試中,會出現奇怪的堆棧調用信息,尤其是swizzled的命名很混亂,一切方法調用都是混亂的。對比標準的swizzled方式,你會在堆棧中看到清晰的命名方法。swizzling還有一個比較難調試的一點, 在于你很難記住當前確切的哪個方法已經被swizzling了。
在代碼里面寫好文檔注釋,即使你認為這段代碼只有你一個人會看。遵循這個方式去實踐,你的代碼都會沒問題。它的調試也沒有多線程的調試困難。
最后
經過在“神經病院”3天的修煉之后,對OC 的Runtime理解更深了。
關于黑魔法Method swizzling,我個人覺得如果使用得當,還是很安全的。一個簡單而安全的措施是你僅僅只在load方法中去swizzle。和編程中很多事情一樣,不了解它的時候會很危險可怕,但是一旦明白了它的原理之后,使用它又會變得非常正確高效。
對于多人開發,尤其是改動過Runtime的地方,文檔記錄一定要完整。如果某人不知道某個方法被Swizzling了,出現問題調試起來,十分蛋疼。
如果是SDK開發,某些Swizzling會改變全局的一些方法的時候,一定要在文檔里面標注清楚,否則使用SDK的人不知道,出現各種奇怪的問題,又要被坑好久。
在合理使用 + 文檔完整齊全 的情況下,解決特定問題,使用Runtime還是非常簡潔安全的。
日常可能用的比較多的Runtime函數可能就是下面這些
//獲取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實現
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協議
BOOL class_conformsToProtocol(Class cls, Protocol *protocol)
//為cls類對象添加新方法
BOOL class_addMethod(Class cls, SEL name, IMP imp, const char *types)
//替換cls類對象中name對應方法的實現
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對應的方法實現的imp指針
IMP method_getImplementation(Method m)
//獲取m方法的對應編碼
const char *method_getTypeEncoding(Method m)
//獲取m方法參數的個數
unsigned int method_getNumberOfArguments(Method m)
//copy方法返回值類型
char *method_copyReturnType(Method m)
//獲取m方法index索引參數的類型
char *method_copyArgumentType(Method m, unsigned int index)
//獲取m方法返回值類型
void method_getReturnType(Method m, char *dst, size_t dst_len)
//獲取方法的參數類型
void method_getArgumentType(Method m, unsigned int index, char *dst, size_t dst_len)
//設置m方法的具體實現指針
IMP method_setImplementation(Method m, IMP imp)
//交換m1,m2方法對應具體實現的函數指針
void method_exchangeImplementations(Method m1, Method m2)
//獲取v的名稱
const char *ivar_getName(Ivar v)
//獲取v的類型編碼
const char *ivar_getTypeEncoding(Ivar v)
//設置object對象關聯的對象
void objc_setAssociatedObject(id object, const void *key, id value, objc_AssociationPolicy policy)
//獲取object關聯的對象
id objc_getAssociatedObject(id object, const void *key)
//移除object關聯的對象
void objc_removeAssociatedObjects(id object)
這些API看上去不好記,其實使用的時候不難,關于方法操作的,一般都是method開頭,關于類的,一般都是class開頭的,其他的基本都是objc開頭的,剩下的就看代碼補全的提示,看方法名基本就能找到想要的方法了。當然很熟悉的話,可以直接打出指定方法,也不會依賴代碼補全。
還有一些關于協議相關的API以及其他一些不常用,但是也可能用到的,就需要查看Objective-C Runtime官方API文檔,這個官方文檔里面詳細說明,平時不懂的多看看文檔。
最后請大家多多指教。
Ps.這篇干貨有點多,簡書提示文章字數快到上限了,還好都寫完了。順利出院了!