【原】AFNetworking源碼閱讀(一)


【原】AFNetworking源碼閱讀(一)

本文轉載請注明出處 —— polobymulberry-博客園

1. 前言


AFNetworking版本:3.0.4

由於我平常並沒有經常使用AFNetworking的經歷,所以這次閱讀AFNetworking源代碼,我想回到最原點,從AFNetworking提供的iOS Example開始閱讀。至於閱讀的方式,和閱讀SDWebImage一樣,逐字逐句地去扣。我不是很聰明,所以就用這種蠢辦法吧,O(∩_∩)O哈哈~

新增:准備給自己加點難度,把AFNetworking對應的Tests部分也看了!

iOS Example的代碼其實很規范,值得學習。這里是我的感悟:我覺得熟悉業務,再看代碼才是正確的姿勢。不管什么源碼,我一般都會先了解代碼是用來做什么的,怎么用的,也就是它的業務邏輯。

2. iOS Example代碼結構


QQ20151228-4@2x

上面這個圖只是簡單地羅列了一下該example的架構。還沒有深入研究具體的邏輯。我們還是按照代碼順序一步一步往下看。

2.1 AppDelegate

此文件主要就是實現函數didFinishLaunchingWithOptions。將windows的rootViewController設置為rootViewController為GlobaltimelineViewController的NavigationController。此處有兩點需要注意一下:

  • 第一處
NSURLCache *URLCache = [[NSURLCache alloc] initWithMemoryCapacity:4 * 1024 * 1024 diskCapacity:20 * 1024 * 1024 diskPath:nil];
[NSURLCache setSharedURLCache:URLCache];

NSURLCache 為您的應用的 URL 請求提供了內存中(對應memoryCapacity)以及磁盤上(對應diskCapacity)的綜合緩存機制。所以你想使用NSURLCache帶來的好處,就需要在此處設置一個sharedURLCache。

  • 第二處
[[AFNetworkActivityIndicatorManager sharedManager] setEnabled:YES];

為了說明AFNetworkingActivityIndicator是什么,直接上圖:

QQ20160110-2@2x

當你有session task正在運行時,這個小菊花就會轉啊轉。這個是自動檢測的,只需要你設置AFNetworkingActivityIndicatorManager的sharedManager中的enabled設為YES即可。

這里我簡單看了下AFNetworkingActivityIndicatorManager,發現它對外接口不多,比較容易理解它的業務流程。所以我准備在第三部分就將AFNetworkingActivityIndicatorManager的源碼拿下。

設置完了cache和AFNetworkingActivityIndicator,接着就是進入GlobalTimelineViewController(UITableViewController)了。這里我學到一個,就是UITableViewController可以使用initWithStyle進行初始化

2.2 GlobalTimelineViewController

主要是圍繞UITableView的delegate和dataSource來說。

2.2.1 UITableViewDelegate

主要是計算heightForRowAtIndexPath這個函數比較麻煩,這里的Cell比較簡單,可以直接使用posts中存儲的text值來計算高度,核心代碼就下面這句:

CGRect rectToFit = [text boundingRectWithSize:CGSizeMake(240.0f, CGFLOAT_MAX) options:NSStringDrawingUsesLineFragmentOrigin attributes:@{NSFontAttributeName: [UIFont systemFontOfSize:12.0f]} context:nil];

對於boundingRectWithSize的使用又增進了一步。

2.2.2 UITableViewDataSource

主要是用posts作為數據源,而posts的獲取在此處尤為關鍵,是通過Post本身(model)的globalTimelinePostsWithBlock函數獲取數據的,這里作者將網絡端的請求放在了model里面。

接着調用了refreshControl控件的setRefreshingWithStateOfTask:。setRefreshingWithStateOfTask:其實是UIRefreshControl+AFNetworking的一個category中定義的。UIRefreshControl+AFNetworking的源碼很簡單,放在第四部分講。

