对于小文件的下载也是比较实用的,比如下载一些图片文件、txt 文件、mp3 文件等。

小文件下载

如果文件比较小,下载方式有以下几种:

使用 NSData 的方法

1
2
3
4
5
6
NSURL *url = [NSURL URLWithString:@"http://120.25.226.186:32812/resources/images/minion_15.png"];
NSData *data = [NSData dataWithContentsOfURL:url];
NSLog(@"data's length = %zd", data.length);
// 打印:
data's length = 44471

在 Chrome 的开发者工具 -> Network 中可以查看响应头等信息:

data length

可以看到,服务器给客户端返回的响应也包含有内容的长度。

但是,这种方法不适合做断点下载的操作,也无法取消操作。

使用 NSURLConnection 发送一个 HTTP 请求

其实就是发起一个 GET 请求即可。

1
2
3
4
NSURLRequest *request = [NSURLRequest requestWithURL:[NSURL URLWithString:@"http://120.25.226.186:32812/resources/images/minion_15.png"]];
[NSURLConnection sendAsynchronousRequest:request queue:[NSOperationQueue mainQueue] completionHandler:^(NSURLResponse * _Nullable response, NSData * _Nullable data, NSError * _Nullable connectionError) {
NSLog(@"data's length = %zd", data.length);
}];

不过,这种方法也不适合做断点下载的操作。

如果是下载图片文件,还可以使用 SDWebImage 框架

使用 SDWebImageDownloader 的方法:

1
- (id <SDWebImageOperation>)downloadImageWithURL:(NSURL *)url options:(SDWebImageDownloaderOptions)options progress:(SDWebImageDownloaderProgressBlock)progressBlock completed:(SDWebImageDownloaderCompletedBlock)completedBlock;

大文件下载

如果想用 NSURLConnection 下载一些稍微比较大的文件,就可以使用 NSURLConnection 代理的方式来实现,具体可以看 06_网络_02_NSURLConnection.md。MJ 写的 Demo:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
@interface ViewController () <NSURLConnectionDataDelegate>
@property (weak, nonatomic) IBOutlet UIProgressView *progressView;
/** 文件数据 */
@property (nonatomic, strong) NSMutableData *fileData;
/** 文件的总长度 */
@property (nonatomic, assign) NSInteger contentLength;
@end
@implementation ViewController
- (void)viewDidLoad {
[super viewDidLoad];
NSURL *url = [NSURL URLWithString:@"http://120.25.226.186:32812/resources/videos/minion_15.mp4"];
[NSURLConnection connectionWithRequest:[NSURLRequest requestWithURL:url] delegate:self];
}
#pragma mark - <NSURLConnectionDataDelegate>
- (void)connection:(NSURLConnection *)connection didReceiveResponse:(NSHTTPURLResponse *)response {
self.contentLength = [response.allHeaderFields[@"Content-Length"] integerValue];
self.fileData = [NSMutableData data];
}
- (void)connection:(NSURLConnection *)connection didReceiveData:(NSData *)data {
[self.fileData appendData:data];
CGFloat progress = 1.0 * self.fileData.length / self.contentLength;
NSLog(@"已下载:%.2f%%", (progress) * 100);
self.progressView.progress = progress; // 因为 didReceiveData 这个方法在主线程中执行,所以可以直接刷新 UI
}
- (void)connectionDidFinishLoading:(NSURLConnection *)connection {
NSLog(@"下载完毕");
// 将文件写入沙盒中
// 缓存文件夹
NSString *caches = [NSSearchPathForDirectoriesInDomains(NSCachesDirectory, NSUserDomainMask, YES) lastObject];
// 文件路径
NSString *file = [caches stringByAppendingPathComponent:@"minion_15.mp4"];
// 写入数据
[self.fileData writeToFile:file atomically:YES];
self.fileData = nil;
}
@end

用 NSURLConnection 可以下载一些稍微比较大的文件(minion_15.mp4 大概 10M 以内),上面的下载方法是把视频下载完毕之后再存到沙盒里的,这样会导致在下载过程中,内存会越来越大(因为这个 fileData 越来越大),所以推荐使用 NSURLSession 来下载,做到边下载边存到沙盒。

不过在这之前,还是可以先用 NSURLConnection 代理方法来下载,利用 NSFileHandle 来做到边下载边存到沙盒,但是这种方法还是不支持断点下载:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
#define XMGFile [[NSSearchPathForDirectoriesInDomains(NSCachesDirectory, NSUserDomainMask, YES) lastObject] stringByAppendingPathComponent:@"minion_15.mp4"]
@interface ViewController () <NSURLConnectionDataDelegate>
@property (weak, nonatomic) IBOutlet UIProgressView *progressView;
/** 文件的总长度 */
@property (nonatomic, assign) NSInteger contentLength;
/** 当前下载的总长度 */
@property (nonatomic, assign) NSInteger currentLength;
/** 文件句柄对象 */
@property (nonatomic, strong) NSFileHandle *handle;
@end
@implementation ViewController
- (void)viewDidLoad {
[super viewDidLoad];
NSURL *url = [NSURL URLWithString:@"http://120.25.226.186:32812/resources/videos/minion_15.mp4"];
[NSURLConnection connectionWithRequest:[NSURLRequest requestWithURL:url] delegate:self];
}
#pragma mark - <NSURLConnectionDataDelegate>
/**
* 接收到响应的时候:创建一个空的文件
*/
- (void)connection:(NSURLConnection *)connection didReceiveResponse:(NSHTTPURLResponse *)response {
// 获得文件的总长度
self.contentLength = [response.allHeaderFields[@"Content-Length"] integerValue];
// 创建一个空的文件
[[NSFileManager defaultManager] createFileAtPath:XMGFile contents:nil attributes:nil];
// 创建文件句柄
self.handle = [NSFileHandle fileHandleForWritingAtPath:XMGFile];
}
/**
* 接收到具体数据:马上把数据写入一开始创建好的文件
*/
- (void)connection:(NSURLConnection *)connection didReceiveData:(NSData *)data {
// 指定数据的写入位置 -- 文件内容的最后面
[self.handle seekToEndOfFile];
// 写入数据
[self.handle writeData:data];
// 拼接总长度
self.currentLength += data.length;
// 进度
self.progressView.progress = 1.0 * self.currentLength / self.contentLength;
}
- (void)connectionDidFinishLoading:(NSURLConnection *)connection {
// 关闭handle
[self.handle closeFile];
self.handle = nil;
// 清空长度
self.currentLength = 0;
}
@end

