Source Code Learning - AFNetworking

NSURLSession 的最简单实用方法

NSURLSession 的不同之处在于,它把 NSURLConnection 替换为
NSURLSession
NSURLSessionConfiguration
NSURLSessionTask 子类:NSURLSessionDataTask、NSURLSessionUploadTask、NSURLSessionDownloadTask

NSURLSessionConfiguration *config = [NSURLSessionConfiguration defaultSessionConfiguration];

NSOperationQueue *queue = [NSOperationQueue mainQueue];
NSURLSession *session = [NSURLSession sessionWithConfiguration:config delegate:self delegateQueue:queue];

NSURLSessionDownloadTask *task = [session downloadTaskWithURL:[NSURL URLWithString:@"https://www.baidu.com"]];
[task resume];

可以用这个最简单的例子跟 AF 对比下,看看别人怎么写一个库

设置 NSURL 的几种方式

NSURL *baseURL = [NSURL URLWithString:@"http://example.com/v1/"];
// http://example.com/v1/foo
[NSURL URLWithString:@"foo" relativeToURL:baseURL];           
// http://example.com/v1/foo?bar=baz    
[NSURL URLWithString:@"foo?bar=baz" relativeToURL:baseURL];         

NSURLSession 和 NSURLConnection 的区别

与 NSURLConnection 相比,NSURLSession 最直接的改善就是提供了配置每个会话的缓存,协议,Cookie 和证书政策(Credential Policies),甚至跨应用程序共享它们的能力。

NSURLSessionConfiguration 的类型

+ (NSURLSessionConfiguration *)defaultSessionConfiguration;

返回标准配置,这实际上与 NSURLConnection 的网络协议栈是一样的,具有相同的共享 NSHTTPCookieStorage,共享 NSURLCache 和共享 NSURLCredentialStorage。

+ (NSURLSessionConfiguration *)ephemeralSessionConfiguration;

返回一个预设配置,没有持久性存储的缓存,Cookie或证书。这对于实现像“秘密浏览”功能的功能来说,是很理想的。

+ (NSURLSessionConfiguration *)backgroundSessionConfigurationWithIdentifier:(NSString *)identifier     NS_AVAILABLE(10_10, 8_0);

独特之处在于,它会创建一个后台会话。后台会话不同于常规的,普通的会话,它甚至可以在应用程序挂起,退出,崩溃的情况下运行上传和下载任务。初始化时指定的标识符,被用于向任何可能在进程外恢复后台传输的守护进程提供上下文。

线程保活

- (void)start {
    // NSRecursiveLock 递归锁的说明见 [GCD](http://alicialy.github.io/2016/06/24/Concurrent-Programming-and-Synchronization/)
    [self.lock lock];
    // ...
    [self performSelector:@selector(operationDidStart) onThread:[[self class] networkRequestThread] withObject:nil waitUntilDone:NO modes:[self.runLoopModes allObjects]];
    [self.lock unlock];
}

- (void)operationDidStart {
    [self.lock lock];
    if (![self isCancelled]) {
        self.connection = [[NSURLConnection alloc] initWithRequest:self.request delegate:self startImmediately:NO];
        // ...
    }
    [self.lock unlock];
    // ...
}

创建一个线程,在线程中创建 NSURLConnection,NSURLConnection 请求后的回调也会是在这个子线程中,如果不使用线程保活方式,线程结束就退出了,无法收到回调信息,线程保活则是在子线程中添加了一个 Runloop

+ (void)networkRequestThreadEntryPoint:(id)__unused object {
    @autoreleasepool {
        [[NSThread currentThread] setName:@"AFNetworking"];
        NSRunLoop *runLoop = [NSRunLoop currentRunLoop];
        [runLoop addPort:[NSMachPort port] forMode:NSDefaultRunLoopMode];
        [runLoop run];
    }
}

+ (NSThread *)networkRequestThread {
    static NSThread *_networkRequestThread = nil;
    static dispatch_once_t oncePredicate;
    dispatch_once(&oncePredicate, ^{
        _networkRequestThread = [[NSThread alloc] initWithTarget:self selector:@selector(networkRequestThreadEntryPoint:) object:nil];
        [_networkRequestThread start];
    });
    return _networkRequestThread;
}

如果每个请求就开一个线程,保活这个线程的话,开销太大,所以使用一个单例

KVO 的手动触发