注意setRefreshingWithStateOfTask:有一個參數就是NSURLSessionTask*。而這個NSURLSessionTask的獲取是調用了Post類中的globalTimelinePostsWithBlock:函數。

在globalTimelinePostsWithBlock:函數中其實封裝了一層AFHTTPSessionManager的GET函數

- (nullable NSURLSessionDataTask *)GET:(NSString *)URLString
                            parameters:(nullable id)parameters
                              progress:(nullable void (^)(NSProgress *downloadProgress)) downloadProgress
                               success:(nullable void (^)(NSURLSessionDataTask *task, id _Nullable responseObject))success
                               failure:(nullable void (^)(NSURLSessionDataTask * _Nullable task, NSError *error))failure;

具體細節后面討論,此處我們知道是根據一個url獲取到服務器端的數據即可。注意獲取到的數據是JSON格式的,這里作者在Post類,即Model中定義了一個JSON---->Model函數-initWithAttributes,,也就是說模型數據轉化部分也放在了model中。

另外,調用GET方法不是直接用AFHTTPSessionManager的manager,而是又定義了一個AFAppDotNetAPIClient,繼承自AFHTTPSessionManager。並在其定義的單例模式中簡單地封裝了一些AFHTTPSessionManager的設置。

+ (instancetype)sharedClient {
    static AFAppDotNetAPIClient *_sharedClient = nil;
    static dispatch_once_t onceToken;
    dispatch_once(&onceToken, ^{
        // 初始化HTTP Client的base url,此處為@"https://api.app.net/"
        _sharedClient = [[AFAppDotNetAPIClient alloc] initWithBaseURL:[NSURL URLWithString:AFAppDotNetAPIBaseURLString]];
        // 設置HTTP Client的安全策略為AFSSLPinningModeNone
        _sharedClient.securityPolicy = [AFSecurityPolicy policyWithPinningMode:AFSSLPinningModeNone];
    });
    
    return _sharedClient;
}

知識點:SSL Pinning

Https對比Http已經很安全,但在建立安全鏈接的過程中,可能遭受中間人攻擊。防御這種類型攻擊的最直接方式是Client使用者能正確鑒定Server發的證書【目前很多瀏覽器在這方面做的足夠好,用戶只要不在遇到警告時還繼續其中的危險操作】,而對於Client的開發者而言,一種方式保持一個可信的根證書頒發機構列表,確認可信的證書,警告或阻止不是可信根證書頒發機構頒發的證書。

SSL Pinning其實就是證書綁定,一般瀏覽器的做法是信任可信根證書頒發機構頒發的證書,但在移動端【非瀏覽器的桌面應用亦如此】,應用只和少數的幾個Server有交互,所以可以做得更極致點,直接就在應用內保留需要使用的具體Server的證書。對於iOS開發者而言,如果使用AFNetwoking作為網絡庫,那么要做到這點就很方便,直接證書作為資源打包進去就好,AFNetworking會自動加載,具體代碼就不貼了,nsscreencast已經有很好的tutorial


至於model根據網絡層獲取的數據賦值,除了user的頭像那塊比較難,因為涉及到UIImageView+AFNetworking等文件,其他部分很簡單。而AFNetworking的UIImageView+AFNetworking的部分其實很類似SDWebImage的思路。

3.AFNetworkActivityIndicatorManager


上面簡單地說了下這個類的作用。如果要我去實現這個類,面臨的兩個問題就是:

  1. 1.如何在status bar上顯示那個小菊花。
  2. 2.如何判斷什么時候顯示這個小菊花,也就是怎么判斷session task的開始和結束。

3.1 問題一:如何顯示小菊花?

我搜尋了一下代碼,發現顯示方式很簡單,是系統自帶的。就一行代碼:

[[UIApplication sharedApplication] setNetworkActivityIndicatorVisible:networkActivityIndicatorVisible];

