美文网首页
[iOS] NSConnection与NSSession

[iOS] NSConnection与NSSession

作者: 木小易Ying | 来源:发表于2019-10-20 09:58 被阅读0次

1. 原始NSData的dataWithContentsOfURL获取

NSURL *url = [NSURL URLWithString:@"http://b-ssl.duitang.com/uploads/item/201704/02/20170402193824_A3mnS.jpeg"];
NSData *data = [NSData dataWithContentsOfURL:url];
NSLog(@"continued: %@", data);
self.imageView.image = [UIImage imageWithData:data];

输出:
continued: <0xffd8ffe0 00104a4……

这种方式非常原始,同步并且没有办法取消,也没有进度,顶多给你个error,也木有post啥的header,适合轻量下载个图片啥的,但也不能在主线程搞,得抛回主线程设置图片。

2. NSConnection

(1) 同步获取

NSConnection至少还是提供通过设置request设置POST方法啥的滴:

#import "NetworkViewController.h"

@interface NetworkViewController ()

@end

@implementation NetworkViewController

- (void)viewDidLoad {
    [super viewDidLoad];
    
    [self startSyncConnection];
    
    NSLog(@"continue");
}

- (void)startSyncConnection {
    //1、创建一个URL
    NSURL *url = [NSURL URLWithString:@"https://www.baidu.com"];

    //2、创建请求(Request)对象(默认为GET请求);
    NSURLRequest *requst = [[NSURLRequest alloc]initWithURL:url];

    //3、发送请求
    /*
     第一个参数:请求对象
     第二个参数:响应头
     第三个参数:错误信息
     返回值:NSData类型,响应体信息
     */
    NSError *error = nil;
    NSURLResponse *response = nil;
    //发送同步请求(sendSynchronousRequest)
    NSData *data = [NSURLConnection sendSynchronousRequest:requst returningResponse:&response error:&error];
    
    NSLog(@"connection data:%@",[[NSString alloc]initWithData:data encoding:NSUTF8StringEncoding]);
    NSLog(@"connection error:%@",error);
}

@end

输出
Example1[1286:256676] connection data:<html>
<head>
    <script>
location.replace(location.href.replace("https://","http://"));
    </script>
</head>
<body>
    <noscript><meta http-equiv="refresh" content="0;url=http://www.baidu.com/"></noscript>
</body>
</html>
Example1[1286:256676] connection error:(null)
Example1[1286:256676] continue

可以看到在sendSynchronousRequest之后的代码都会等待connection返回以后再执行,所以这种方式其实不适合下载,只适合访问一些返回json格式的API,然后对数据解析以后显示。

另外其实NSConnection在iOS 9.0以后就不推荐了,上面的代码是会报warning的。官方还是推荐NSSession的。

如果用来下载图片就是酱紫的:

#import "NetworkViewController.h"

@interface NetworkViewController ()

@property (weak, nonatomic) IBOutlet UIImageView *imageView;

@end

@implementation NetworkViewController

- (void)viewDidLoad {
    [super viewDidLoad];
}

- (IBAction)start:(id)sender {
    [self startSyncConnection];
    
    NSLog(@"continue");
}

- (void)startSyncConnection {
    NSURL *url = [NSURL URLWithString:@"http://b-ssl.duitang.com/uploads/item/201704/02/20170402193824_A3mnS.jpeg"];

    NSURLRequest *requst = [[NSURLRequest alloc]initWithURL:url];

    NSError *error = nil;
    NSURLResponse *response = nil;
    
    NSData *data = [NSURLConnection sendSynchronousRequest:requst returningResponse:&response error:&error];
    
    self.imageView.image = [UIImage imageWithData:data];
    NSLog(@"connection data:%@", data);
    NSLog(@"connection error:%@", error);
}

@end

