iOS文件斷點(diǎn)下載功能的實(shí)現(xiàn)

一播掷、整體框架及思路
主要涉及兩個(gè)類,
1撼班、DownloadManager歧匈,文件下載管理器是一個(gè)單例,負(fù)責(zé)調(diào)度和管理具體的下載操作权烧。它主要有兩個(gè)屬性眯亦,下載操作緩存(字典類型),以u(píng)rl為key般码,防止相同的下載操作多次執(zhí)行。下載隊(duì)列(NSOperationQueue)乱顾,用來(lái)控制并發(fā)數(shù)板祝,防止隊(duì)列中同時(shí)有過(guò)多的下載操作同時(shí)執(zhí)行。
2走净、DownloadOperation,文件下載操作券时,對(duì)應(yīng)具體的下載操作,繼承于NSOperation伏伯。相關(guān)注意點(diǎn):1橘洞、執(zhí)行下載請(qǐng)求前,先同步發(fā)送一個(gè)HEAD請(qǐng)求说搅,獲取目標(biāo)文件的大小炸枣,經(jīng)過(guò)和本地文件比對(duì)后確定斷點(diǎn)下載的斷點(diǎn)。 2、通過(guò)設(shè)置range請(qǐng)求頭告訴服務(wù)器斷點(diǎn)适肠,這是斷點(diǎn)下載的核心霍衫。3、在循環(huán)接收服務(wù)器返回的文件數(shù)據(jù)過(guò)程中侯养,如果用全局屬性去拼接敦跌,會(huì)造成內(nèi)存浪費(fèi)」淇可以用NSOutputStream解決內(nèi)存峰值問(wèn)題柠傍,每接收一點(diǎn),就寫到本地文件中辩稽。

外界調(diào)動(dòng)示例:

 //要下載的文件的URL
    NSURL *url = [NSURL URLWithString:@"http://dlsw.baidu.com/sw-search-sp/soft/2a/25677/QQ_V4.1.1.1456905733.dmg"];
    //通過(guò)下載管理器單例執(zhí)行下載
    [[downloadManager sharedManager] downloadWith:url pregressBlock:^(CGFloat progress) {
       //進(jìn)度回調(diào)携兵,返回下載進(jìn)度
        dispatch_async(dispatch_get_main_queue(), ^{
            //回到主線程刷新進(jìn)度提醒
            self.progress.progress = progress;
        });
    } complete:^(NSString *path, NSError *error) {
        //完成回調(diào),返回文件路徑或者失敗信息
        NSLog(@"下載完成%@",path);
    }];


實(shí)現(xiàn)代碼:

DownloadManager.m

@interface downloadManager()

/**
 *  下載操作緩存,以u(píng)rl為key搂誉,防止多次下載
 */
@property (nonatomic,strong)NSMutableDictionary *downloadCache;

/**
 *  下載隊(duì)列,可以控制并發(fā)數(shù)
 */
@property (nonatomic,strong)NSOperationQueue *downloadQueue;
@end

@implementation downloadManager

/**
 *  下載管理器單例
 *
 *  @return 返回唯一的實(shí)例
 */
+ (instancetype)sharedManager{
    static dispatch_once_t onceToken;
    static downloadManager *manager;
    dispatch_once(&onceToken, ^{
        manager = [[self alloc]init];
    });
    return manager;
}
/**
 *  下載操作
 *
 *  @param url           要下載的url
 *  @param progressBlock 進(jìn)度回調(diào)
 *  @param complete      完成回調(diào)
 */
- (void)downloadWith:(NSURL *)url pregressBlock:(void (^)(CGFloat progress))progressBlock complete:(void(^)(NSString *path,NSError *error))complete{
    //在開始下載之前徐紧,判斷是否正在下載
    if ([self.downloadCache objectForKey:url]) {
        NSLog(@"正在下載");
        return;
    }
    //開始下載文件
    downloadOperation *download = [downloadOperation downloadWith:url progressBlock:progressBlock complete:^(NSString *path, NSError *error) {
        //下載完成
        //移除下載操作緩存
        [self.downloadCache removeObjectForKey:url];
        //調(diào)用控制器傳過(guò)來(lái)的完成回調(diào)
        complete(path,error);
    }];
    //把下載操作保存起來(lái)
    [self.downloadCache setObject:download forKey:url];
    //把操作發(fā)到隊(duì)列中
    [self.downloadQueue addOperation:download];
}