但關鍵作為一個這么牛逼的庫,肯定不能就這么簡單就把菊花漏出來了。對了!它還允許用戶自定義處理(用戶需要自己定義networkActivityActionBlock)。見代碼(AFNetworkActivityIndicatorManager.m下的setNetworkActivityIndicatorVisible:函數):

if (self.networkActivityActionBlock) {
    self.networkActivityActionBlock(networkActivityIndicatorVisible);
} else {
    [[UIApplication sharedApplication] setNetworkActivityIndicatorVisible:networkActivityIndicatorVisible];
}

3.2 問題二:什么時候顯示與隱藏小菊花?

這個算是比較困難的問題。首先你得涉及到狀態的處理和轉移(處理就是指遇到這個狀態我應該做什么,轉移表示的是如何進行狀態轉移的)。縱觀全局,發現獲取和維護都是使用了currentState這個屬性。這個currentState是一個AFNetworkActivityManagerState類型的屬性,何為AFNetworkActivityManagerState:

typedef NS_ENUM(NSInteger, AFNetworkActivityManagerState) {
    AFNetworkActivityManagerStateNotActive,
    AFNetworkActivityManagerStateDelayingStart,
    AFNetworkActivityManagerStateActive,
    AFNetworkActivityManagerStateDelayingEnd
};

我們從中大概也可以看出AFNetworkActivityIndicatorManager所需要處理的狀態就這四種。NotActive和Active我清楚,就是判斷當前有沒有session task,但是DelayingStart和DelayingEnd是什么?不着急,先看看這些狀態用來干啥的?

3.2.1 狀態的處理

我們先搜索currentState。發現setCurrentState:函數集中了狀態的處理過程

整個函數是包含在@synchronized中,使用self作為鎖的唯一標識。主要是擔心多個網絡線程同時修改currentState。接着就是判斷currentState是否有變化,如果變了,就執行if語句中的函數。這里有一個貌似配對的函數willChangeValueForKey:和didChangeValueForKey:。


知識點:手動通知

KVO中有兩種通知Observer的方式,自動通知和手動通知。自動通知顧名思義就是只要值變化了,就自動通知觀察者。

  • 但是有時候我們有些地方的值變化了,並不想通知觀察者亦或不想立即通知觀察者,或者
  • 此處雖然值還沒變,但是我也想通知觀察者,那么就可以使用手動通知,在你想發送給觀察者消息的地方,加上willChangeValueForKey和didChangeValueForKey。

說白了只要加上這兩句話,就會通知觀察者,不管是不是值變化了(親測值沒變化也有效)。不過,在此之前最好是把自動通知關掉,可以利用automaticallyNotifiesObserversForKey:來返回NO,達到關閉自動通知的功能(當然,開着也行,那么自動通知和手動通知會揉在一起,執行起來很亂)。


跟着就是判斷currentState,並作出相應處理了:

  • AFNetworkActivityManagerStateNotActive

一上來就出現了cancelActivationDelayTimer和cancelActivationDelayTimer兩個函數。看懂這兩個函數不難,直接查找startActiviationDelayTimer和startCompletionDelayTimer兩個函數,看看我們的activationDelayTimer和completionDelayTimer是做什么的即可。我們發現這兩個函數都是定義了一個計時器。具體看代碼:

- (void)startActivationDelayTimer {
    // 定義了一個名為activationDelayTimer的定時器,定時器的時間為self.activationDelay。
    // 執行完定時器后,執行activationDelayTimerFired函數
    self.activationDelayTimer = [NSTimer
                                 timerWithTimeInterval:self.activationDelay target:self selector:@selector(activationDelayTimerFired) userInfo:nil repeats:NO];
    // 將該定時器添加到RunLoop里面執行
    [[NSRunLoop mainRunLoop] addTimer:self.activationDelayTimer forMode:NSRunLoopCommonModes];
}

至於startCompletionDelayTimer類似,此處直接放出源碼,具體幾個細節后面詳解:

