ios:NSURLSessionDataTask做文件断点下载

之前用afn2.x的AFHttpOperation结合sqlite数据库管理做了文件的断点下载功能,之后苹果宣布要开始限制ipv4,不过AFN的东西时给予high-level的APIs的,因此不需要修改,但是国外的开发者建议使用AFN3.0版本。

闲来无事就想重新集成一下,迁移AFN3.0的时候因为没有了HTTPOperation,所以在修改代码的时候全部用NSURLSessionDowonloadTask代替,不过由于之前的数据库逻辑已经定型,且多处使用,修改起来比较复杂,DownloadTask是先下载临时文件,下载完成后再迁移到指定文件夹,并不能通过range来指定下载位置的起始,如果用户直接杀死App,又需要记住resumeData来重新下载,这样在多线程同时下载多个的时候集成出了问题,可能是我逻辑没有屡通,总觉的这样修改起来比较费力。

最后我完全摒弃了AFN,改而实用系统提供的URLSession和URLSessionDataTask及它的代理方法来实现,这样不需要修改现存的数据库逻辑,只需要修改下载暂停继续这部分的控制。

感谢大神提供:https://github.com/HHuiHao/HSDownloadManager。

首先用一个单例类来管理下载,单例类并不存储下载内容的数据,数据只在函数之间传递。

<p class="p1"><span class="s1">/**</span></p><p class="p1"><span class="s1"> *  </span><span class="s2">单例</span></p><p class="p1"><span class="s1"> *</span></p><p class="p1"><span class="s1"> *  @return </span><span class="s2">返回单例对象</span></p><p class="p1"><span class="s1"> */</span></p><p class="p2"><span class="s1">+ (</span><span class="s3">instancetype</span><span class="s1">)sharedInstance;</span></p><p class="p3"><span class="s1"></span>
</p><p class="p1"><span class="s1">/**</span></p><p class="p4"><span class="s4"> *  </span><span class="s1">开启任务下载资源</span></p><p class="p1"><span class="s1"> *</span></p><p class="p1"><span class="s1"> *  @param model         </span><span class="s2">下载参数</span></p><p class="p1"><span class="s1"> *  @param progressBlock </span><span class="s2">回调下载进度</span></p><p class="p1"><span class="s1"> *  @param stateBlock    </span><span class="s2">下载状态</span></p><p class="p1"><span class="s1"> */</span></p><p class="p2"><span class="s1">- (</span><span class="s3">void</span><span class="s1">)download:(</span><span class="s5">DownloadModel</span><span class="s1"> *)model progress:(</span><span class="s3">void</span><span class="s1">(^)(</span><span class="s6">NSInteger</span><span class="s1"> receivedSize, </span><span class="s6">NSInteger</span><span class="s1"> expectedSize, </span><span class="s6">CGFloat</span><span class="s1"> progress))progressBlock state:(</span><span class="s3">void</span><span class="s1">(^)(</span><span class="s5">DownloadState</span><span class="s1"> state))stateBlock;</span></p><p class="p3"><span class="s1"></span>
</p>

model包含下载文件的参数,对应创建的sqlite数据库的表,可以在内重组文件名,从而保证拿到已下载大小的range,重新创建task继续下载,我使用的方法也是居于上面的git连接修改的,因为要适用自己的项目,其他大概都差不多,大家可以先看看git项目,相信可以满足大家的大部分需求。

在作者的session类中,我也添加了doanloadModel的引用,从而方便在代理方法中拿到文件路径相关的参数做比较及存值。

@property (nonatomic, copy) NSString *fileName;

@property (nonatomic,strong)DownloadModel *model;

文笔拙劣,其实写个博客也是想为了给自己留个笔记,方便以后有用的话不需要在翻来翻去。直接贴代码吧,还惦记着NBA总决赛呢,大家可以参考我上面给的git项目地址,作者写的很好。

TTDownloadManager

//
//  TTDownloadManager.h
//  DownloadDemo
//
//  Created by qihb on 16/6/3.
//  Copyright © 2016年 Qihb. All rights reserved.
//

#import <Foundation/Foundation.h>
#import "TTSessionModel.h"
#import "DownloadModel.h"

@interface TTDownloadManager : NSObject

/**
 *  单例
 *
 *  @return 返回单例对象
 */
+ (instancetype)sharedInstance;