除了用 NSFileHandle 来做到边下载边存到沙盒,还可以使用 NSOutputStream 来将文件写入沙盒。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
@interface ViewController ()<NSURLConnectionDataDelegate>
@property (strong, nonatomic) NSOutputStream *stream;
@end
@implementation ViewController
- (void)viewDidLoad {
[super viewDidLoad];
NSURL *url = [NSURL URLWithString:@"http://120.25.226.186:32812/resources/videos/minion_01.mp4"];
[NSURLConnection connectionWithRequest:[NSURLRequest requestWithURL:url] delegate:self];
}
- (void)connection:(NSURLConnection *)connection didReceiveResponse:(NSURLResponse *)response {
NSString *caches = [NSSearchPathForDirectoriesInDomains(NSCachesDirectory, NSUserDomainMask, YES) lastObject];
// 可以通过 suggestedFilename 拿到建议的文件名称
NSString *cachesPath = [caches stringByAppendingPathComponent:response.suggestedFilename];
NSLog(@"cachesPath = %@", cachesPath);
// 利用 NSOutputStream 往 Path 中写入数据,append = YES 时,每次写入都是追加到文件尾部
self.stream = [[NSOutputStream alloc] initToFileAtPath:cachesPath append:YES];
// 打开流,如果文件不存在的话,会自动创建
[self.stream open];
}
- (void)connection:(NSURLConnection *)connection didReceiveData:(NSData *)data {
[self.stream write:[data bytes] maxLength:data.length];
}
- (void)connectionDidFinishLoading:(NSURLConnection *)connection {
[self.stream close];
NSLog(@"写入完毕,可以去沙盒目录查看文件。");
}
@end

注意:NSURLConnectionDataDelegate 的代理方法是在主线程中执行的(直接在代理方法中打印当前线程即可),如果我们想这些代理方法在子线程执行,可以设置 NSURLConnection 的代理方法是在哪个队列中执行:

1
2
3
4
5
NSURL *url = [NSURL URLWithString:@"http://120.25.226.186:32812/resources/videos/minion_01.mp4"];
NSURLConnection *connection = [NSURLConnection connectionWithRequest:[NSURLRequest requestWithURL:url] delegate:self];
// 决定代理方法在哪个队列中执行
[connection setDelegateQueue:[[NSOperationQueue alloc] init]];

说一点关于 NSURLConnection 坑:

NSURLConnection 的请求方法(connectionWithRequest)放在子线程执行会发现代理方法不会回调,因为 NSURLConnection 想要在请求过程中接收数据,那它得一直监听,因此它会被放到 RunLoop 里,而子线程中默认没有开启 RunLoop,所以 NSURLConnection 的代理方法不会回调。

想要将 NSURLConnection 的请求放在子线程中,就得手动开启 RunLoop。如果想要在请求完毕之后销毁 RunLoop,由于 Foundation 中的 RunLoop 没有销毁的方法,所以得用 Core Foundation 的 RunLoop:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
@interface ViewController ()<NSURLConnectionDataDelegate>
/** RunLoop */
@property (assign, nonatomic) CFRunLoopRef runLoop;
@end
@implementation ViewController
- (void)viewDidLoad {
[super viewDidLoad];
dispatch_async(dispatch_get_global_queue(QOS_CLASS_DEFAULT, 0), ^{
NSURL *url = [NSURL URLWithString:@"http://120.25.226.186:32812/resources/images/minion_15.png"];
NSURLConnection *connection = [NSURLConnection connectionWithRequest:[NSURLRequest requestWithURL:url] delegate:self];
[connection setDelegateQueue:[[NSOperationQueue alloc] init]];
self.runLoop = CFRunLoopGetCurrent();
CFRunLoopRun();
});
}
- (void)connection:(NSURLConnection *)connection didReceiveResponse:(NSURLResponse *)response {
NSLog(@"didReceiveResponse -> %@", [NSThread currentThread]);
}
- (void)connection:(NSURLConnection *)connection didReceiveData:(NSData *)data {
NSLog(@"didReceiveData -> %@", [NSThread currentThread]);
}
- (void)connectionDidFinishLoading:(NSURLConnection *)connection {
NSLog(@"connectionDidFinishLoading -> %@", [NSThread currentThread]);
CFRunLoopStop(self.runLoop);
}
@end

最后说一点,大文件下载,建议使用 NSURLSession 或者第三方框架。