- (void)cancelDownload:(NSURL *)url{
    //從字典中取出下載操作
    downloadOperation *down = [self.downloadCache objectForKey:url];
    [down cancleDown];
    //從字典中移除
    [self.downloadCache removeObjectForKey:url];
}
#pragma mark -數(shù)據(jù)懶加載
- (NSMutableDictionary *)downloadCache{
    if (_downloadCache == nil) {
        _downloadCache = [NSMutableDictionary dictionary];
    }
    return _downloadCache;
}

- (NSOperationQueue *)downloadQueue{
    if (_downloadQueue == nil) {
        _downloadQueue = [[NSOperationQueue alloc]init];
        //設(shè)置最大并發(fā)數(shù)
        _downloadQueue.maxConcurrentOperationCount = 3;
    }
    return _downloadQueue;
}
@end

DownloadOperation.m

@interface downloadOperation()
/**
 *  文件總大小
 */
@property (nonatomic,assign)long long expectLength;
/**
 *  已經(jīng)下載的大小
 */
@property (nonatomic,assign)long long downloadLength;

/**
 *  文件輸出流
 */
@property (nonatomic,strong)NSOutputStream *outPut;

/**
 *  下載連接
 */
@property (nonatomic,strong)NSURLConnection *connection;

/**
 *  進(jìn)度回調(diào)block
 */
@property (nonatomic,copy)void (^progressBlock)(CGFloat);

/**
 *  完成回調(diào)block
 */
@property (nonatomic,copy)void (^complete)(NSString *,NSError *);

/**
 *  文件保存的路徑
 */
@property (nonatomic,copy)NSString *targetPath;
/**
 *  <#Description#>
 */
@property (nonatomic,strong)NSURL *url;
@end

@implementation downloadOperation

+ (instancetype)downloadWith:(NSURL *)url progressBlock:(void (^)(CGFloat progress))progressBlock complete:(void (^)(NSString *path,NSError *error))complete{
    downloadOperation *down = [[self alloc]init];
    //把block保存起來(lái)
    down.progressBlock = progressBlock;
    down.complete = complete;
    //拼接文件保存的路徑
    down.targetPath = [url.path appendCaches];
    down.url = url;
    //調(diào)用下載方法
    //[down download:url];
    return down;
}

