AFNetworking的漂亮細節

寫在開頭

最近重讀了AFNetworking源碼,發現很多以前讀不懂,也不知道為啥這么寫的代碼慢慢讀懂了。過程中被AFNetworking作者的對細節,舒服,整潔的追求所折服。把一些個人覺得寫的漂亮的用法總結下來,本文不在于探討AFNetworking源碼的具體業余實現,盡量從代碼本身和設計角度進行總結(源碼解析推薦AFNetworking到底做了什么?這篇文章)。

1.Dispatch_once方法聲明C語言變量方法

感覺很像OC的property的getter方法,一種C語言懶加載的感覺,static修飾符意味只在該編譯單元可見(對應OC就是.m文件),配合單例,只會被執行一次。類似于if(!object)的感覺。

如下面例子創建了一個queue的方法,調用后返回是同一個變量。

static dispatch_queue_t url_session_manager_creation_queue() {
    static dispatch_queue_t af_url_session_manager_creation_queue;
    static dispatch_once_t onceToken;
    dispatch_once(&onceToken, ^{
        af_url_session_manager_creation_queue = dispatch_queue_create("com.alamofire.networking.session.manager.creation", DISPATCH_QUEUE_SERIAL);
    });

    return af_url_session_manager_creation_queue;
}

2.使用反射機制來確定KeyPath

在KVO中,我們一般會觀察通過一個屬性,而@property其實是一個語法糖,屬性=ivar(實例對象)+setter方法+getter方法。而getter方法名就是屬性d的名字。利用OC中的反射機制NSStringFromSelector方法獲取屬性的getter方法的字符串,其實就是屬性的KeyPath。這樣的KeyPath不容易寫錯,也容易跳轉去看屬性的定義。

 [progress addObserver:self
            forKeyPath:NSStringFromSelector(@selector(fractionCompleted))
                      options:NSKeyValueObservingOptionNew
                      context:NULL];

3.用__unused修飾符修飾不用的Delegate中的變量

一般協議delegate聲明時,會把delegate弱持有者作為第一個參數傳入delegate方法中,可是有時候delegate的實現者并不關心或不區分delegate對象是誰持有的。

- (void)URLSession:(__unused NSURLSession *)session
              task:(NSURLSessionTask *)task
didCompleteWithError:(NSError *)error{
    //......
}

4.避開Cocoa類族的坑

在Foudation框架中,某些類其實是類族,比如NSArray,生成的某一個NSArray對象實際上可能是NSArray的子類。所以用Method Swizzling的去Hook一些系統類方法的時候,要注意某些類實際上是子類,甚至不同系統版本繼承鏈和方法實現都不一樣(不一定調用了父類的同名方法)。在AFURLSessionManager中為了Hook系統的NSURLSessionTask的resume和suspend的方法實現中加上通知。由于NSURLSessionTask是一個類族,且iOS7和iOS8上Task類的繼承鏈不同,于是有了以下嚴謹的代碼。

if (NSClassFromString(@"NSURLSessionTask")) {
        NSURLSessionConfiguration *configuration = [NSURLSessionConfiguration ephemeralSessionConfiguration];
        NSURLSession * session = [NSURLSession sessionWithConfiguration:configuration];//先構建NSURLSession
#pragma GCC diagnostic push
#pragma GCC diagnostic ignored "-Wnonnull"
        NSURLSessionDataTask *localDataTask = [session dataTaskWithURL:nil];//再通過session對象構建一個task對象
#pragma clang diagnostic pop
        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];
}

5.增加方法到類中再進行Method Swizzling

同時為了避免直接換帶來多次交換把原來方法弄亂的問題,是先將需要換的方法add到需要替換類中(相當于生成一個副本),然后讓那個類里面的副本方法去交換,也就是不影響原來擁有這個方法的類里的方法。

+ (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));
    }
}

6.用GCD的同步方法來封裝代碼塊

iOS8以下dataTaskWithRequest是生成的task時是并發執行的,造成taskIdentifer偶發不唯一,解決辦法是使這個方法串行執行,同時用Dispatch_sync等待結果返回。調用時封裝了一個C方法。

 url_session_manager_create_task_safely(^{
        downloadTask = [self.session downloadTaskWithRequest:request];
  });
static dispatch_queue_t url_session_manager_creation_queue() {
    static dispatch_queue_t af_url_session_manager_creation_queue;
    static dispatch_once_t onceToken;
    //創建一個串行隊列,只創建一次
    dispatch_once(&onceToken, ^{
        af_url_session_manager_creation_queue = dispatch_queue_create("com.alamofire.networking.session.manager.creation", DISPATCH_QUEUE_SERIAL);
    });
    return af_url_session_manager_creation_queue;
}

static void url_session_manager_create_task_safely(dispatch_block_t block) {
    if (NSFoundationVersionNumber < NSFoundationVersionNumber_With_Fixed_5871104061079552_bug) {
        //iOS8以下則調用,同步等待結果
        dispatch_sync(url_session_manager_creation_queue(), block);
    } else {
        block();
    }
}