输出:
Example1[1536:319342] connection data:{length = 237631, bytes = 0xffd8ffe0 00104a46 49460001 01010060 ... Example1[1536:319342] connection error:(null)
Example1[1536:319342] continue
(2) 异步获取
- (void)startSyncConnection {
    NSURL *url = [NSURL URLWithString:@"http://b-ssl.duitang.com/uploads/item/201704/02/20170402193824_A3mnS.jpeg"];

    NSURLRequest *requst = [[NSURLRequest alloc]initWithURL:url];

    NSError *error = nil;
    NSURLResponse *response = nil;
    
    [NSURLConnection sendAsynchronousRequest:requst queue:[NSOperationQueue mainQueue] completionHandler:^(NSURLResponse * _Nullable response, NSData * _Nullable data, NSError * _Nullable connectionError) {
        NSLog(@"----%@",[NSThread currentThread]);

        self.imageView.image = [UIImage imageWithData:data];
        NSLog(@"connection data:%@", data);
        NSLog(@"connection error:%@", error);
    }];
}

输出:
continue
Example1[1571:325684] ----<NSThread: 0x2809ee600>{number = 1, name = main}
Example1[1571:325684] connection data:{length = 237631, bytes = 0xffd8ffe0 00104a46 49460001 01010060 ... 80200802 0080ffd9 }
Example1[1571:325684] connection error:(null)

这里的参数queue就是回调block执行在哪里,所以如果你设为了[[NSOperationQueue alloc] init]需要将block里面和UI相关的抛回主线程。

但是这种异步用block来回调结束的方式没有办法看到progress,非常鸡肋,如果想看进度需要用delegate的方式:

NSURL *url = [NSURL URLWithString:@"https://github.com/SwiftEducation/teaching-app-dev-swift-archive/archive/master.zip"];
    
NSURLRequest *request = [[NSURLRequest alloc]initWithURL:url];

NSURLConnection *connection = [[NSURLConnection alloc] initWithRequest:request delegate:self ];
[connection start];


// NSURLConnectionDataDelegate
- (void)connection:(NSURLConnection *)connection didReceiveData:(NSData *)data {
    NSLog(@"receive size:%lu", (unsigned long)data.length);
}

- (void)connection:(NSURLConnection *)connection didReceiveResponse:(NSURLResponse *)response {
    NSLog(@"total size:%ld", response.expectedContentLength);
}

- (void)connectionDidFinishLoading:(NSURLConnection *)connection {
    NSLog(@"finish");
}

输出:
total size:174739211
receive size:5480
receive size:24740
receive size:19180
……

由于下载的文件过大,每次都会回调didReceiveData,将这次下载好的data传入,所以data其实是累加的,而didReceiveResponse只会在开始调用一次,connectionDidFinishLoading会在结束调用一次。

而且如果没有进度回调,只能在结束的时候一次性将data写入本地,这样会突然使用内存增多;在有回调的时候可以每次都append data,将数据分次写入,对内存压力更小。


※ 线程问题

默认下载是在主进程进行的,虽然你可以通过queue改connection回调/delegate的运行线程,但下载这个事儿是你将connection写在哪里就在哪里运行。

如果把下载放在主进程,会导致UI卡顿以及下载进度受UI影响,所以我们得在非主进程干这个事儿,例如:

dispatch_async(dispatch_get_global_queue(0, 0), ^{
    NSURL *url = [NSURL URLWithString:@"https://github.com/SwiftEducation/teaching-app-dev-swift-archive/archive/master.zip"];
    
    NSURLRequest *request = [[NSURLRequest alloc]initWithURL:url];
    
    NSURLConnection *connection = [[NSURLConnection alloc] initWithRequest:request delegate:self];
    [connection start];
});

然后你会发现一个很神奇的事情,就是delegate都不回调了,下载不会进行,这是为什么呢?

因为NSURLConnection跟RunLoop有关系,NSURLConnection发出请求,等待接收服务器一点一点返回的数据,需要有一个运行循环等待服务器给NSURLConnection数据,也就是说NSURLConnection是在RunLoop中接收服务器返回的数。
NSURLConnection内部会关联当前线程对应的RunLoop,不断的给当前线程的RunLoop传递消息,RunLoop接收到Source进行处理。

然而我们之前讨论runloop的时候有看到,子线程的RunLoop默认是不存在的,所以接受不到服务器返回的数据。在主线程中,主线程的RunLoop是默认启动的,所以可以接受服务器返回的数据

要想NSURLConnection在子线程发送请求,可以接收到服务器返回的数据,要开启子线程的RunLoop:

dispatch_async(dispatch_get_global_queue(0, 0), ^{
    NSURL *url = [NSURL URLWithString:@"https://github.com/SwiftEducation/teaching-app-dev-swift-archive/archive/master.zip"];
    
    NSURLRequest *request = [[NSURLRequest alloc]initWithURL:url];
    
    NSURLConnection *connection = [[NSURLConnection alloc] initWithRequest:request delegate:self];
    [connection start];
    
    CFRunLoopGetCurrent();
    CFRunLoopRun();
});

runloop默认会在里面什么也没有的时候stop,也就是如果没有source也没有timer,他就停止了,所以正常其实是不用在下载结束后手动停止的,如果你希望更保险一点,可以在finish的时候手动停止runloop。


※ 问题代码

dispatch_async(dispatch_get_global_queue(0, 0), ^{
    CFRunLoopGetCurrent();
    CFRunLoopRun();
    
    NSURL *url = [NSURL URLWithString:@"https://github.com/SwiftEducation/teaching-app-dev-swift-archive/archive/master.zip"];
    
    NSURLRequest *request = [[NSURLRequest alloc]initWithURL:url];
    
    NSURLConnection *connection = [[NSURLConnection alloc] initWithRequest:request delegate:self];
    [connection start];
});

这段代码如果运行会发现delegate仍旧没有回调,这是为啥类?其实就是因为runloop在run的那一瞬间,里面什么也没有它就又停止了。。所以必须放到后面connection start以后再run起来。

我看有文章说NSSession的优点是能在后台任务,于是我就试了下NSConnection,如果退到后台也有在下载。。于是其实这点应该差不多滴。

3. NSSession

NSURLSession是2013年iOS 7发布的用于替代NSURLConnection的,iOS 9之后NSURLConnection彻底推出历史舞台,所以如果target是9.0+调用NSURLConnection可能会报黄色warning哈。

(1) 下载任务
  • 下面是一个下载举例,步骤大概是:
  1. 创建一个NSURLSessionConfiguration
  2. 通过configuration创建NSURLSession
  3. 创建NSURLSessionDownloadTask
  4. 开始task
  5. 如果需要监听下载进度,需要实现delegate
NSURL *url = [NSURL URLWithString:@"https://github.com/SwiftEducation/teaching-app-dev-swift-archive/archive/master.zip"];

// NSURLRequest *request = [[NSURLRequest alloc]initWithURL:url];

NSURLSessionConfiguration *configuration = [NSURLSessionConfiguration defaultSessionConfiguration];

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

NSURLSessionDownloadTask *task = [session downloadTaskWithURL:url];

// NSURLSessionDataTask *task = [session //dataTaskWithRequest:request completionHandler:^(NSData * _Nullable data, NSURLResponse * _Nullable response, NSError * _Nullable error) {
//  NSLog(@"finished");
// }];

[task resume];


// NSURLSessionDownloadDelegate
- (void)URLSession:(NSURLSession *)session downloadTask:(NSURLSessionDownloadTask *)downloadTask didFinishDownloadingToURL:(NSURL *)location {
    NSLog(@"finish");
}

- (void)URLSession:(NSURLSession *)session downloadTask:(NSURLSessionDownloadTask *)downloadTask didResumeAtOffset:(int64_t)fileOffset expectedTotalBytes:(int64_t)expectedTotalBytes {
    NSLog(@"resume offset:%lld", fileOffset);
}

- (void)URLSession:(NSURLSession *)session downloadTask:(NSURLSessionDownloadTask *)downloadTask didWriteData:(int64_t)bytesWritten totalBytesWritten:(int64_t)totalBytesWritten totalBytesExpectedToWrite:(int64_t)totalBytesExpectedToWrite {
    NSLog(@"receive size:%lld", bytesWritten);
}

输出:
receive size:77
receive size:97
receive size:17600
receive size:14562
……
  • 注意点:
  1. 如果调用dataTaskWithRequest: completionHandler:创建task, delegate不会回调,即如果带block就不会回调delegate,如果需要delegate可以改为NSURLSessionDownloadTask *downlaodTask = [sharedSession downloadTaskWithRequest:request]
  2. 创建的task默认是挂起状态,需要手动resume开启任务

(2) 各种配置

※ NSURLSessionConfiguration

NSURLSessionConfiguration为NSURLSession配置一些请求所需要的策略。如:超时、缓存策略、链接需求的。

NSURLSession会拷贝configuration。所以session一旦初始化结束就不会再更改configuration。若要修改则只能用更改后的NSURLSessionConfiguration对象重新创建新的NSURLSession对象。需要注意的是,NSURLSessionConfiguration中的某些设置可能会被NSURLSessionTask的 request 覆盖。

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

  • (NSURLSessionConfiguration *)ephemeralSessionConfiguration;
    返回一个预设配置,没有持久性存储的缓存,Cookie或证书。所有相关内容只保存在内存中,当 session 失效或应用终止,所有内容被清空。该配置可提高私密性,因为不会向硬盘上写入数据。这对于实现像"秘密浏览"功能的功能来说,是很理想的。

  • (NSURLSessionConfiguration *)backgroundSessionConfigurationWithIdentifier:(NSString *)identifier NS_AVAILABLE(10_10, 8_0);
    独特之处在于,它会创建一个后台会话。后台会话不同于常规的,普通的会话,它甚至可以在应用程序挂起,退出,
    崩溃的情况下运行上传和下载任务。初始化时指定的标识符,被用于向任何可能在进程外恢复后台传输的守护进程提供上下文 (如果是用户主动终止程序就不行了,如果想要再次开始传输,必须用户手动开启application)。
    identifier 是configuration的唯一标示,不能为空或nil.

下面来做做试验~

NSURLSessionConfiguration *configuration = [NSURLSessionConfiguration backgroundSessionConfigurationWithIdentifier:@"swift"];

// 文件保存地址:
sandbox://Library/Caches/com.apple.nsurlsessiond/Downloads/{包名}/CFNetworkDownloadxxxx.tmp

// 在delegate里面输出了totaltotalBytesWritten
// 第二次开始的时候输出:
session totalBytesWritten size:26028434

但是,你会发现它多出了一个tmp文件,而且didReceive回调的时候感觉是两个任务同时在跑,所以其实当我们重新开始一个相同identifier的task的时候,上一个没有结束的task继续跑了,当前的也跑了,会导致多个一起跑。。。

如果退到后台再进入前台,也会发现totalBytesWritten比退到后台那一刻多出来很多,所以即使在后台也是有在下载的,虽然这点任何一个type都实现了-。-

NSURLSessionConfiguration *configuration = [NSURLSessionConfiguration ephemeralSessionConfiguration];
或者
NSURLSessionConfiguration *configuration = [NSURLSessionConfiguration defaultSessionConfiguration];

// 文件保存地址:
sandbox://tmp/CFNetworkDownloadxxxx.tmp

如果你停止task,所有tmp文件都会被自动删掉哦:

dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(30 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
    [task cancel];
});

