写一个基于NSURLSession的网络下载库

前段时间AFNetworking 更新到3.0 ,彻底废弃NSURLConnection的API转由NSURLSession来实现,代码并没有改动很大,AF封装的很好了,读完源码感觉收获很大。

下载不可避免的会遇到多线程异步下载问题,iOS中多线程解决方案一般是GCD和NSOperation。为了下载器便于管理,这里是创建一个类继承于NSOperation重写其start方法即可(GCD版本基本一致,最后会提到其实现代码和思路)

相关代码和下载库地址都在这里:https://github.com/LeeBlaze/LNDownload.git

NSOperation 版本

 1 typedef NS_ENUM(NSInteger, DownloadState){
 2     
 3     DownloadState_Ready = 0,
 4     
 5     DownloadState_Suspend,
 6     
 7     DownloadState_Doing,
 8     
 9     DownloadState_Success,
10     
11     DownloadState_Cancel,
12     
13     DownloadState_Fail,
14 };

 

首先定义了下载状态的枚举类型,不多解释,下面看看相关的属性

@protocol LNDownloaderDelegate ;

@interface LNDownloader : NSOperation

@property (nonatomic, weak)id<LNDownloaderDelegate>delegate;
@property (nonatomic, assign,readonly) DownloadState state;

@property (nonatomic, assign,readonly) float downloadRate;

@property (nonatomic, assign,readonly) float progress;

@property (nonatomic, copy, readonly)NSURL *downloadURL;

@property (nonatomic, copy, readonly)NSString *downloadPath;

@property (nonatomic, copy, readonly)NSString *filePath;
@property (nonatomic, copy, readwrite)NSString *fileName;

@property (nonatomic, strong, readonly) NSMutableURLRequest *fileRequest;

@property (readonly,getter=isSuspend) BOOL isSuspend;

 

 一开始声明了一个代理,用来提供下载进度,下载状态和下载地址等信息,相关代码待会再说。