/**
 *  开启任务下载资源
 *
 *  @param model         下载参数
 *  @param progressBlock 回调下载进度
 *  @param stateBlock    下载状态
 */
- (void)download:(DownloadModel *)model progress:(void(^)(NSInteger receivedSize, NSInteger expectedSize, CGFloat progress))progressBlock state:(void(^)(DownloadState state))stateBlock;

/**
 *  查询该资源的下载进度值
 *
 *  @param url 下载地址
 *
 *  @return 返回下载进度值
 */
- (CGFloat)progress:(NSString *)fileName;

/**
 *  获取该资源总大小
 *
 *  @param url 下载地址
 *
 *  @return 资源总大小
 */
- (NSInteger)fileTotalLength:(NSString *)url;

/**
 *  判断该资源是否下载完成
 *
 *  @param url 下载地址
 *
 *  @return YES: 完成
 */
- (BOOL)isCompletion:(NSString *)url;

/**
 *  删除该资源
 *
 *  @param url 下载地址
 */
- (void)deleteFile:(NSString *)url;

/**
 *  清空所有下载资源
 */
- (void)deleteAllFile;

/**
 *  暂停所有下载
 */
-(void)pauseAllTask;

/**
 *  取消下载
 *
 */
-(void)cancelTaskWithModel:(DownloadModel *)model;

@end

.m

//
//  TTDownloadManager.m
//  DownloadDemo
//
//  Created by qihb on 16/6/3.
//  Copyright © 2016年 Qihb. All rights reserved.
//

#import "TTDownloadManager.h"
#import "NSString+Hash.h"
// 缓存主目录
#define TTCachesDirectory [[NSSearchPathForDirectoriesInDomains(NSCachesDirectory, NSUserDomainMask, YES) lastObject] stringByAppendingPathComponent:@"TTCache"]

// 保存文件名
#define TTFileKey(url) url.md5String

// 文件的存放路径(caches)
#define TTFileFullpath(url) [TTCachesDirectory stringByAppendingPathComponent:TTFileName(url)]

// 文件的已下载长度
#define TTDownloadLength(name) [[[NSFileManager defaultManager] attributesOfItemAtPath:SAVE_MODEL_PATH(name) error:nil][NSFileSize] integerValue]

// 存储文件总长度的文件路径(caches)
#define TTTotalLengthFullpath [TTCachesDirectory stringByAppendingPathComponent:@"totalLength.plist"]

@interface TTDownloadManager ()<NSCopying, NSURLSessionDelegate>

/** 保存所有任务(注:用md5后作为key) */
@property (nonatomic, strong) NSMutableDictionary *tasks;
/** 保存所有下载相关信息 */
@property (nonatomic, strong) NSMutableDictionary *sessionModels;

@end

@implementation TTDownloadManager

- (NSMutableDictionary *)tasks
{
    if (!_tasks) {
        _tasks = [NSMutableDictionary dictionary];
    }
    return _tasks;
}

- (NSMutableDictionary *)sessionModels
{
    if (!_sessionModels) {
        _sessionModels = [NSMutableDictionary dictionary];
    }
    return _sessionModels;

}

static TTDownloadManager *_downloadManager;

+ (instancetype)allocWithZone:(struct _NSZone *)zone
{
    static dispatch_once_t onceToken;

    dispatch_once(&onceToken, ^{
        _downloadManager = [super allocWithZone:zone];
    });

    return _downloadManager;
}

- (nonnull id)copyWithZone:(nullable NSZone *)zone
{
    return _downloadManager;
}

+ (instancetype)sharedInstance
{
    static dispatch_once_t onceToken;
    dispatch_once(&onceToken, ^{
        _downloadManager = [[self alloc] init];
    });

    return _downloadManager;
}

/**
 *  创建缓存目录文件
 */
- (void)createCacheDirectory
{
    NSFileManager *fileManager = [NSFileManager defaultManager];
    if (![fileManager fileExistsAtPath:TTCachesDirectory]) {
        [fileManager createDirectoryAtPath:TTCachesDirectory withIntermediateDirectories:YES attributes:nil error:NULL];
    }
}

/**
 *  开启任务下载资源
 */
- (void)download:(DownloadModel *)model progress:(void (^)(NSInteger, NSInteger, CGFloat))progressBlock state:(void (^)(DownloadState))stateBlock
{
    NSString *model_url = model.model_url;
    NSString *fileName = [NSString stringWithFormat:@"%@.%@",model.model_md5_str,model.model_format];

    if (!model_url) return;
    if ([self isCompletion:fileName]) {
        stateBlock(DownloadStateCompleted);
        NSLog(@"----该资源已下载完成");
        return;
    }

    // 暂停
    if ([self.tasks valueForKey:fileName]) {
        [self handle:fileName];

        return;
    }

    // 创建缓存目录文件
    [self createCacheDirectory];

    NSURLSession *session = [NSURLSession sessionWithConfiguration:[NSURLSessionConfiguration defaultSessionConfiguration] delegate:self delegateQueue:[[NSOperationQueue alloc] init]];

    NSString *toPath = SAVE_MODEL_PATH(fileName);

    // 创建流
    NSOutputStream *stream = [NSOutputStream outputStreamToFileAtPath:toPath append:YES];

    // 创建请求
    NSMutableURLRequest *request = [NSMutableURLRequest requestWithURL:[NSURL URLWithString:model_url]];

    // 设置请求头
    NSString *range = [NSString stringWithFormat:@"bytes=%zd-", TTDownloadLength(fileName)];
    [request setValue:range forHTTPHeaderField:@"Range"];

    // 创建一个Data任务
    NSURLSessionDataTask *task = [session dataTaskWithRequest:request];
    NSTimeInterval time = [[NSDate date] timeIntervalSince1970];
    NSUInteger taskIdentifier = (unsigned long)time;
    [task setValue:@(taskIdentifier) forKeyPath:@"taskIdentifier"];

    // 保存任务
    [self.tasks setValue:task forKey:fileName];

    TTSessionModel *sessionModel = [[TTSessionModel alloc] init];
    sessionModel.url = model_url;
    sessionModel.fileName = fileName;
    sessionModel.model = model;
    sessionModel.progressBlock = progressBlock;
    sessionModel.stateBlock = stateBlock;
    sessionModel.stream = stream;
    [self.sessionModels setValue:sessionModel forKey:@(task.taskIdentifier).stringValue];

    [self start:fileName];
}

- (void)handle:(NSString *)key
{
    NSURLSessionDataTask *task = [self getTask:key];
    if (task.state == NSURLSessionTaskStateRunning) {
        [self pause:key];
    } else {
        [self start:key];
    }
}

/**
 *  开始下载
 */
- (void)start:(NSString *)key
{
    NSURLSessionDataTask *task = [self getTask:key];
    [task resume];

    [self getSessionModel:task.taskIdentifier].stateBlock(DownloadStateStart);
}

/**
 *  暂停下载
 */
- (void)pause:(NSString *)key
{
    NSURLSessionDataTask *task = [self getTask:key];
    [task suspend];

    [self getSessionModel:task.taskIdentifier].stateBlock(DownloadStateSuspended);
}

/**
 *  根据url获得对应的下载任务
 */
- (NSURLSessionDataTask *)getTask:(NSString *)key
{
    return (NSURLSessionDataTask *)[self.tasks valueForKey:key];
}

/**
 *  根据url获取对应的下载信息模型
 */
- (TTSessionModel *)getSessionModel:(NSUInteger)taskIdentifier
{
    return (TTSessionModel *)[self.sessionModels valueForKey:@(taskIdentifier).stringValue];
}

/**
 *  判断该文件是否下载完成
 */
- (BOOL)isCompletion:(NSString *)fileName
{
    if ([self fileTotalLength:fileName] && TTDownloadLength(fileName) == [self fileTotalLength:fileName]) {
        return YES;
    }
    return NO;
}

/**
 *  查询该资源的下载进度值
 */
- (CGFloat)progress:(NSString *)fileName
{
    return [self fileTotalLength:fileName] == 0 ? 0.0 : 1.0 * TTDownloadLength(fileName) /  [self fileTotalLength:fileName];
}

/**
 *  获取该资源总大小
 */
- (NSInteger)fileTotalLength:(NSString *)fileName
{
    return [[NSDictionary dictionaryWithContentsOfFile:TTTotalLengthFullpath][fileName] integerValue];
}

#pragma mark - 删除
/**
 *  删除该资源
 */