就实践而言,其实三种区别不大,backgroundSessionConfigurationWithIdentifier的好处是可以接着上次的下载,ephemeralSessionConfiguration虽然也有Cache下载到沙盒,但是据说是木有Cookie之类的,所以适合隐私模式。


NSURLSessionConfiguration还有一些属性可以配置:

  • identifier
    用于唯一标识一个后台下载的 session,当系统重启被自动关闭的应用时,可以通过这个标识符,重新创建 configuration 和 session 对象来恢复中断的下载。

  • HTTPAdditionalHeaders
    一个 dictionary,包含了需要额外添加到 session 中所有 request 的 header,如User-Agent。不应添加
    Authorization、Connection、Host和WWW-Authenticate。如果 request 中已存在某一 header,则使用 request 中的值,该 dictionary 默认为空。

  • networkServiceType
    NSURLRequestNetworkServiceType类型,network service type 影响着操作系统对于网络传输的优先级排列,进而影响系统电量、性能的消耗。默认值是NSURLNetworkServiceTypeDefault。

  • allowsCellularAccess
    布尔值,指明在移动网络下是否可发起请求。

  • timeoutIntervalForRequest
    请求的超时时间,默认值为60秒。

  • timeoutIntervalForResource
    所有资源下载或上传完成的超时时间,中间请求失败可重新请求,单位为秒,默认时间为7天。

  • sharedContainerIdentifier
    用于应用扩展与应用间的后台会话的扩展容器。(这个我也不是很懂在干神马)


  • Cookie配置

(1)HTTPCookieAcceptPolicy,接收 cookie 的策略。

NSHTTPCookieAcceptPolicy类型,默认值是
NSHTTPCookieAcceptPolicyOnlyFromMainDocumentDomain
HTTPCookieStorage,对于default 和 background session,该属性值为NSHTTPCookieStorage的单例对象,而 ephemeral session 的默认值为一个私有的NSHTTPCookieStorage对象,只将 cookie 保存在内存中,如果 session 不想保存 cookie,可将该属性设为 nil。

(2)HTTPShouldSetCookies,布尔值,指明请求时是否携带 cookie 信息。


※缓存策略(NSURLCache)

首先Cache和Cookie不是一个东西哈,Cookie是键值对一般,常用于登录时候的账户密码;Cache缓存可以是键值对也可以是别的。

另外NSURLCache与NSCache也不是一个东西哈。

NSURLCache:当一个请求完成下载来自服务器的回应,一个缓存的回应将在本地保存。下一次同一个请求再发起时,本地保存的回应就会马上返回,不需要连接服务器。

NSCache:NSCache是Foundation框架提供的缓存类的实现,使用方式类似于可变字典,最重要的是它是线程安全的,而NSMutableDictionary不是线程安全的,在多线程环境下使用NSCache是更好的选择。

URLCache用于保存缓存响应的 NSURLCache 对象,default session 默认使用NSURLCache的单例对象,background session 的默认值为 nil,而 ephemeral session 的默认值是一个私有的NSURLCache对象,数据只保存在内存中。如果不想使用缓存,可直接设置该值为 nil。

configuration的cachePolicys属性指明了请求使用缓存响应的策略:

NSURLRequestUseProtocolCachePolicy = 0 //默认的缓存策略, 如果缓存不存在,直接从服务端获取。如果缓存存在,会根据response中的Cache-Control字段判断下一步操作,如: Cache-Control字段为must-revalidata, 则询问服务端该数据是否有更新,无更新的话直接返回给用户缓存数据,若已更新,则请求服务端.
NSURLRequestReloadIgnoringLocalCacheData = 1 //忽略本地缓存数据,直接请求服务端.
NSURLRequestIgnoringLocalAndRemoteCacheData = 4 //忽略本地缓存,代理服务器以及其他中介,直接请求源服务端.
NSURLRequestReloadIgnoringCacheData = NSURLRequestReloadIgnoringLocalCacheData
NSURLRequestReturnCacheDataElseLoad = 2 //有缓存就使用,不管其有效性(即忽略Cache-Control字段), 无则请求服务端.
NSURLRequestReturnCacheDataDontLoad = 3 //只加载本地缓存. 没有就失败. (确定当前无网络时使用)
NSURLRequestReloadRevalidatingCacheData = 5 //缓存数据必须得得到服务端确认有效才使用

NSURLCache 通过保存NSURLRequest对象和其相应的NSCachedURLResponse对象,实现了对网络请求的缓存。它支持对内存缓存空间和硬盘缓存空间的设置,以及硬盘缓存路径的设置。

通过NSURLSessionConfiguration或NSURLRequest对象中设置的
NSURLRequestCachePolicy影响着请求如何使用缓存的行为。

对于是否存储响应以及在NSURLRequestUseProtocolCachePolicy下是否使用缓存响应,如 HTTP 或 HTTPS 协议,受 header 中缓存相关的字段影响(Cache-Control、Last-Modified、Etag)。

他的一些方法:

- cachedResponseForRequest:
- storeCachedResponse:forRequest:
- removeAllCachedResponses
- removeCachedResponseForRequest:
- getCachedResponseForDataTask:completionHandler:
- removeCachedResponseForDataTask:
- removeCachedResponsesSinceDate:
- storeCachedResponse:forDataTask:

代理NSURLSessionDataDelegate中的方法
URLSession:dataTask:willCacheResponse:completionHandler:使用户有机会在获取数据后修改缓存行为。方法中要执行 block 参数 completionHandler,如果缓存则传入一个NSCachedURLResponse对象,若不想缓存可传入 nil。当返回的响应为缓存中的时候,则不调用该方法。


※ Task

  • NSURLSessionDataTask
    一般的get、post等请求
  • NSURLSessionUploadTask
    用于上传文件或者数据量比较大的请求
  • NSURLSessionDownloadTask
    用于下载文件或者数据量比较大的请求
  • NSURLSessionStreamTask
    建立一个TCP / IP连接的主机名和端口或一个网络服务对象。

NSURLSessionDataTask和NSURLSessionUploadTask都没有再实现额外的方法,它们只是用来区分不同的 task。

而NSURLSessionDownloadTask除了用来区分 task,还额外实现了一个方法- cancelByProducingResumeData:,这个方法调用了基类NSURLSessionTask中的- cancel方法,参数 completionHandler 要求传入一个 block,用来处理已传输的数据 resume data,这个 resume data 将来可用于 session 的- downloadTaskWithResumeData:方法,以便继续中断的下载。

如果你想操作task可以通过下面三个方法:
- (void)suspend;  //暂停
- (void)resume;  //开始或者恢复
- (void)cancel;  //关闭任务
- (void)cancelByProducingResumeData:(void (^)(NSData * __nullable resumeData))completionHandler;
//取消(保留之前缓存的数据,方便下次缓存进行使用)```

NSURLSessionDownloadTask每创建一个task,相当于创建一个线程,在被调用之前,是挂起状态。可以进行开始、暂停、取消等操作,也可通过NSURLSessionTaskState查询状态为运行、挂起、取消或者完成。


  • 可以通过task的state属性查看任务当前的状态:
typedef NS_ENUM(NSInteger, NSURLSessionTaskState) {
    NSURLSessionTaskStateRunning = 0,
    NSURLSessionTaskStateSuspended = 1,
    NSURLSessionTaskStateCanceling = 2, /*当向task发送cancel消息后,会转变为该状态,代理会收到- URLSession:task:didCompleteWithError:消息*/
    NSURLSessionTaskStateCompleted = 3, /*完成状态不包括被取消的情况*/
};

通过NSURLSession 创建的 task 默认是状态是 suspended,调用- resume 方法后,task 的状态变为 running。通过- suspend 方法可以将 task 重新变为挂起状态,当恢复 running 状态时,download task 可以从中断的地方继续传输,而其他 task 都需要重新开始。当调用- cancel 方法后,task 变为 canceling 状态,在发送给 delegate 的URLSession:task:didCompleteWithError:消息中,error 参数的 domain 为NSURLErrorDomain,code 为NSURLErrorCancelled。


  • task优先级

task的priority属性接受0到1之间的浮点数,通过每一个 task 不同的 priority 值,可以提示系统 task 执行的优先级,1为最高,0为最低。默认值是NSURLSessionTaskPriorityDefault,即 0.5。系统提供了3个常量值:

NSURLSessionTaskPriorityDefault //0.5
NSURLSessionTaskPriorityLow // 0
NSURLSessionTaskPriorityHigh // 1

※ 注意哦:NSSession天然不支持同步
如果想做到同步获取response,可以选择用GCD的信号量哈~

3. 区别

(1)NSSession文件下载后,默认存放沙盒tmp路径下,名称随机生成,并在下载完成后自动删除,因此需要在下载完成的代理方法中进行转存

(2)NSURLConnection 通过connectionWithRequest: delegate:设置代理实现方法,添加异步处理后,还需手动开启子线程的消息循环,方法执行完后会自动关闭。
NSURLSession自动异步处理,不许干预,只是创建任务后默认挂起,需要resume。

(3)NSURLConnection 的下载进度监测实现过程比较复杂,而NSURLSession使用更简便,只需要通过代理方法提供的参数直接使用即可

(4)NSURLConnection和NSURLSession都可以实现对下载过程可控,包括:开始、暂停、和继续,而NSSession能对程序意外退出再次进入重新下载做出更好的处理

4. 断点续传实现

如果实现下载,最简单的就是建一个NSMutableData,每次回调的时候将新来的数据append给NSMutableData,然后在finish的时候把NSMutableData writeToFile。但是在结束前NSMutableData都在内存里面,这样非常不好会造成内存峰值还是持续性的,所以应该下一点写入一点。