- (void)setAllowsCellularAccess:(BOOL)allowsCellularAccess {
    [self willChangeValueForKey:NSStringFromSelector(@selector(allowsCellularAccess))];

    _allowsCellularAccess = allowsCellularAccess;

    [self didChangeValueForKey:NSStringFromSelector(@selector(allowsCellularAccess))];
}

KVO 手动触发和自动触发不是互斥的,如果需要控制某些变量手动触发,某些自动触发,需要重载 automaticallyNotifiesObserversForKey 方法

(BOOL)automaticallyNotifiesObserversForKey:(NSString *)theKey {
    BOOL automatic = NO;
    if ([theKey isEqualToString:@"balance"]) {
        automatic = NO;
    } else {
        automatic = [super automaticallyNotifiesObserversForKey:theKey];
    }
    return automatic;
}

可变参数

NS_REQUIRES_NIL_TERMINATION :是一个宏,用于编译时非 nil 结尾的检查

- (void)print:(NSString *)firstArg, ... NS_REQUIRES_NIL_TERMINATION {
    if (firstArg) {
        NSLog(@"%@", firstArg);

        va_list args;
        NSString *arg;
        va_start(args, firstArg);
        while ((arg = va_arg(args, NSString *))) {
            NSLog(@"%@", arg);
        }
        va_end(args);
    }
}

调用时要以 nil 作为结尾标志 [instance print:@”a”, @”b”, nil];

常量的定义

AF 中找不到任何宏定义的字符串、数字、数组,都是使用静态常量

数组常量的定义方式如下

static NSArray * AFHTTPRequestSerializerObservedKeyPaths() {
    static NSArray *_AFHTTPRequestSerializerObservedKeyPaths = nil;
    static dispatch_once_t onceToken;
    dispatch_once(&onceToken, ^{
        _AFHTTPRequestSerializerObservedKeyPaths = @[NSStringFromSelector(@selector(allowsCellularAccess)), NSStringFromSelector(@selector(cachePolicy)), NSStringFromSelector(@selector(HTTPShouldHandleCookies)), NSStringFromSelector(@selector(HTTPShouldUsePipelining)), NSStringFromSelector(@selector(networkServiceType)), NSStringFromSelector(@selector(timeoutInterval))];
    });
    return _AFHTTPRequestSerializerObservedKeyPaths;
}

字符串、数字等

static NSString * const kAFMultipartFormCRLF = @"\r\n";
NSUInteger const kAFUploadStream3GSuggestedPacketSize = 1024 * 16;

开闭原则

代码中随处可见的符合开闭原则的 readonly 属性设置,对外只读,对内可读写

// xxx.h
@interface AFHTTPRequestOperation : AFURLConnectionOperation
    @property (readonly, nonatomic, strong) id responseObject;
@end

// xxx.m
@interface AFHTTPRequestOperation ()
    @property (readwrite, nonatomic, strong) id responseObject;
@end

此外,利用 @dynamic 变量是只读的,则只保留读取方法

@property (readonly, nonatomic, getter = isNetworkActivityIndicatorVisible) BOOL networkActivityIndicatorVisible;

@dynamic networkActivityIndicatorVisible;

- (BOOL)isNetworkActivityIndicatorVisible {
    return self.activityCount > 0;
}

另外微信安装包的瘦身也提到过,改用 @dynamic 去掉类成员变量和类属性,安装包减少了2.5M。

断言

参数不能为空的地方不要忘了使用断言

NSParameterAssert(fileURL);

归档

在 iOS6 中,苹果引入了一个新的协议,是基于 NSCoding 的 NSSecureCoding。NSSecureCoding 在解码时要同时指定 key 和要解码的对象的类。能安全地写有关归档的代码,可以确保正在加载的数据文件是安全的。

Tips

  • 获取两个 float 数据的最大值 fmaxf(70, 100);
  • 三目运算 x ? : y,省略的是 x,等价于 x ? x : y。使用 self.completionQueue ?: dispatch_get_main_queue()
  • 常量定义尽量不使用宏,除非需要 ifdef 这种宏,使用静态变量定义数组,定义返回数组的静态函数见
    static NSArray * AFHTTPRequestSerializerObservedKeyPaths()

参考文献

忘记NSURLConnection,拥抱NSURLSession吧
KVO Manual Change Notification

请我喝汽水儿