- (void)main{
    @autoreleasepool {
        [self download];
    }
}
- (void)download{
   
        NSURL *url = self.url;
        //斷點(diǎn)下載,如果完全相同炭懊,不下載并级。否則,從fileSize開始下載
        long long fileSize = [self checkServerFileSize:url];
        if (fileSize == self.expectLength) {
            return ;
        }
        NSMutableURLRequest *request = [NSMutableURLRequest requestWithURL:url];
        //斷點(diǎn)續(xù)傳必須設(shè)置range請(qǐng)求頭
        NSString *range = [NSString stringWithFormat:@"bytes=%lld-",fileSize];
        //下載進(jìn)度就是本地文件的大小
        self.downloadLength = fileSize;
        //設(shè)置請(qǐng)求頭
        [request setValue:range forHTTPHeaderField:@"range"];
        
        //開始下載,通過(guò)遵守代理協(xié)議侮腹,回調(diào)下載過(guò)程
        [NSURLConnection connectionWithRequest:request delegate:self];
        //如果子線程在執(zhí)行完當(dāng)前代碼后需要一直保持的嘲碧,需要把runloop跑起來(lái)
        [[NSRunLoop currentRunLoop] run];
    
}
//檢查服務(wù)器上文件的大小
- (long long)checkServerFileSize:(NSURL *)url{
    //要下載文件的url
    //NSURL *url = [NSURL URLWithString:@"http://dlsw.baidu.com/sw-search-sp/soft/2a/25677/QQ_V4.1.1.1456905733.dmg"];
    //創(chuàng)建獲取文件大小的請(qǐng)求
    NSMutableURLRequest *headRequest = [NSMutableURLRequest requestWithURL:url];
    //請(qǐng)求方法
    headRequest.HTTPMethod = @"HEAD";
    //創(chuàng)建一個(gè)響應(yīng)頭
    NSURLResponse *headResponse;
    [NSURLConnection sendSynchronousRequest:headRequest returningResponse:&headResponse error:nil];
    long long serverSize = headResponse.expectedContentLength;
    //記錄服務(wù)器文件的大小,用于計(jì)算進(jìn)度
    self.expectLength = serverSize;
    //拼接文件路徑
    NSString *path = [headResponse.suggestedFilename appendCaches];
    //判斷服務(wù)器文件大小跟本地文件大小的關(guān)系
    NSFileManager *manager = [NSFileManager defaultManager];
    if (![manager fileExistsAtPath:path]) {
        NSLog(@"不存在父阻,從頭開始下載");
        return 0;
    }
    //獲取文件的屬性
    NSDictionary *dict = [manager attributesOfItemAtPath:path error:nil];
    //獲取本地文件的大小
    long long fileSize = dict.fileSize;
    if (fileSize > serverSize) {
        //文件出錯(cuò)愈涩,刪除文件
        [manager removeItemAtPath:path error:nil];
        NSLog(@"從頭開始");
        return 0;
    }else if(fileSize < serverSize){
        NSLog(@"從本地文件大小開始下載");
        return fileSize;
    }else{
        NSLog(@"已經(jīng)下載完畢");
        return fileSize;
    }
}

#pragma mark -下載回調(diào)
//獲得相應(yīng),響應(yīng)頭中包含了文件總大小的信息
- (void)connection:(NSURLConnection *)connection didReceiveResponse:(NSURLResponse *)response
{
    //    //設(shè)置文件總大小
    //    self.expectLength = response.expectedContentLength;
    NSString *path = self.targetPath;
    //不需要手動(dòng)去創(chuàng)建,如果文件不存在加矛,自動(dòng)創(chuàng)建
    self.outPut = [NSOutputStream outputStreamToFileAtPath:path append:YES];
    //在入文件之前履婉,先打開文件
    [self.outPut open];
}
//接收到數(shù)據(jù)
- (void)connection:(NSURLConnection *)connection didReceiveData:(NSData *)data{
    //分多次接收數(shù)據(jù),加起來(lái)就是總數(shù)據(jù)
    self.downloadLength += data.length;
    //計(jì)算當(dāng)前下載進(jìn)度
    CGFloat progress = self.downloadLength *1.0 / self.expectLength;
    //NSLog(@"當(dāng)前進(jìn)度%f",progress);
    
    //進(jìn)度回調(diào)調(diào)用的頻率非常高斟览,我們可以在子線程回調(diào)毁腿。用戶是否要刷新UI,由自己決定
    if (self.progressBlock) {
        self.progressBlock(progress);
    }
    
    //保存每次接收到的數(shù)據(jù)
    //#warning 每次的數(shù)據(jù)都保存在內(nèi)存中苛茂,消耗大量?jī)?nèi)存
    //    [self.data appendData:data];
    //解決方法:使用文件句柄
    
    [self.outPut write:data.bytes maxLength:data.length];
    
}
//下載完成
- (void)connectionDidFinishLoading:(NSURLConnection *)connection{
    //關(guān)閉文件
    [self.outPut close];
    //下載完成后已烤,自動(dòng)回到主線程
    dispatch_async(dispatch_get_global_queue(0, 0), ^{
        self.complete(self.targetPath,nil);
    });
}
//取消下載
- (void)cancleDown{
    [self.connection cancel];
}
@end

其他
NSString+Path.m

@implementation NSString (Path)
- (NSString *)appendCaches {
    // 獲取緩存目錄
    NSString *cache = [NSSearchPathForDirectoriesInDomains(NSCachesDirectory, NSUserDomainMask, YES) lastObject];
    // 獲取路徑的最后一部份(文件名)
    NSString *filename = [self lastPathComponent];
    
    return [cache stringByAppendingPathComponent:filename];
}