但如果下载中断了,下次下载的时候用户肯定不希望开始新的下载,于是就会牵扯到断点续传,也就是通过头文件里面的range规定从哪里开始下载:

range

我用session试了一下,的确可以从指定字节开始下载,问题是自己保存啥的总是有问题,非常难受。。
如果想用delegate可以这么设置header:

NSURLSessionConfiguration *configuration = [NSURLSessionConfiguration backgroundSessionConfigurationWithIdentifier:url.absoluteString];
    
configuration.HTTPAdditionalHeaders = @{@"Range": [NSString stringWithFormat:@"bytes=%lld-",offset]};
    
NSURLSession *session = [NSURLSession sessionWithConfiguration:configuration delegate:self delegateQueue:[NSOperationQueue mainQueue]];
    
NSURLSessionDataTask *dataTask = [session dataTaskWithURL:url];

如果用request,不需要delegate回调只要complete回调可以这么写:

NSURLSessionConfiguration *configuration = [NSURLSessionConfiguration backgroundSessionConfigurationWithIdentifier:url.absoluteString];

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

NSMutableURLRequest *request = [NSMutableURLRequest requestWithURL:url cachePolicy:NSURLRequestReloadIgnoringLocalCacheData timeoutInterval:0];
[request setValue:[NSString stringWithFormat:@"bytes=%lld-",offset] forHTTPHeaderField:@"Range"];

NSURLSessionDataTask *dataTask = [session dataTaskWithRequest:request completionHandler:^(NSData * _Nullable data, NSURLResponse * _Nullable response, NSError * _Nullable error) {
    NSLog(@"finished");
}];

但实在是文件断点续传有点bug,我猜测和文件锁没有close啥的有关,但是比较麻烦于是换了NSSession的自带resume data的功能了~ 非常方便~ 再次感受了NSSession的优秀~

但是注意哦,自带的断点续传需要服务器满足以下要求:


断点续传要求
// h文件
#import <Foundation/Foundation.h>

NS_ASSUME_NONNULL_BEGIN

@interface DownloadManager : NSObject

+ (instancetype)sharedInstance;

- (void)downLoadWithUrl:(NSString *)fileUrl;

- (void)cancel;

@end

NS_ASSUME_NONNULL_END


// m文件
#import "DownloadManager.h"

@interface DownloadManager() <NSURLSessionDownloadDelegate> {
    NSURLSessionDownloadTask *dataTask;
    
    NSString *downLoadedPath;
    NSString *resumeDataPath;
    
    BOOL isDownloading;
}

@end

@implementation DownloadManager

+ (instancetype)sharedInstance {
    static DownloadManager *instance;
    static dispatch_once_t onceToken;
    dispatch_once(&onceToken, ^{
        instance = [[DownloadManager alloc] init];
    });
    
    return instance;
}

- (void)downLoadWithUrl:(NSString *)fileUrl {
    if (isDownloading) {
        return;
    }
    
    fileUrl = @"https://downloads.slack-edge.com/mac_releases/Slack-4.1.0-macOS.dmg";
    //1.文件的存放
    NSString *fileName = fileUrl.lastPathComponent;
    
    NSString *documentPath = NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES).firstObject;
    
    downLoadedPath = [documentPath stringByAppendingPathComponent:fileName];
    resumeDataPath = [documentPath stringByAppendingPathComponent:[NSString stringWithFormat:@"resume-%@", fileName]];
    
    BOOL isDir = NO;
    if ([[NSFileManager defaultManager] fileExistsAtPath:downLoadedPath isDirectory:&isDir]) {
        // 下载过啦
        return;
    }
    
    NSURL *url = [NSURL URLWithString:fileUrl];
    
    isDownloading = YES;
    
    NSURLSessionConfiguration *configuration = [NSURLSessionConfiguration backgroundSessionConfigurationWithIdentifier:url.absoluteString];
    
    NSURLSession *session = [NSURLSession sessionWithConfiguration:configuration delegate:self delegateQueue:[NSOperationQueue mainQueue]];
    
    if ([[NSFileManager defaultManager] fileExistsAtPath:resumeDataPath]) {
        dataTask = [session downloadTaskWithResumeData:[[NSFileManager defaultManager] contentsAtPath:resumeDataPath]];
    } else {
        dataTask = [session downloadTaskWithURL:url];
    }
    
    [dataTask resume];
}

- (void)cancel {
    if (!isDownloading) {
        return;
    }
    __weak typeof(self) weakSelf = self;
    [dataTask cancelByProducingResumeData:^(NSData * _Nullable resumeData) {
        __strong typeof(weakSelf) strongSelf = weakSelf;
        [resumeData writeToFile: strongSelf->resumeDataPath atomically:NO];
    }];
    isDownloading = NO;
}

#pragma mark - delegate
- (void)URLSession:(NSURLSession *)session downloadTask:(NSURLSessionDownloadTask *)downloadTask didFinishDownloadingToURL:(NSURL *)location {
    [[NSFileManager defaultManager] moveItemAtURL:location toURL:[NSURL fileURLWithPath:downLoadedPath] error:nil];
    
    [[NSFileManager defaultManager] removeItemAtPath:resumeDataPath error:nil];
    NSLog(@"session finish");
    isDownloading = NO;
}

- (void)URLSession:(NSURLSession *)session downloadTask:(NSURLSessionDownloadTask *)downloadTask didResumeAtOffset:(int64_t)fileOffset expectedTotalBytes:(int64_t)expectedTotalBytes {
    NSLog(@"session didResumeAtOffset:%lld", fileOffset);
}

- (void)URLSession:(NSURLSession *)session downloadTask:(NSURLSessionDownloadTask *)downloadTask didWriteData:(int64_t)bytesWritten totalBytesWritten:(int64_t)totalBytesWritten totalBytesExpectedToWrite:(int64_t)totalBytesExpectedToWrite {
    NSLog(@"session totalBytesWritten size:%lld", totalBytesWritten);
}

- (void)URLSession:(NSURLSession *)session task:(NSURLSessionTask *)task willPerformHTTPRedirection:(NSHTTPURLResponse *)response newRequest:(NSURLRequest *)request completionHandler:(void (^)(NSURLRequest * _Nullable))completionHandler {
}