- (void)deleteFile:(NSString *)fileName
{
    NSFileManager *fileManager = [NSFileManager defaultManager];
    if ([fileManager fileExistsAtPath:SAVE_MODEL_PATH(fileName)]) {

        // 删除沙盒中的资源
        [fileManager removeItemAtPath:SAVE_MODEL_PATH(fileName) error:nil];
        // 删除任务
        [self.tasks removeObjectForKey:fileName];
        [self.sessionModels removeObjectForKey:@([self getTask:fileName].taskIdentifier).stringValue];
        // 删除资源总长度
        if ([fileManager fileExistsAtPath:TTTotalLengthFullpath]) {

            NSMutableDictionary *dict = [NSMutableDictionary dictionaryWithContentsOfFile:TTTotalLengthFullpath];
            [dict removeObjectForKey:fileName];
            [dict writeToFile:TTTotalLengthFullpath atomically:YES];

        }
    }
}

/**
 *  暂停所有下载
 */
-(void)pauseAllTask{
    NSArray *allKeys = [self.tasks allKeys];
    if (allKeys&&allKeys.count>0) {
        for (int i=0; i<allKeys.count; i++) {
            NSString *key = [allKeys objectAtIndex:i];
            NSURLSessionDataTask *task = [self getTask:key];
            [task suspend];
            TTSessionModel *sessionModel =[self getSessionModel:task.taskIdentifier];
            sessionModel.model.model_download_flag = @"2";
            [sessionModel.model saveOrUpdate];
        }
    }
}

/**
 *  取消下载
 *
 */
-(void)cancelTaskWithModel:(DownloadModel *)model{
    NSString *key = [NSString stringWithFormat:@"%@.%@",model.model_md5_str,model.model_format];
    NSURLSessionDataTask *task = [self getTask:key];
    [task cancel];

    [self deleteFile:key];
}

#pragma mark - 代理
#pragma mark NSURLSessionDataDelegate
/**
 * 接收到响应
 */
- (void)URLSession:(NSURLSession *)session dataTask:(NSURLSessionDataTask *)dataTask didReceiveResponse:(NSHTTPURLResponse *)response completionHandler:(void (^)(NSURLSessionResponseDisposition))completionHandler
{

    TTSessionModel *sessionModel = [self getSessionModel:dataTask.taskIdentifier];

    // 打开流
    [sessionModel.stream open];

    // 获得服务器这次请求 返回数据的总长度
    NSInteger totalLength = [response.allHeaderFields[@"Content-Length"] integerValue] + TTDownloadLength(sessionModel.fileName);
    sessionModel.totalLength = totalLength;

    // 存储总长度
    NSMutableDictionary *dict = [NSMutableDictionary dictionaryWithContentsOfFile:TTTotalLengthFullpath];
    if (dict == nil) dict = [NSMutableDictionary dictionary];
    dict[sessionModel.fileName] = @(totalLength);
    [dict writeToFile:TTTotalLengthFullpath atomically:YES];

    // 接收这个请求,允许接收服务器的数据
    completionHandler(NSURLSessionResponseAllow);
}

/**
 * 接收到服务器返回的数据
 */
- (void)URLSession:(NSURLSession *)session dataTask:(NSURLSessionDataTask *)dataTask didReceiveData:(NSData *)data
{
    TTSessionModel *sessionModel = [self getSessionModel:dataTask.taskIdentifier];

    // 写入数据
    [sessionModel.stream write:(uint8_t *)data.bytes maxLength:data.length];

    // 下载进度
    NSUInteger receivedSize = TTDownloadLength(sessionModel.fileName);
    NSUInteger expectedSize = sessionModel.totalLength;
    CGFloat progress = 1.0 * receivedSize / expectedSize;

    sessionModel.progressBlock(receivedSize, expectedSize, progress);
}

/**
 * 请求完毕(成功|失败)
 */
- (void)URLSession:(NSURLSession *)session task:(NSURLSessionTask *)task didCompleteWithError:(NSError *)error
{
    TTSessionModel *sessionModel = [self getSessionModel:task.taskIdentifier];
    if (!sessionModel) return;

    if ([self isCompletion:sessionModel.fileName]) {
        // 下载完成
        sessionModel.stateBlock(DownloadStateCompleted);
    } else if (error){

        if (error.code == NSURLErrorTimedOut||error.code == NSURLErrorNetworkConnectionLost) {
            sessionModel.stateBlock(DownloadStateSuspended);
        }else{

        // 下载失败
            sessionModel.stateBlock(DownloadStateUnBegin);
        }
    }

    // 关闭流
    [sessionModel.stream close];
    sessionModel.stream = nil;

    // 清除任务
    [self.tasks removeObjectForKey:sessionModel.fileName];
    [self.sessionModels removeObjectForKey:@(task.taskIdentifier).stringValue];
}

@end

TTSessionModel

//
//  TTSessionModel.h
//  DownloadDemo
//
//  Created by qihb on 16/6/3.
//  Copyright © 2016年 Qihb. All rights reserved.
//

#import <Foundation/Foundation.h>
#import <UIKit/UIKit.h>
#import "DownloadModel.h"

typedef enum {
    /** 未下载 */
    DownloadStateUnBegin = 0,
    /** 下载中 */
    DownloadStateStart,
    /** 下载暂停 */
    DownloadStateSuspended,
    /** 下载完成 */
    DownloadStateCompleted,
}DownloadState;

//0未下载  1正在下载  2暂停下载 3已完成下载

@interface TTSessionModel : NSObject

/** 流 */
@property (nonatomic, strong) NSOutputStream *stream;

/** 下载地址 */
@property (nonatomic, copy) NSString *url;

@property (nonatomic, copy) NSString *fileName;

@property (nonatomic,strong)DownloadModel *model;

/** 获得服务器这次请求 返回数据的总长度 */
@property (nonatomic, assign) NSInteger totalLength;

/** 下载进度 */
@property (nonatomic, copy) void(^progressBlock)(NSInteger receivedSize, NSInteger expectedSize, CGFloat progress);

/** 下载状态 */
@property (nonatomic, copy) void(^stateBlock)(DownloadState state);

@end

DownloadModel

//
//  DownloadModelList.h
//  Up Studio
//
//  Created by qihb on 16/5/12.
//  Copyright © 2016年 gjh. All rights reserved.
//

#import "DBBaseModel.h"

#define dbModelKeyWord @"model_md5_str"

@interface DownloadModel : DBBaseModel

@property(nonatomic,copy)NSString *model_md5_str;           //md5串
@property(nonatomic,copy)NSString *model_image_url;         //图片远程下载地址
@property(nonatomic,copy)NSString *model_url;               //模型远程下载地址
@property(nonatomic,copy)NSString *model_size;              //文件大小
@property(nonatomic,copy)NSString *model_name;              //名字
@property(nonatomic,copy)NSString *model_format;            //格式 up3/stl
@property(nonatomic,copy)NSString *model_type;              //分类
@property(nonatomic,copy)NSString *model_image_path;        //图片本地路径
@property(nonatomic,copy)NSString *model_path;              //模型本地路径
@property(nonatomic,copy)NSString *model_subModel_num;      //子模型数量
@property(nonatomic,copy)NSString *model_payyed_flag;       //付款标识
@property(nonatomic,copy)NSString *model_price;             //价格
@property(nonatomic,copy)NSString *model_copyright;         //版权
@property(nonatomic,copy)NSString *model_from_source;       //来源 0预设 1下载 2本地保存
@property(nonatomic,copy)NSString *model_download_flag;     //0未下载  1正在下载  2暂停下载 3已完成下载
@property(nonatomic,copy)NSString *model_download_percentage;//下载百分比
@property(nonatomic,copy)NSString *recent_time;             //最近一次使用时间

//获取所有下载完成的模型数据
+(NSArray *)findAllDownloadFinished;

@end

downloadModel的部分大家可以参照我之前的博客,数据库DataBaseQueue多线程安全的博客,这个就是继承DBBaseModel实现的,也是最近改到项目里的,写代码就是边开发边重构吗,学以致用。

时间: 2024-10-13 16:20:42

ios:NSURLSessionDataTask做文件断点下载的相关文章

ios开发网络学习四:NSURLConnection大文件断点下载

#import "ViewController.h" @interface ViewController ()<NSURLConnectionDataDelegate> @property (weak, nonatomic) IBOutlet UIProgressView *progressView; @property (nonatomic, assign) NSInteger totalSize; @property (nonatomic, assign) NSInte

iOS- 利用AFNetworking(AFN) - 实现文件断点下载