7.重寫respondsToSelector更改Delegate實現的判斷依據

AFNetworking內部的AFURLSessionManager將所有NSURLSessionDelegate的方法都接管了并轉換成外界Set進來的Block實現,其中有一些轉換并沒有做任何處理,單純轉換成Block。所以是否響應這個Delegate方法其實是block是否存在,于是內部就重寫了respondsToSelector。

- (BOOL)respondsToSelector:(SEL)selector {
    if (selector == @selector(URLSession:task:willPerformHTTPRedirection:newRequest:completionHandler:)) {
        return self.taskWillPerformHTTPRedirection != nil;
    } else if (selector == @selector(URLSession:dataTask:didReceiveResponse:completionHandler:)) {
        return self.dataTaskDidReceiveResponse != nil;
    } else if (selector == @selector(URLSession:dataTask:willCacheResponse:completionHandler:)) {
        return self.dataTaskWillCacheResponse != nil;
    } else if (selector == @selector(URLSessionDidFinishEventsForBackgroundURLSession:)) {
        return self.didFinishEventsForBackgroundURLSession != nil;
    }

    return [[self class] instancesRespondToSelector:selector];
}

8.@dynamic關鍵字實現默認調用父類setter&getter

當子類重新聲明一個父類的屬性時,其實默認合成了setter&getter并覆蓋了父類的默認實現。對于想子類聲明屬性卻希望默認調用父類屬性的setter&getter,可以用@dynamic關鍵字。

AFHTTPSessionManger是AFURLSessionManger的子類,也聲明了securityPolicy屬性并重寫其setter方法,但希望getter方法調用父類的。

@dynamic securityPolicy;

- (void)setSecurityPolicy:(AFSecurityPolicy *)securityPolicy {
    if (securityPolicy.SSLPinningMode != AFSSLPinningModeNone && ![self.baseURL.scheme isEqualToString:@"https"]) {
        NSString *pinningMode = @"Unknown Pinning Mode";
        switch (securityPolicy.SSLPinningMode) {
            case AFSSLPinningModeNone:        pinningMode = @"AFSSLPinningModeNone"; break;
            case AFSSLPinningModeCertificate: pinningMode = @"AFSSLPinningModeCertificate"; break;
            case AFSSLPinningModePublicKey:   pinningMode = @"AFSSLPinningModePublicKey"; break;
        }
        NSString *reason = [NSString stringWithFormat:@"A security policy configured with `%@` can only be applied on a manager with a secure base URL (i.e. https)", pinningMode];
        @throw [NSException exceptionWithName:@"Invalid Security Policy" reason:reason userInfo:nil];
    }
    [super setSecurityPolicy:securityPolicy];
}

9.KVO用Context區分父類與子類

KVO中的Api- (void)addObserver:(NSObject *)observer forKeyPath:(NSString *)keyPath options:(NSKeyValueObservingOptions)options context:(nullable void *)context總是在dealloc方法中配合- (void)removeObserver:(NSObject *)observer forKeyPath:(NSString *)keyPath context:(nullable void *)context成對使用來移除觀察者。但如果父類和子類都同時觀察一個keyPath,那么容易導致addObserver和removeObserver的個數不匹配(子類未調用父類的addObserver方法但調用了父類的dealloc),導致重復調用remove同一個keyPath而Crash。所以加上context作為類別唯一標識才是比較安全的做法。

- (void)dealloc {
    for (NSString *keyPath in AFHTTPRequestSerializerObservedKeyPaths()) {
        if ([self respondsToSelector:NSSelectorFromString(keyPath)]) {
            [self removeObserver:self forKeyPath:keyPath context:AFHTTPRequestSerializerObserverContext];
        }
    }
}

10.用GCD實現同步屬性

實現某個屬性值setter方法和getter方法的同步除了用NSLock或者@synchronized關鍵字加鎖外,可以使用在并發隊列里setter配合dispatch_barrier_async加上getter配合dispatch_sync實現。這樣讀取是同步并發的,寫入是在沒有讀取都完成后,同步執行的,并且性能比加鎖更好。AFHTTPRequestSerializer里的HTTPHeaderField就是這樣的。

- (void)setValue:(NSString *)value
forHTTPHeaderField:(NSString *)field
{
    dispatch_barrier_async(self.requestHeaderModificationQueue, ^{
        [self.mutableHTTPRequestHeaders setValue:value forKey:field];
    });
}

- (NSString *)valueForHTTPHeaderField:(NSString *)field {
    NSString __block *value;
    dispatch_sync(self.requestHeaderModificationQueue, ^{
        value = [self.mutableHTTPRequestHeaders valueForKey:field];
    });
    return value;
}

11.對NSStream類的操作

AFHTTPRequestSerizlizer進行Multipart協議支持時,使用了NSStream對象。

一般使用方法是

//創建流對象
NSInputStream *stream = [[NSInputStream alloc] initWithData:[NSData data]];