可以看到几乎所有属性都是只读状态,这里避免用户对下载状态,进度,下载地址等进行更改,由下载库统一管理,只暴露出相关只读属性返回当前任务状态进度等相关信息(只读属性的赋值参见KVO

在下载回调时,一般有两种常用方式,代理和block,AF中采用了block得方式,这里提供两种方式:

代理和block

/*
 
 利用代理回调的初始化方式;
 
 */

- (instancetype) initWithDownloadURL:(NSURL *)url
                        downloafPath:(NSString *)path;

/*

 

 block回调的初始化方式;

 

 */

 

- (instancetype) initWithDownloadURL:(NSURL *)url

                        downloafPath:(NSString *)path

                            progress:(void (^)(int64_t writtenByte,int64_t totalByte,float progress))progress

                               error:(void (^)(NSError *error))error

                            complete:(void (^)(BOOL downloadFinished, NSURLSessionDownloadTask *task))completBlock;

当然还要有下载的开启,取消,删除等方法:

- (void)cancelDownloaderAndRemoveFile:(BOOL)remove;

- (void)pause;

- (void)resume;

下载库写的比较简略,一些对于错误的处理没有涉及,实际项目中可以加上。

.m文件中:

//  Copyright © 2015年 Lee. All rights reserved.
//


#define FILE_MANAGER   [NSFileManager defaultManager]
#import "LNDownloader.h"


@interface LNDownloader()<NSURLSessionDownloadDelegate>


@property (nonatomic, strong) NSMutableURLRequest *fileRequest;
@property (nonatomic, copy)   NSURL               *downloadURL;
@property (nonatomic, copy)   NSString            *downloadPath;
@property (nonatomic, assign) DownloadState        state;
@property (nonatomic, assign) float               progress;
@property (nonatomic, strong) NSMutableData       *receiveData;
@property (nonatomic, strong) NSData       *resumeData;

@property (nonatomic, assign)int64_t                  writtenByte;
@property (nonatomic, assign)int64_t                  expectTotalByte;
@property (nonatomic, strong)NSURLSession             *session;
@property (nonatomic, strong)NSURLSessionDownloadTask *downloadTask;

@property (nonatomic,copy)void(^progressBlock)(int64_t writtenByte,int64_t totalByte,float progress);
@property (nonatomic,copy)void(^errorBlock)(NSError *error);
@property (nonatomic,copy)void(^completeBlock)(BOOL downloadFinished, NSURLSessionDownloadTask *task);

这里定义了三个block,第一个用来回调下载进度,第二个是下载失败的回调,第三个是下载完成的回调。因为我们在.h文件中声明了一个isSuspend属性,算是对NSOperation属性的一个扩充吧,在.m中我们要实现其getter方法,因此我们需要@dynamic  isSuspend

 1 @dynamic isSuspend;
 2 #pragma mark  init
 3 - (instancetype)initWithDownloadURL:(NSURL *)url
 4                        downloafPath:(NSString *)path
 5 {
 6     self = [super init];
 7     if (self)
 8     {
 9         self.downloadURL = url;
10         self.state = DownloadState_Ready;
11         self.downloadPath = path;
12         self.fileRequest = [[NSMutableURLRequest alloc] initWithURL:url
13                                                         cachePolicy:NSURLRequestUseProtocolCachePolicy timeoutInterval:30];
14     }
15     return self;
16 }
17 
18 - (instancetype)initWithDownloadURL:(NSURL *)url
19                        downloafPath:(NSString *)path
20                            progress:(void (^)(int64_t, int64_t,float))progress
21                               error:(void (^)(NSError *))error
22                            complete:(void (^)(BOOL, NSURLSessionDownloadTask *))completBlock
23 {
24     self = [self initWithDownloadURL:url downloafPath:path];
25     if (self)
26     {
27         self.progressBlock = progress;
28         self.errorBlock    = error;
29         self.completeBlock = completBlock;
30     }
31     return self;
32 }

在初始化方法中,分别对downloadUrl state downloadPath和fileRequest等进行了赋值和request的初始化,对于一些progress state属性等是readonly声明,所以在内部赋值的时候需要使用KVC

- (void)start
{
    [self willChangeValueForKey:@"isExecuting"];
    self.state = DownloadState_Doing;
    [self didChangeValueForKey:@"isExecuting"];
    
    self.session = [NSURLSession sessionWithConfiguration:[NSURLSessionConfiguration defaultSessionConfiguration]
                                                     delegate:self
                                                delegateQueue:nil];

     self.downloadTask = [self.session downloadTaskWithRequest:self.fileRequest];
    [self.downloadTask resume];
}

在重写resmue的时候因为可能是暂停下载任务继续下载的情况,所以通过判断resumeData是否为空来获得或新建对应的task.下载的进度和状态完成情况都是通过代理回调给我们需要注意的是

- (void)URLSession:(NSURLSession *)session

              task:(NSURLSessionTask *)task

didCompleteWithError:(NSError *)error这个方法在我们暂停或者下载失败的时候都会回调,因此我们需要通过当前task的状态进行判断:

    if (error)
    {
        if (self.state == DownloadState_Suspend)
        {
            
        }
        else
        {
           [self downloadWithError:error task:self.downloadTask];
        }
        
    }
   

  在暂停时,这里没有做任何处理。

下载的progress和状态等通过通知和代理传值,监听相关的通知即可获取需要的所有信息:

- (void)URLSession:(NSURLSession *)session
      downloadTask:(NSURLSessionDownloadTask *)downloadTask
      didWriteData:(int64_t)bytesWritten
 totalBytesWritten:(int64_t)totalBytesWritten
totalBytesExpectedToWrite:(int64_t)totalBytesExpectedToWrite
{
    self.writtenByte = totalBytesWritten;
    self.expectTotalByte = totalBytesExpectedToWrite;
   
    dispatch_async(dispatch_get_main_queue(), ^{
        if (self.progressBlock) {
            self.progressBlock(totalBytesWritten,totalBytesExpectedToWrite,self.progress);
        }
        if (self.writtenByte *1.0 / self.expectTotalByte > 0.01 || self.writtenByte *1.0 /self.expectTotalByte >= 0.99)
        {
             self.progress = (float)self.writtenByte / self.expectTotalByte;
            if ([self.delegate respondsToSelector:@selector(download:progress:)])
            {
                [self.delegate download:self progress:self.progress];
            }
        }
        
    });
}

- (void)downloadWithError:(NSError *)error task:(NSURLSessionDownloadTask *)task
{
    if (error)
    {
        dispatch_async(dispatch_get_main_queue(), ^{
            if (self.errorBlock)
            {
                self.errorBlock(error);
            }
            if ([self.delegate respondsToSelector:@selector(download:didStopWithError:)]) {
                [self.delegate download:self didStopWithError:error];
            }
        });
    }
    BOOL success = error == nil;
    
  
    dispatch_async(dispatch_get_main_queue(), ^{
            
        if (self.completeBlock)
        {
            self.completeBlock(success,task);
        }
        if (self.state == DownloadState_Suspend) {
            return ;
        }
        if ([self.delegate respondsToSelector:@selector(download:didFinishWithSuccess:atPath:)]) {
            [self.delegate download:self didFinishWithSuccess:success atPath:[self.downloadPath stringByAppendingPathComponent:self.fileName]];
        }

    });
    if (self.state == DownloadState_Suspend) {
        return ;
    }
    DownloadState state = success ? DownloadState_Success :DownloadState_Fail;
    [self cancelAndSetDownloadStateWhenDownloadFinish:state];
}

- (void)cancelAndSetDownloadStateWhenDownloadFinish:(DownloadState)state
{
    [self.downloadTask cancel];
    [self willChangeValueForKey:@"isFinished"];
    [self willChangeValueForKey:@"isExecuting"];
    self.state = state;
    [self didChangeValueForKey:@"isExecuting"];
    [self didChangeValueForKey:@"isFinished"];
}
- (void)cancelDownloaderAndRemoveFile:(BOOL)remove
{
    [self.downloadTask cancel];
    self.delegate = nil;
    [self removeFile];
    [self cancel];
}

  

 

posted @ 2016-03-25 14:28  那年明月  阅读(325)  评论(0编辑  收藏  举报