iOS- 利用AFNetworking(AFN) - 实现文件断点下载 官方建议AFN的使用方法 1. 定义一个全局的AFHttpClient:包含有 1> baseURL 2> 请求 3> 操作队列 NSOperationQueue 2. 由AFHTTPRequestOperation负责所有的网络操作请求 0.导入框架准备工作 •1. 将框架程序拖拽进项目 •2.  添加iOS框架引用 –SystemConfiguration.framework –MobileCoreService

iOS- 利用AFNetworking3.0+(最新AFN) - 实现文件断点下载

0.导入框架准备工作 •1. 将AFNetworking3.0+框架程序拖拽进项目 •2. 或使用Cocopod 导入AFNetworking3.0+ •3.  引入 #import "AFNetworking.h" ----> 1.UI准备工作 A. 定义一个全局的 NSURLSessionDownloadTask:下载管理句柄    由其负责所有的网络操作请求 1 2 3 4 5 @interface ViewController () {     // 下载句柄     N

使用NSURLConnection实现大文件断点下载

使用NSURLConnection实现大文件断点下载 由于是实现大文件的断点下载,不是下载一般图片什么的.在设计这个类的时候本身就不会考虑把下载的文件缓存到内存中,而是直接写到文件系统. 要实现断点下载,需要满足1个条件,那就是,必须要服务器支持断点下载. 实现的思路是这样子的: 1.  第一次会获取到被下载文件的总大小(服务器提供这个值) 下载文件总大小 = 期望从服务器获取文件的大小 + 本地已经下载的文件的大小 2.  设置请求的缓存策略为不会读取本地中已经缓存的数据(NSURLReque

iOS 大文件断点下载

iOS 在下载大文件的时候,可能会因为网络或者人为等原因,使得下载中断,那么如何能够进行断点下载呢? // resumeData的文件路径 #define XMGResumeDataFile [[NSSearchPathForDirectoriesInDomains(NSCachesDirectory, NSUserDomainMask, YES) lastObject] stringByAppendingPathComponent:@"resumeData.tmp"] #import

iOS中利用NSURLSession进行文件断点下载

第一部分 知识储备 1.对NSURLSesiion的认识 NSURLSesiion是苹果在iOS7推出的一个类,它具备了NSURLConnection所具备的方法,同时也比它更强大.苹果推出它的目的大有取代NSURLConnection的趋势或者目的. 2.  NSURLSesiion的作用 实现对文件的下载与上传.在NSURLSesiion中,任何请求都可以被看做是一个任务.而NSURLSesiionData 有两个子类:NSURLSessionDownlaodTask实现文件下载和NSURL

Android网络编程之——文件断点下载(暂停/继续/重新下载)

开头还是不说废话了直接进入主题吧! 一:关于断点下载所涉及到的知识点 1.对SQLite的增删改查(主要用来保存当前任务的一些信息) 2.HttpURLConnection的请求配置 HttpURLConnection connection = null; //设置下载请求属性 connection.setRequestProperty(); 3.RandomAccessFile 对文件进行写入 RandomAccessFile rwd = null; //从文件的某一位置写入 rwd.seek

android网络编程之HttpUrlConnection的讲解--实现文件断点下载

1.没有实现服务器端,下载地址为网上的一个下载链接. 2.网络开发不要忘记在配置文件中添加访问网络的权限 <uses-permission android:name="android.permission.INTERNET"/> 3.网络请求.处理不能在主线程中进行,一定要在子线程中进行.因为网络请求一般有1~3秒左右的延时,在主线程中进行造成主线程的停顿,对用户体验来说是致命的.(主线程应该只进行UI绘制,像网络请求.资源下载.各种耗时操作都应该放到子线程中). 4.断点

iOS-服务器文件断点下载

文件下载基本步骤:1.获取下载链接,创建响应发送请求.(使用异步请求,避免因文件过大下载时间长而阻塞主线程).2.当接到响应时在下载目录中创建文件.创建文件使用NSFileHandle进行文件内部处理.(检验文件是否存在——利用NSFileManager创建文件——NSFileHandle的fileHandleForWritingAtPath方法对文件进行写入).3.接收数据时,将分段接收的数据写入文件中4.文件接收完毕后,关闭NSFileHandle.以上为普通下载步骤,此处不用代码示范,以下