- (NSString *)appendDocuments {
    // 獲取緩存目錄
    NSString *document = [NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES) lastObject];
    // 獲取路徑的最后一部份(文件名)
    NSString *filename = [self lastPathComponent];
    
    return [document stringByAppendingPathComponent:filename];
}

- (NSString *)appendTmp {
    // 獲取緩存目錄
    NSString *temp = NSTemporaryDirectory();
    // 獲取路徑的最后一部份(文件名)
    NSString *filename = [self lastPathComponent];
    
    return [temp stringByAppendingPathComponent:filename];
}

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個(gè)濱河市妓羊,隨后出現(xiàn)的幾起案子胯究,更是在濱河造成了極大的恐慌,老刑警劉巖躁绸,帶你破解...
    沈念sama閱讀 216,651評(píng)論 6 501
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件裕循,死亡現(xiàn)場(chǎng)離奇詭異臣嚣,居然都是意外死亡,警方通過(guò)查閱死者的電腦和手機(jī)费韭,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,468評(píng)論 3 392
  • 文/潘曉璐 我一進(jìn)店門茧球,熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái),“玉大人星持,你說(shuō)我怎么就攤上這事抢埋。” “怎么了督暂?”我有些...
    開封第一講書人閱讀 162,931評(píng)論 0 353
  • 文/不壞的土叔 我叫張陵揪垄,是天一觀的道長(zhǎng)。 經(jīng)常有香客問(wèn)我逻翁,道長(zhǎng)饥努,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 58,218評(píng)論 1 292
  • 正文 為了忘掉前任八回,我火速辦了婚禮酷愧,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘缠诅。我一直安慰自己溶浴,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,234評(píng)論 6 388
  • 文/花漫 我一把揭開白布管引。 她就那樣靜靜地躺著士败,像睡著了一般。 火紅的嫁衣襯著肌膚如雪褥伴。 梳的紋絲不亂的頭發(fā)上谅将,一...
    開封第一講書人閱讀 51,198評(píng)論 1 299
  • 那天,我揣著相機(jī)與錄音重慢,去河邊找鬼饥臂。 笑死,一個(gè)胖子當(dāng)著我的面吹牛伤锚,可吹牛的內(nèi)容都是我干的擅笔。 我是一名探鬼主播,決...
    沈念sama閱讀 40,084評(píng)論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼屯援,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼!你這毒婦竟也來(lái)了念脯?” 一聲冷哼從身側(cè)響起狞洋,我...
    開封第一講書人閱讀 38,926評(píng)論 0 274
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤,失蹤者是張志新(化名)和其女友劉穎绿店,沒(méi)想到半個(gè)月后吉懊,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體庐橙,經(jīng)...
    沈念sama閱讀 45,341評(píng)論 1 311
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,563評(píng)論 2 333
  • 正文 我和宋清朗相戀三年借嗽,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了态鳖。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 39,731評(píng)論 1 348
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡恶导,死狀恐怖浆竭,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情惨寿,我是刑警寧澤邦泄,帶...
    沈念sama閱讀 35,430評(píng)論 5 343
  • 正文 年R本政府宣布,位于F島的核電站裂垦,受9級(jí)特大地震影響顺囊,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜蕉拢,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,036評(píng)論 3 326
  • 文/蒙蒙 一特碳、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧晕换,春花似錦午乓、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,676評(píng)論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)。三九已至恕汇,卻和暖如春腕唧,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背瘾英。 一陣腳步聲響...
    開封第一講書人閱讀 32,829評(píng)論 1 269
  • 我被黑心中介騙來(lái)泰國(guó)打工枣接, 沒(méi)想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人缺谴。 一個(gè)月前我還...
    沈念sama閱讀 47,743評(píng)論 2 368
  • 正文 我出身青樓但惶,卻偏偏與公主長(zhǎng)得像,于是被迫代替她去往敵國(guó)和親湿蛔。 傳聞我的和親對(duì)象是個(gè)殘疾皇子膀曾,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 44,629評(píng)論 2 354

推薦閱讀更多精彩內(nèi)容