@end

比较神奇的是,resumeData其实不大,只有三四kb的样子,应该是NSSession有优化吧,并不是直接存了下载了的data进去,如果那样的话对内存也会有影响下载大文件的时候,如果你把resumeData作为实例变量存起来,太大就有问题了。

打开resume data是一个binary文件,用plutil -convert xml1 resume-Slack-4.1.0-macOS.dmg以后放入sublime查看就是下面酱紫的,虽然仍旧看不懂-。-:

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
    <key>$archiver</key>
    <string>NSKeyedArchiver</string>
    <key>$objects</key>
    <array>
        <string>$null</string>
        <dict>
            <key>$class</key>
            <dict>
                <key>CF$UID</key>
                <integer>19</integer>
            </dict>
            <key>NS.keys</key>
            <array>
                <dict>
                    <key>CF$UID</key>
                    <integer>2</integer>
                </dict>
                <dict>
                    <key>CF$UID</key>
                    <integer>3</integer>
                </dict>
                <dict>
                    <key>CF$UID</key>
                    <integer>4</integer>
                </dict>
                <dict>
                    <key>CF$UID</key>
                    <integer>5</integer>
                </dict>
                <dict>
                    <key>CF$UID</key>
                    <integer>6</integer>
                </dict>
                <dict>
                    <key>CF$UID</key>
                    <integer>7</integer>
                </dict>
                <dict>
                    <key>CF$UID</key>
                    <integer>8</integer>
                </dict>
                <dict>
                    <key>CF$UID</key>
                    <integer>9</integer>
                </dict>
            </array>
            <key>NS.objects</key>
            <array>
                <dict>
                    <key>CF$UID</key>
                    <integer>10</integer>
                </dict>
                <dict>
                    <key>CF$UID</key>
                    <integer>12</integer>
                </dict>
                <dict>
                    <key>CF$UID</key>
                    <integer>13</integer>
                </dict>
                <dict>
                    <key>CF$UID</key>
                    <integer>14</integer>
                </dict>
                <dict>
                    <key>CF$UID</key>
                    <integer>15</integer>
                </dict>
                <dict>
                    <key>CF$UID</key>
                    <integer>16</integer>
                </dict>
                <dict>
                    <key>CF$UID</key>
                    <integer>17</integer>
                </dict>
                <dict>
                    <key>CF$UID</key>
                    <integer>18</integer>
                </dict>
            </array>
        </dict>
        <string>NSURLSessionResumeCurrentRequest</string>
        <string>NSURLSessionResumeOriginalRequest</string>
        <string>NSURLSessionDownloadURL</string>
        <string>NSURLSessionResumeInfoTempFileName</string>
        <string>NSURLSessionResumeBytesReceived</string>
        <string>NSURLSessionResumeEntityTag</string>
        <string>NSURLSessionResumeInfoVersion</string>
        <string>NSURLSessionResumeServerDownloadDate</string>
        <dict>
            <key>$class</key>
            <dict>
                <key>CF$UID</key>
                <integer>11</integer>
            </dict>
            <key>NS.data</key>
            <data>
            YnBsaXN0MDDUAQIDBAUGhYZYJHZlcnNpb25YJG9iamVjdHNZJGFy
            Y2hpdmVyVCR0b3ASAAGGoK8QGwcIOk9VVlxdXl83YDthYnJzdHV2
            d3h5ent8gVUkbnVsbN8QIwkKCwwNDg8QERITFBUWFxgZGhscHR4f
            ICEiIyQlJicoKSorLC0uLS0tMjMtLTY3ODk6Ozw3PTo/QC0/ODpF
            OkVIOEo8LU1fECBfX25zdXJscmVxdWVzdF9wcm90b19wcm9wX29i
            al8xOF8QIF9fbnN1cmxyZXF1ZXN0X3Byb3RvX3Byb3Bfb2JqXzIw
            XxAgX19uc3VybHJlcXVlc3RfcHJvdG9fcHJvcF9vYmpfMTlfECBf
            X25zdXJscmVxdWVzdF9wcm90b19wcm9wX29ial8yMV8QH19fbnN1
            cmxyZXF1ZXN0X3Byb3RvX3Byb3Bfb2JqXzRfEBhib3VuZEludGVy
            ZmFjZUlkZW50aWZpZXJfEB9fX25zdXJscmVxdWVzdF9wcm90b19w
            cm9wX29ial82XxAfX19uc3VybHJlcXVlc3RfcHJvdG9fcHJvcF9v
            YmpfMV8QH19fbnN1cmxyZXF1ZXN0X3Byb3RvX3Byb3Bfb2JqXzhf
            EBpfX25zdXJscmVxdWVzdF9wcm90b19wcm9wc18QFGFsbG93ZWRQ
            cm90b2NvbFR5cGVzXxAacGF5bG9hZFRyYW5zbWlzc2lvblRpbWVv
            dXRfEB9fX25zdXJscmVxdWVzdF9wcm90b19wcm9wX29ial8zViRj
            bGFzc18QHnJlcXVpcmVzU2hvcnRDb25uZWN0aW9uVGltZW91dFIk
            MF8QH19fbnN1cmxyZXF1ZXN0X3Byb3RvX3Byb3Bfb2JqXzVfEBBz
            dGFydFRpbWVvdXRUaW1lUiQxXxAhc2NoZW1lV2FzVXBncmFkZWRE
            dWVUb0R5bmFtaWNIU1RTXxAfX19uc3VybHJlcXVlc3RfcHJvdG9f
            cHJvcF9vYmpfMFIkMl8QH19fbnN1cmxyZXF1ZXN0X3Byb3RvX3By
            b3Bfb2JqXzdfECBfX25zdXJscmVxdWVzdF9wcm90b19wcm9wX29i
            al8xMF8QIF9fbnN1cmxyZXF1ZXN0X3Byb3RvX3Byb3Bfb2JqXzEx
            Wmlnbm9yZUhTVFNfECBfX25zdXJscmVxdWVzdF9wcm90b19wcm9w
            X29ial8xMl8QEnByZXZlbnRIU1RTU3RvcmFnZV8QIF9fbnN1cmxy
            ZXF1ZXN0X3Byb3RvX3Byb3Bfb2JqXzEzXxAfX19uc3VybHJlcXVl
            c3RfcHJvdG9fcHJvcF9vYmpfMl8QIF9fbnN1cmxyZXF1ZXN0X3By
            b3RvX3Byb3Bfb2JqXzE0XxAgX19uc3VybHJlcXVlc3RfcHJvdG9f
            cHJvcF9vYmpfMTVfEB9fX25zdXJscmVxdWVzdF9wcm90b19wcm9w
            X29ial85XxAgX19uc3VybHJlcXVlc3RfcHJvdG9fcHJvcF9vYmpf
            MTZfECBfX25zdXJscmVxdWVzdF9wcm90b19wcm9wX29ial8xN4AN
            gACADoAAgACAAIAJgAOAAIAAEAAjAAAAAAAAAACAB4AaCBACgAgQ
            CQiAAhAWgACAAoAHCIAKCIAKgAaAB4ALgAiAAIAMCNNQFlEtU1RX
            TlMuYmFzZVtOUy5yZWxhdGl2ZYAAgAWABF8QQ2h0dHBzOi8vZG93
            bmxvYWRzLnNsYWNrLWVkZ2UuY29tL21hY19yZWxlYXNlcy9TbGFj
            ay00LjEuMC1tYWNPUy5kbWfSV1hZWlokY2xhc3NuYW1lWCRjbGFz
            c2VzVU5TVVJMollbWE5TT2JqZWN0I0BOAAAAAAAAEAAJECQT////
            //////9TR0VU02NkFmVrcVdOUy5rZXlzWk5TLm9iamVjdHOlZmdo
            aWqAD4AQgBGAEoATpWxtbm9wgBSAFYAWgBeAGIAZWlVzZXItQWdl
            bnRWQWNjZXB0XxAPQWNjZXB0LUxhbmd1YWdlXxAPQWNjZXB0LUVu
            Y29kaW5nWF9faGhhYV9fXxAzQXBwUmVsYXhJT1MvMS4yLjEuMSBD
            Rk5ldHdvcmsvOTc4LjAuNyBEYXJ3aW4vMTguNi4wUyovKlVlbi11
            c18QEWJyLCBnemlwLCBkZWZsYXRlXxEBHA0KDQpZbkJzYVhOME1E
            RFVBUUlEQkFVSENRdGFWWE5sY2kxQloyVnVkRlpCWTJObGNIUmZF
            QTlCWTJObGNIUXRUR0Z1WjNWaFoyVmZFQTlCWTJObGNIUXRSVzVq
            YjJScGJtZWhCbDhRTTBGd2NGSmxiR0Y0U1U5VEx6RXVNaTR4TGpF
            Z1EwWk9aWFIzYjNKckx6azNPQzR3TGpjZ1JHRnlkMmx1THpFNExq
            WXVNS0VJVXlvdktxRUtWV1Z1TFhWem9ReGZFQkZpY2l3Z1ozcHBj
            Q3dnWkdWbWJHRjBaUWdSSENNMVIwbC9nWVdIalk4QUFBQUFBQUFC
            QVFBQUFBQUFBQUFOQUFBQUFBQUFBQUFBQUFBQUFBQUFvdz090ldY
            fX5fEBNOU011dGFibGVEaWN0aW9uYXJ5o3+AW18QE05TTXV0YWJs
            ZURpY3Rpb25hcnlcTlNEaWN0aW9uYXJ50ldYgoNcTlNVUkxSZXF1
            ZXN0ooRbXE5TVVJMUmVxdWVzdF8QD05TS2V5ZWRBcmNoaXZlctGH
            iF8QG05TS2V5ZWRBcmNoaXZlUm9vdE9iamVjdEtleYABAAgAEQAa
            ACMALQAyADcAVQBbAKQAxwDqAQ0BMAFSAW0BjwGxAdMB8AIHAiQC
            RgJNAm4CcQKTAqYCqQLNAu8C8gMUAzcDWgNlA4gDnQPAA+IEBQQo
            BEoEbQSQBJIElASWBJgEmgScBJ4EoASiBKQEpgSvBLEEswS0BLYE
            uAS6BLsEvQS/BMEEwwTFBMYEyATJBMsEzQTPBNEE0wTVBNcE2ATf
            BOcE8wT1BPcE+QU/BUQFTwVYBV4FYQVqBXMFdQV2BXgFgQWFBYwF
            lAWfBaUFpwWpBasFrQWvBbUFtwW5BbsFvQW/BcEFzAXTBeUF9wYA
            BjYGOgZABlQHdAd5B48HkwepB7YHuwfIB8sH2AfqB+0ICwAAAAAA
            AAIBAAAAAAAAAIkAAAAAAAAAAAAAAAAAAAgN
            </data>
        </dict>
        <dict>
            <key>$classes</key>
            <array>
                <string>NSMutableData</string>
                <string>NSData</string>
                <string>NSObject</string>
            </array>
            <key>$classname</key>
            <string>NSMutableData</string>
        </dict>
        <dict>
            <key>$class</key>
            <dict>
                <key>CF$UID</key>
                <integer>11</integer>
            </dict>
            <key>NS.data</key>
            <data>
            YnBsaXN0MDDUAQIDBAUGWVpYJHZlcnNpb25YJG9iamVjdHNZJGFy
            Y2hpdmVyVCR0b3ASAAGGoK0HCChDSUpQUVJTJ1RVVSRudWxs3xAd
            CQoLDA0ODxAREhMUFRYXGBkaGxwdHh8gISIjJCUmJygoKissLS4u
            Ki0oMygnNTY3Kio6NyotPj8rQVIkMV8QEHN0YXJ0VGltZW91dFRp
            bWVfEB5yZXF1aXJlc1Nob3J0Q29ubmVjdGlvblRpbWVvdXRfEBJw
            cmV2ZW50SFNUU1N0b3JhZ2VfEBhib3VuZEludGVyZmFjZUlkZW50
            aWZpZXJfECBfX25zdXJscmVxdWVzdF9wcm90b19wcm9wX29ial8x
            MFYkY2xhc3NfECBfX25zdXJscmVxdWVzdF9wcm90b19wcm9wX29i
            al8xMV8QIF9fbnN1cmxyZXF1ZXN0X3Byb3RvX3Byb3Bfb2JqXzEy
            XxAgX19uc3VybHJlcXVlc3RfcHJvdG9fcHJvcF9vYmpfMTNfEBpf
            X25zdXJscmVxdWVzdF9wcm90b19wcm9wc18QIF9fbnN1cmxyZXF1
            ZXN0X3Byb3RvX3Byb3Bfb2JqXzE0Wmlnbm9yZUhTVFNfECBfX25z
            dXJscmVxdWVzdF9wcm90b19wcm9wX29ial8xNV8QIXNjaGVtZVdh
            c1VwZ3JhZGVkRHVlVG9EeW5hbWljSFNUU18QGnBheWxvYWRUcmFu
            c21pc3Npb25UaW1lb3V0XxAUYWxsb3dlZFByb3RvY29sVHlwZXNS
            JDBfEB9fX25zdXJscmVxdWVzdF9wcm90b19wcm9wX29ial85XxAf
            X19uc3VybHJlcXVlc3RfcHJvdG9fcHJvcF9vYmpfOF8QH19fbnN1
            cmxyZXF1ZXN0X3Byb3RvX3Byb3Bfb2JqXzdfEB9fX25zdXJscmVx
            dWVzdF9wcm90b19wcm9wX29ial82XxAfX19uc3VybHJlcXVlc3Rf
            cHJvdG9fcHJvcF9vYmpfNV8QH19fbnN1cmxyZXF1ZXN0X3Byb3Rv
            X3Byb3Bfb2JqXzRfEB9fX25zdXJscmVxdWVzdF9wcm90b19wcm9w
            X29ial8zUiQyXxAfX19uc3VybHJlcXVlc3RfcHJvdG9fcHJvcF9v
            YmpfMV8QH19fbnN1cmxyZXF1ZXN0X3Byb3RvX3Byb3Bfb2JqXzBf
            EB9fX25zdXJscmVxdWVzdF9wcm90b19wcm9wX29ial8yEAkjAAAA
            AAAAAAAICIAAgAKADIAHgAqACoAAgAcIgAsIEAAQAoAIgACAAIAJ
            gAiAAIAHEBCAA4ACgAYI00QPRSpHSFdOUy5iYXNlW05TLnJlbGF0
            aXZlgACABYAEXxBDaHR0cHM6Ly9kb3dubG9hZHMuc2xhY2stZWRn
            ZS5jb20vbWFjX3JlbGVhc2VzL1NsYWNrLTQuMS4wLW1hY09TLmRt
            Z9JLTE1OWiRjbGFzc25hbWVYJGNsYXNzZXNVTlNVUkyiTU9YTlNP
            YmplY3QjQE4AAAAAAAAQAAkQJBP//////////9JLTFZXXE5TVVJM
            UmVxdWVzdKJYT1xOU1VSTFJlcXVlc3RfEA9OU0tleWVkQXJjaGl2
            ZXLRW1xfEBtOU0tleWVkQXJjaGl2ZVJvb3RPYmplY3RLZXmAAQAI
            ABEAGgAjAC0AMgA3AEUASwCIAIsAngC/ANQA7wESARkBPAFfAYIB
            nwHCAc0B8AIUAjECSAJLAm0CjwKxAtMC9QMXAzkDPANeA4ADogOk
            A60DrgOvA7EDswO1A7cDuQO7A70DvwPAA8IDwwPFA8cDyQPLA80D
            zwPRA9MD1QPXA9kD2wPdA94D5QPtA/kD+wP9A/8ERQRKBFUEXgRk
            BGcEcAR5BHsEfAR+BIcEjASZBJwEqQS7BL4E3AAAAAAAAAIBAAAA
            AAAAAF0AAAAAAAAAAAAAAAAAAATe
            </data>
        </dict>
        <string>https://downloads.slack-edge.com/mac_releases/Slack-4.1.0-macOS.dmg</string>
        <string>CFNetworkDownload_AWztkt.tmp</string>
        <integer>97409</integer>
        <string>"e78f09a0416769788a0f40196e97d83a-15"</string>
        <integer>4</integer>
        <string>Thu, 10 Oct 2019 20:40:28 GMT</string>
        <dict>
            <key>$classes</key>
            <array>
                <string>NSMutableDictionary</string>
                <string>NSDictionary</string>
                <string>NSObject</string>
            </array>
            <key>$classname</key>
            <string>NSMutableDictionary</string>
        </dict>
    </array>
    <key>$top</key>
    <dict>
        <key>NSKeyedArchiveRootObjectKey</key>
        <dict>
            <key>CF$UID</key>
            <integer>1</integer>
        </dict>
    </dict>
    <key>$version</key>
    <integer>100000</integer>
</dict>
</plist>

而且如果用resumeData,缓存文件夹下面不会生成多个tmp文件都在下载。

但是如果用户强制退出没有调用cancelByProducingResumeData就没有办法断点续传了哦,查了一下如果强制退出/闪退,会回调didCompleteWithError,所以可以加上:

- (void)URLSession:(NSURLSession *)session task:(NSURLSessionTask *)task didCompleteWithError:(NSError *)error {
    if (error) {
        [self cancel];
    }
}

另外,当任务发生错误中断的时候,Task会包含出错的Error信息,Error的Userinfo中有一个字段NSURLSessionDownloadTaskResumeData,其中就包含了ResumeData,直接用这个data就好其实~

现在即使外面没有手动调用cancel仍旧可以断点续传哦~~

参考:
NSConnection:
https://www.jianshu.com/p/e72622831747
https://www.jianshu.com/p/827da18fc935
https://blog.csdn.net/jh_1995/article/details/50640830

NSSesson:
https://www.cnblogs.com/whoislcj/p/6369717.html
https://www.jianshu.com/p/e8735fff360b
https://www.jianshu.com/p/d35dd5d7fad7
https://www.jianshu.com/p/4c373a1cb40f

两者都有:
http://www.cocoachina.com/cms/wap.php?action=article&id=24102
https://www.jianshu.com/p/668b263befc8

断点续传:
https://www.jianshu.com/p/65b05ab1b130

相关文章

网友评论

      本文标题:[iOS] NSConnection与NSSession

      本文链接:https://www.haomeiwen.com/subject/bgppmctx.html