• 【原】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. 参考文章

  • 相关阅读:
    Java分享笔记:关于Java反射机制
    Java分享笔记:自定义枚举类 & 使用enum关键字定义枚举类
    Java分享笔记:RandomAccessFile流 & 在文件指定位置插入内容
    Spark-源码-SparkContext的初始化
    Spark-源码-Spark-StartAll Master Worler启动流程
    Spark-源码-Spark-Submit 任务提交
    Hadoop2学习路程-HDFS
    JavaSE 第二次学习随笔(五)
    JavaSE 第二次学习随笔(四)
    JavaSE 第二次学习随笔(三)
  • 原文地址:https://www.cnblogs.com/polobymulberry/p/5081049.html
Copyright © 2020-2023  润新知