在之前介紹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團隊的目標是做中國的不孕不育市場,哈哈。技術牛人挺多,投資方,管理方也是硅谷出身,樂觀其成)。