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

前言

到了今天終於要"出院"了,要總結一下住院幾天的收穫,談談Runtime到底能爲咱們開發帶來些什麼好處。固然它也是把雙刃劍,使用不當的話,也會成爲開發路上的一個大坑。html

目錄

  • 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:方法就能知道,一個類能夠作到繼承多個類的效果,只須要在這一步將消息轉發給正確的類對象就能夠模擬多繼承的效果。git

官方文檔上記錄了這樣一段例子。github

在OC程序中能夠借用消息轉發機制來實現多繼承的功能。 在上圖中,一個對象對一個消息作出迴應,相似於另外一個對象中的方法借過來或是「繼承」過來同樣。 在圖中,warrior實例轉發了一個negotiate消息到Diplomat實例中,執行Diplomat中的negotiate方法,結果看起來像是warrior實例執行了一個和Diplomat實例同樣的negotiate方法,其實執行者仍是Diplomat實例。objective-c

這使得不一樣繼承體系分支下的兩個類能夠「繼承」對方的方法,這樣一個類能夠響應本身繼承分支裏面的方法,同時也能響應其餘不相干類發過來的消息。在上圖中Warrior和Diplomat沒有繼承關係,可是Warrior將negotiate消息轉發給了Diplomat後,就好似Diplomat是Warrior的超類同樣。算法

消息轉發提供了許多相似於多繼承的特性,可是他們之間有一個很大的不一樣: 編程

多繼承:合併了不一樣的行爲特徵在一個單獨的對象中,會獲得一個重量級多層面的對象。 vim

消息轉發:將各個功能分散到不一樣的對象中,獲得的一些輕量級的對象,這些對象經過消息經過消息轉發聯合起來。數組

這裏值得說明的一點是,即便咱們利用轉發消息來實現了「假」繼承,可是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.這篇乾貨有點多,還好都寫完了。順利出院了!

相關文章
相關標籤/搜索