//放入Runloop中
[stream scheduleInRunLoop:NSRunLoop.currentRunLoop forMode:NSDefaultRunLoopMode];

//打開流
[stream open];

//.......

//從Runloop中移除
[stream removeFromRunLoop:NSRunLoop.currentRunLoop forMode:NSDefaultRunLoopMode];

//關閉流
[stream close];

結果發現AFNetworking在析構NSStream時,沒有調用removeFromRunLoop,僅僅調用了close,我一開始還以為漏了,結果后來書寫Demo驗證發現其引用計數的變化時如下的。

//放入Runloop中-----引用計數+2
[stream scheduleInRunLoop:NSRunLoop.currentRunLoop forMode:NSDefaultRunLoopMode];

//打開流-----引用計數不變
[stream open];

//.......

//從Runloop中移除-----引用計數-2
[stream removeFromRunLoop:NSRunLoop.currentRunLoop forMode:NSDefaultRunLoopMode];

//關閉流-----引用計數-2
[stream close];

也就是說open和close對引用計數的影響不是一對的,從一定程度上解釋只調用close也可以達到removeFromRunLoop的原因,個人猜測close和removeFromRunLoop調用任意一個都可以。順便提一句AFMultipartBodyStream還對NSStreamStauts一些只讀屬性改成讀寫的,并自定義了所有流的方法。

12.Category中實現屬性懶加載

使用了OC的關聯對象,先獲取判斷是否為空,不然就生成并關聯上。

- (AFRefreshControlNotificationObserver *)af_notificationObserver {
    AFRefreshControlNotificationObserver *notificationObserver = objc_getAssociatedObject(self, @selector(af_notificationObserver));
    if (notificationObserver == nil) {
        notificationObserver = [[AFRefreshControlNotificationObserver alloc] initWithActivityRefreshControl:self];
        objc_setAssociatedObject(self, @selector(af_notificationObserver), notificationObserver, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
    }
    return notificationObserver;
}

13.頭文件明確聲明Nonull和Nullable

自從支持Swift后,和Swift中的?和!對應,OC引入了Nonull和Nullable關鍵字,當然對每個方法參數和屬性聲明關鍵字是很大工作量的,這時我們可以用一對系統宏包含在最前和最后,中間的默認關鍵字就是Nonull了,這時候針對Nullable的參數或屬性進行補充就可以了。

//.h
NS_ASSUME_NONNULL_BEGIN

//...
@property (nonatomic, strong, nullable) id <AFImageRequestCache> imageCache;
//...

NS_ASSUME_NONNULL_END

最后

AFNetworking是一份寫得十分嚴謹漂亮的源碼,其中對KVO&KVC,GCD,Block,關聯對象的運用十分巧妙且準確,同時接口的封裝,代碼的劃分也很恰當。閱讀之后對于代碼規范又有了新的理解。

有任何問題歡迎評論私信
QQ:757765420
Email:nemocdz@gmail.com
Github:Nemocdz
微博:@Nemocdz

謝謝觀看

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 230,247評論 6 543
  • 序言:濱河連續發生了三起死亡事件,死亡現場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發現死者居然都...
    沈念sama閱讀 99,520評論 3 429
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 178,362評論 0 383
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 63,805評論 1 317
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 72,541評論 6 412
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發上,一...
    開封第一講書人閱讀 55,896評論 1 328
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,887評論 3 447
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 43,062評論 0 290
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當地人在樹林里發現了一具尸體,經...
    沈念sama閱讀 49,608評論 1 336
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 41,356評論 3 358
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發現自己被綠了。 大學時的朋友給我發了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 43,555評論 1 374
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 39,077評論 5 364
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發生泄漏。R本人自食惡果不足惜,卻給世界環境...
    茶點故事閱讀 44,769評論 3 349
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 35,175評論 0 28
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 36,489評論 1 295
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 52,289評論 3 400
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 48,516評論 2 379

推薦閱讀更多精彩內容

  • 面試題參考1 : 面試題[http://www.cocoachina.com/ios/20150803/12872...
    江河_ios閱讀 1,755評論 0 4
  • *面試心聲:其實這些題本人都沒怎么背,但是在上海 兩周半 面了大約10家 收到差不多3個offer,總結起來就是把...
    Dove_iOS閱讀 27,197評論 30 471
  • 按:春天來了,你發現了嗎?把你感受到的春天寫下來吧。不超過140字。 張天焱: 當一股清新的,熟悉的,久違的氣息鉆...
    簡約語文閱讀 4,203評論 5 20
  • 影片講述的是一位摔跤出身的父親訓練兩位女兒成為摔跤手,成為全國冠軍,又代表印度參賽成為世界冠軍的事……故...
    門扉孟晶晶閱讀 162評論 1 1
  • 很多事 我不問+你不說=這就是距離 我問了+你不說=這就是隔閡 我問了+你說了=這就是尊重 你想說+我想問=這就是...
    ec0dd6eefd56閱讀 222評論 0 1