神經病院 Objective-C Runtime 出院第三天——如何正確使用 Runtime

一縷殤流化隱半邊冰霜發表於2019-03-04

前言

到了今天終於要"出院"了,要總結一下住院幾天的收穫,談談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 方法,替換任何類的實現以及新增任意類。

舉的最多的例子應該就是埋點統計使用者資訊的例子。

假設我們需要在頁面上不同的地方統計使用者資訊,常見做法有兩種:

  1. 傻瓜式的在所有需要統計的頁面都加上程式碼。這樣做簡單,但是重複的程式碼太多。
  2. 把統計的程式碼寫入基類中,比如說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方法的區別。

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.這篇乾貨有點多,還好都寫完了。順利出院了!

相關文章