- (void)startCompletionDelayTimer {
    [self.completionDelayTimer invalidate];
    self.completionDelayTimer = [NSTimer timerWithTimeInterval:self.completionDelay target:self selector:@selector(completionDelayTimerFired) userInfo:nil repeats:NO];
    [[NSRunLoop mainRunLoop] addTimer:self.completionDelayTimer forMode:NSRunLoopCommonModes];
}

注意這里添加計時器的時候,使用的Mode是NSRunLoopCommonModes,表示不管RunLoop出於什么狀態,都執行這個計時器任務(因為如果不指定這個mode的話,UI操作會阻塞計時器任務)。

不過現在關鍵是完全不知道這兩個delay是干啥的?找了一會,終於在activationDelay和completionDelay的注釋中找到了答案,恍然大悟,整個小菊花存在的時間是這樣的:

QQ20160111-2

不禁要問,既然session task已經開始了,為什么不直接使用Active作為狀態,還要搞出一個activationDelay,這是因為Apple的HIG(Human Interface Guidelines)說有些session task時間太短了,有可能用戶還沒意識到session task的進行,就已經結束了,就沒必要搞個菊花在上面轉啊轉的(這個用戶的意識盲區在此處默認設定為1秒,即activationDelay)。至於completionDelay,是因為如果有多個session task正在進行,前一個task結束之后,不一會(這個不一會的時間,默認是0.17秒,可能利用了大數據分析出來的(鬼知道怎么測出來了),也就是completionDelay)另一個task就開始,此處認為這個間隙沒必要停止菊花轉。

至於下面三個state感覺就沒必要講了。

  • AFNetworkActivityManagerStateDelayingStart
  • AFNetworkActivityManagerStateActive
  • AFNetworkActivityManagerStateDelayingEnd

3.2.2 狀態的轉移

牛逼的代碼就是不一樣,狀態都這么多…沒辦法,只好全局搜索,發現了這個函數----updateCurrentStateForNetworkActivityChange,我大致看了下,覺得所有狀態變化應該就寫在這了:

- (void)updateCurrentStateForNetworkActivityChange {
    if (self.enabled) {
        switch (self.currentState) {
            case AFNetworkActivityManagerStateNotActive:
                if (self.isNetworkActivityOccurring) {
                    [self setCurrentState:AFNetworkActivityManagerStateDelayingStart];
                }
                break;
            case AFNetworkActivityManagerStateDelayingStart:
                //No op. Let the delay timer finish out.
                break;
            case AFNetworkActivityManagerStateActive:
                if (!self.isNetworkActivityOccurring) {
                    [self setCurrentState:AFNetworkActivityManagerStateDelayingEnd];
                }
                break;
            case AFNetworkActivityManagerStateDelayingEnd:
                if (self.isNetworkActivityOccurring) {
                    [self setCurrentState:AFNetworkActivityManagerStateActive];
                }
                break;
        }
    }
}

結合上面那個圖,大概轉移關系也是可以理解的。

不過在狀態轉移過程中,有一個屬性很重要,叫做isNetworkActivityOccurring。這個其實是最真實的記錄session task起始的狀態。不過這個屬性是根據activityCount來決定的:

- (BOOL)isNetworkActivityOccurring {
    @synchronized(self) {
        return self.activityCount > 0;
    }
}

那什么是activityCount?我們發現activityCount的增減是通過incrementActivityCountdecrementActivityCount兩個函數進行的。這兩個函數也是使用了手動KVO的形式,具體實現很簡單,此處就不贅述了。我們再看在networkRequestDidStart函數中調用了incrementActivityCount,在networkRequestDidFinish調用了decrementActivityCount。而這兩個networkRequestDid*函數也是使用了KVO。具體這兩個函數什么時候執行,已經超出了第一篇文章要研究的范圍了。我們大概從他們的名字可以猜出networkRequest開始的時候activityCount++,networkRequest結束的時候activityCount—。

