Objective-C Runtime 解析(三)—— swizzle method

在之前介紹Objective-C的方法消息機制時,我們知道Objective的方法調用是Runtime動態的在dispatch table中尋找方法實現的。
對於這個動態尋找的過程,我們可以進行“干涉”,從而實現Objective方法的調換或添加。
這裏我們介紹Objective的方法調換——swizzle method

swizzle method

在很多第三方庫中,我們經常能夠看到swizzle method方法的使用。多是用來爲系統類的方法進行改寫或修復某些iOS的bug。
下面是摘自AFNetWorking中的一段代碼,它的目的是在NSURLSessionTask中調換默認的resume和suspend方法,從而在用戶調用resume和suspend方法的時候,發送AFNSURLSessionTaskDidResumeNotification 與 AFNSURLSessionTaskDidSuspendNotification通知(這種監控sessionTask行爲的方式屬於AOP編程的思路,會在後面介紹AOP編程)。

@interface _AFURLSessionTaskSwizzling : NSObject

@end

@implementation _AFURLSessionTaskSwizzling

+ (void)load {
    if (NSClassFromString(@"NSURLSessionTask")) {
        NSURLSessionConfiguration *configuration = [NSURLSessionConfiguration ephemeralSessionConfiguration];
        NSURLSession * session = [NSURLSession sessionWithConfiguration:configuration];
        NSURLSessionDataTask *localDataTask = [session dataTaskWithURL:nil];
        IMP originalAFResumeIMP = method_getImplementation(class_getInstanceMethod([self class], @selector(af_resume)));
        Class currentClass = [localDataTask class];

        while (class_getInstanceMethod(currentClass, @selector(resume))) {
            Class superClass = [currentClass superclass];
            IMP classResumeIMP = method_getImplementation(class_getInstanceMethod(currentClass, @selector(resume)));
            IMP superclassResumeIMP = method_getImplementation(class_getInstanceMethod(superClass, @selector(resume)));
            if (classResumeIMP != superclassResumeIMP &&
                originalAFResumeIMP != classResumeIMP) {
                [self swizzleResumeAndSuspendMethodForClass:currentClass];
            }
            currentClass = [currentClass superclass];
        }

        [localDataTask cancel];
        [session finishTasksAndInvalidate];
    }
}

+ (void)swizzleResumeAndSuspendMethodForClass:(Class)theClass {
    Method afResumeMethod = class_getInstanceMethod(self, @selector(af_resume));
    Method afSuspendMethod = class_getInstanceMethod(self, @selector(af_suspend));

    if (af_addMethod(theClass, @selector(af_resume), afResumeMethod)) {
        af_swizzleSelector(theClass, @selector(resume), @selector(af_resume));
    }

    if (af_addMethod(theClass, @selector(af_suspend), afSuspendMethod)) {
        af_swizzleSelector(theClass, @selector(suspend), @selector(af_suspend));
    }
}

- (NSURLSessionTaskState)state {
    NSAssert(NO, @"State method should never be called in the actual dummy class");
    return NSURLSessionTaskStateCanceling;
}

- (void)af_resume {
    NSAssert([self respondsToSelector:@selector(state)], @"Does not respond to state");
    NSURLSessionTaskState state = [self state];
    [self af_resume];

    if (state != NSURLSessionTaskStateRunning) {
        [[NSNotificationCenter defaultCenter] postNotificationName:AFNSURLSessionTaskDidResumeNotification object:self];
    }
}

上面爲了調換掉NSURLSessionDataTask的resume和 suspend函數。它是在Class的load方法中調用的,load方法會在Runtime將所有類信息加載到內存中時被自動調用。同時,在AF的實現代碼裏面,還做了判斷:

if (NSClassFromString(@"NSURLSessionTask"))  { // 判斷NSURLSessionTask是否已經被Rumtime loaded
...
}

除了調換函數之外,還針對iOS的實現,做了許多額外操作,如獲取
NSURLSessionDataTask對象的真正Class類型(在iOS不同版本中, 返回的NSURLSessionDataTask對象可能並不是真正NSURLSessionDataTask類型)

Class currentClass = [localDataTask class];

同時會沿着currentClass的superclass向上進行resume和suspend函數的調換。
這裏,爲了學習swizzle method,我們把多餘代碼去掉,聚焦於resum和suspend函數是如何被調換掉的,僅有一行代碼:

[self swizzleResumeAndSuspendMethodForClass:currentClass];

該函數僅有一個參數:要被調換方法的Class(Class類型)

實現:

+ (void)swizzleResumeAndSuspendMethodForClass:(Class)theClass {
    Method afResumeMethod = class_getInstanceMethod(self, @selector(af_resume));
    Method afSuspendMethod = class_getInstanceMethod(self, @selector(af_suspend));

    if (af_addMethod(theClass, @selector(af_resume), afResumeMethod)) {
        af_swizzleSelector(theClass, @selector(resume), @selector(af_resume));
    }

    if (af_addMethod(theClass, @selector(af_suspend), afSuspendMethod)) {
        af_swizzleSelector(theClass, @selector(suspend), @selector(af_suspend));
    }
}

主要是調用了兩個af函數:

af_addMethod     // 將method添加到theClass中
af_swizzleSelector // 將theClass的methodA 與 self 的methodB調換

af_addMethod實現

static inline BOOL af_addMethod(Class theClass, SEL selector, Method method) {
    return class_addMethod(theClass, selector,  method_getImplementation(method),  method_getTypeEncoding(method));
}

很簡單,調用了Runtime函數class_addMethod, 用於將method添加到目標class中。
參數依次爲:
目標class、method 對應的SEL標識, method實現,method的encoding(即表示method返回值,接受參數的字符串)
如果addMethod成功,返回YES,否則返回NO(如果當前class中已經包含該method,同樣會返回NO)

af_swizzleSelector實現

static inline void af_swizzleSelector(Class theClass, SEL originalSelector, SEL swizzledSelector) {
    Method originalMethod = class_getInstanceMethod(theClass, originalSelector);
    Method swizzledMethod = class_getInstanceMethod(theClass, swizzledSelector);
    method_exchangeImplementations(originalMethod, swizzledMethod);
}

沒什麼好說的,調用Runtime函數method_exchangeImplementations,來交換original與swizzle 的實現。

swizzleResumeAndSuspendMethodForClass先利用AddMethod函數,將你自己的函數添加到類中。然後再將你新加入的方法與原有方法進行替換。

不過這裏有一點需要注意的是,
class_getInstanceMethod 如果當前類沒有實現該selector,而superclass實現的話,則會返回superclass的實現。這裏需要注意不要不小心替換掉superclass的實現。

當調換掉resume, suspend後,我們再看af_resume的實現:

- (void)af_resume {
    NSAssert([self respondsToSelector:@selector(state)], @"Does not respond to state");
    NSURLSessionTaskState state = [self state]; // 此時的self不是AFNetWorking, 而是NSSessionTask
    [self af_resume];  // 注意這裏不會照成遞歸調用

    if (state != NSURLSessionTaskStateRunning) {
        [[NSNotificationCenter defaultCenter] postNotificationName:AFNSURLSessionTaskDidResumeNotification object:self];
    }
}

我們需要理解的是:
(1) 此時af_resume已經與系統的resume做了替換,因此當af_resume被調用時,其實是系統的resume被調用,此時的self也應該是系統的task相關類對象。
(2)在af_resume中再調用[self af_resume], 這並不會造成循環遞歸調用。因爲af_resume已經與resume交換了實現,當我們在調用[self af_resume]的時候,實際上是調用的系統resume方法。

AOP編程

上面就是利用Runtime進行方法調換的步驟。
這裏,順便提一下,在Objective-C中,swizzle method是AOP編程的一種實踐。
AOP編程即Aspect Oriented Programming(面向切面編程)。
是什麼意思呢?在我們平日編程中,可能會遇到一些瑣碎的處理,他們與主要業務流程無關,卻不得不去處理,於是這些瑣碎的代碼就分散在主邏輯的各個角落,無法得到集中的管理。如日誌,用戶認證以及上面例子中監視resume,suspend方法等你必須做 又很繁瑣,但是不得不做的事情。這種情況被稱爲Cross Cutting Concerns。而面對這種狀況,業界也提出了一種解決方案——AOP編程(面向切面編程)

所謂面向切面,就是將我們的主業務流程的一些節點,切出,在這些切出的切面中,插入我們的那些瑣碎的代碼,然後在放回業務流程中。表面上看業務邏輯並沒有被打散,其實在哪些切面中,已經插入了我們自己的代碼。那麼對這些瑣碎代碼的管理,就擴大爲對於切面的管理,粒度變得大了,自然也就好維護了。

比如上面例子中,我們將resume,suspend切出,並向裏面插入了notification相關代碼,再放回主業務邏輯中。在表面看,業務邏輯代碼並沒有任何改變,也沒有插入任何瑣碎代碼,但是在業務執行時,卻能夠‘隱式’地自動發送消息。

AOP編程,Objective-C也有第三方AOP庫Aspects

上面僅僅是我個人對AOP編程一些片面的理解,自己連入門都算不上,不對的地方請大家指正。

參考資料

glowing團隊技術博客
(glowing團隊的目標是做中國的不孕不育市場,哈哈。技術牛人挺多,投資方,管理方也是硅谷出身,樂觀其成)。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章