4. UIRefreshControl+AFNetworking

該文件只暴露了一個公共函數----setRefreshingWithStateOfTask:。那我們就進入這個函數好好看看。

- (void)setRefreshingWithStateOfTask:(NSURLSessionTask *)task {
    [[self af_notificationObserver] setRefreshingWithStateOfTask:task];
}

這里只有兩個函數:af_notificationObserver和setRefreshingWithStateOfTask:(注意此處的setRefreshingWithStateOfTask不是UIRefreshControl的category里面的那個函數)。下面細說:

4.1 af_notificationObserver

返回的是一個AFRefreshControlNotificationObserver的變量。主要是為了使用AFRefreshControlNotificationObserver中的setRefreshingWithStateOfTask。

可能會有人問,為什么不直接在UIRefreshControl的category添加這個notificationObserver?因為categor是不允許添加實例變量,注意是不能添加實例變量,而不是屬性成員(具體參見類別不是應該只能添加方法嗎?類別現在能直接添加屬性了?。我這里簡要概括一下原因:

因為category是運行期決定的,所以當你在此處添加實例變量的話,那么這個類的內存空間就要變了,而類的內存空間是在編譯期就確定了。這里你可以添加屬性成員,但是得自己實現getter和setter方法。但是此處作者使用的是objc_getAssociatedObject和objc_setAssociatedObject方法來綁定一個實例變量。我們來看代碼:

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

並沒有什么難點。還有一個沒講的地方就是initWithActivityRefreshControl函數:

- (instancetype)initWithActivityRefreshControl:(UIRefreshControl *)refreshControl
{
    self = [super init];
    if (self) {
        // 僅僅把refreshControl傳過來,后續要調用refreshControl本身的幾個方法
        // 所以注意此處refreshControl定義的是weak屬性
        _refreshControl = refreshControl;
    }
    return self;
}

4.2 setRefreshingWithStateOfTask

獲取到了AFRefreshControlNotificationObserver的一個變量,就可以調用setRefreshingWithStateOfTask

這里面做的事情也比較簡單,就是

  • ①先將之前的observer移除
  • ②如果當前task狀態為NSURLSessionTaskStateRunning,先beginRefreshing,然后添加幾個observer:
  1. 1. AFNetworkingTaskDidResumeNotification(任務繼續,所以調用beginRefreshing)
  2. 2. AFNetworkingTaskDidCompleteNotification(任務完成,所以調用endRefreshing)
  3. 3. AFNetworkingTaskDidSuspendNotification(任務掛起,所以調用endRefreshing)。有了這幾個observer,就可以實時更新refreshControl的狀態。
  • ③如果task不為NSURLSessionTaskStateRunning,也就是當前任務不是正在運行,就調用endRefreshing。

基本上這個category就講完了。這里還想說一下,作者在dealloc調用了removeObserver方法,這個細節。有時候,我會忘記。

5. AFNetworkActivityManagerTests+AFUIRefreshControlTests


說好的要學習XCTest,那么就從這開始吧。我原先是打算把Test部分單獨拉出來寫一篇的。但是覺得實際工作中不太可能寫完所有代碼,才寫測試(沒工作過,大神輕拍)。所以我覺得還是講完一部分,接着講Test會好一點,不容易忘了前面。

5.1 AFNetworking的XCTest前言

此處會對AFNetworking的XCTest做一個簡單介紹。

考慮到代碼的重用性,AFNetworking的所有測試用例類都有一個共同的父類,也就是AFTestCase。它也是XCTestCase的子類,所有測試類都是AFTestCase類的子類。

然后我們把一些公共的輔助方法放在AFTestCase類中,並且加了一些屬性作為每個測試的預置屬性。

我們具體看看AFTestCase有哪些公共的輔助方法:

@interface AFTestCase : XCTestCase

@property (nonatomic, strong, readonly) NSURL *baseURL;
@property (nonatomic, assign) NSTimeInterval networkTimeout;

- (void)waitForExpectationsWithCommonTimeoutUsingHandler:(XCWaitCompletionHandler)handler;

@end

baseURL就不用說了,這里默認是AFNetworkingTestsBaseURLString = @https://httpbin.org/。這個網站非常有名,是一個http庫測試工具。你可以打開網站,一看就明白了。此處作者所有的網絡測試都是基於這個網站的。

至於networkTimeout和waitForExpectationsWithCommonTimeoutUsingHandler還是需要好好說一下,尤其對我這種剛接觸XCTest的人。


知識點:Xcode的異步測試

就拿AFNetworking舉例,大部分網絡請求都是異步操作。也就存在這樣一個問題,網絡請求的進程是區別於主進程的。那么如何在主進程中獲取到網絡請求成功還是失敗的信息就是一個大問題。其實這種問題在日常編碼中經常遇到,就是網絡線程與主線程同步問題。具體可以看看Xcode 6異步測試,講的挺詳細。我這里就概括一下。

首先創建一個XCTestExpectation的變量expectation,如果網絡請求成功了,那么就在網絡請求的completedBlock中調用expectation的fulfill。相當於告訴系統,這個expectation完成了,你后面的那個waitForExpectationsWithTimeout:handler:方法就不要捕獲不到這個expectation了。所以說,你要是在waitForExpectationsWithTimeout函數捕獲到了expectation,那就出問題了。

當然,還有一種情況就是網絡請求超過一定時間了(networkTimeout),那么也認為出問題了,waitForExpectationsWithTimeout也會處理這個錯誤。


下面具體看看每個模塊的Test具體實現。我感覺相對於具體實現,測試的目的才是最重要的

5.2 AFNetworkActivityManagerTests

主要測試那個小菊花的功能。

5.2.1 AFNetworkActivityManagerTests准備工作

首先我們得實例化兩個變量,一個就是AFNetworkActivityIndicatorManager的networkActivityIndicatorManager,另一個就是AFHTTPSessionManager的sessionManager。這里用后者來進行網絡請求,從而測試前者的功能。

不可避免的,首先得實現兩個函數-setUp和tearDown。首先在setUp中進行networkActivityIndicatorManager和sessionManager的初始化與設置,然后在tearDown中釋放資源並取消session任務。很正規的寫法,第一次學習XCTest,值得借鑒。

剩下就是核心的測試代碼,並不難。這里先暫停一下,我想如果我是作者的話,可能會測試哪些功能?

  • 網絡請求失敗和成功的情況
  • 多個網絡請求的情況
  • 對於小菊花來說,那兩個delayTime也需要測試

其實作者也就測試了上面這幾個情況。這里大致介紹下函數用途和實現思路:

  • testThatNetworkActivityIndicatorTurnsOnAndOffIndicatorWhenRequestSucceeds
  • testThatNetworkActivityIndicatorTurnsOnAndOffIndicatorWhenRequestFails

這兩個我覺得放在一起講比較合適。上面那個是測試請求成功的情況,下面是那個是測試請求失敗的情況。

不管請求失敗還是成功,必不可少都會經歷請求的開始和結束,對應到小菊花狀態就是開始轉與不轉。所以要設置兩個XCTestExpectation,一個就是startExpectation,另一個就是endExpectation。注意這兩個的fullfil是放在setNetworkingActivityActionWithBlock中的,而AFNetworking本身框架中的setNetworkingActivityActionWithBlock默認為空,此處自定義setNetworkingActivityActionWithBlock,是為了達到測試的目的。也就是說,此處startExpectation代表小菊花在網絡請求開始的時候轉了,endExpectation代表小菊花在請求結束的時候停止轉了。這里我說的很羅嗦,之所以寫了很多,是因為在沒使用XCTest之前,大多時候,我是看模擬器的運行顯示的情況來判斷的。但是這樣測試不精准,沒有說服力。而這里使用XCTestExpectation做了一個等價替換,顯得很專業,讓我對XCTestExpectation的作用又理解深了一點。

到此為止,請求失敗和請求成功並沒有什么不同。它們的關鍵不同在於請求成功的那個測試函數請求了httpbin中的/delay/1(代表延遲一秒給你返回請求成功的消息),請求失敗的那個測試函數請求了httpbin的/status/404,而這必然會導致請求失敗!另外在成功和失敗的block處還要加上對應的requestExpectation來檢測是否確實請求成功或者失敗了。

這里有一個小細節。就是在測試請求情況的時候,我們的activationDelay和completionDelay是設置為0的。這個我覺得很細節,但是很重要,不會因為這兩個delay的功能有問題而導致網絡請求的測試失敗。

  • testThatVisibilityDelaysAreApplied

測試delaytime是否可用。

說實話,我一開始也沒想到怎么去測試這個delaytime。這里直接說作者的思路。首先定義四個變量,分別記錄請求的開始時間(requestStartTime),請求的結束時間(requestEndTime),小菊花的顯示開始時間(indicatorVisbleTime),小菊花的顯示結束時間(indicatorHiddenTime),不過這些時間的獲取都是利用了CACurrentMediaTime來獲取的。然后使用(indicatorVisbleTime - requestStartTime),表示的就是請求開始到小菊花顯示的時長,也就等價於activationDelay,而(indicatorHiddenTime - requestEndTime)表示的就是請求結束到小菊花結束顯示的時長,也就等價於completionDelay。

  • testThatIndicatorBlockIsOnlyCalledOnceEachForStartAndEndForMultipleRequests

這個函數是用來測試同時開始的兩個網絡請求,但是請求結束的時間不同的情況。想要這樣做就得在請求時長上下功夫。這里作者分別請求httpbin的/delay/4/delay/2達到這個目的。

5.2.2 AFUIRefreshControlTests

這個測試是為了測試UIRefreshControl的那個category,其中難點是在於測什么?

我們知道AFNetworking的UIRefreshControl需要添加三個notification的狀態,其notificationName分別為AFNetworkingTaskDidResumeNotification、AFNetworkingTaskDidCompleteNotification和AFNetworkingTaskDidSuspendNotification。這里測試的話,可以使用XCTest的expectationForNotification函數來測試對應的notificationName(也就是說測試此處的NotificationName在test過程中有沒有響應,關於expectationForNotification函數的作用是我親測后的猜測,expectationForNotification資料太少,沒查到特別權威的)。

下面看測試函數:

  • testTaskDidResumeNotificationDoesNotCauseCrashForUIRCWithTask (注:UIRC表示UIRefreshControl)
  • testTaskDidCompleteNotificationDoesNotCauseCrashForUIRCWithTask
  • testTaskDidSuspendNotificationDoesNotCauseCrashForUIRCWithTask

這三個test函數放在一起說,它們的大概步驟都是一樣的。對於每個函數,使用expectationForNotification來測試自己對應的notificationName是不是響應了。

  • 調用task resume,會post AFNetworkingTaskDidResumeNotification
  • 調用task結束后,會post AFNetworkingTaskDidCompleteNotification
  • 調用task suspend,會post AFNetworkingTaskDidSuspendNotification

另外,在測試AFNetworkingTaskDidCompleteNotification和AFNetworkingTaskDidSuspendNotification的時候,使用了dispatch_after。也就是要延遲一秒,才能一錘定音說這個expectation可以fulfill。這里作者在注釋中簡單說明了原因,因為有時候這個task會在notificationName發出之前就完成了,也就是說先執行了completionHandler中的代碼,結果沒檢驗完notificationName是否響應。

第一篇文章就結束了,后面會深入講解網絡部分

6. 參考文章


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM