iOS大文件断点下载

废话不多所,直接上代码
github网址:https://github.com/SPStore/SPHTTPSessionManager

.h文件

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

typedef NS_ENUM(NSInteger, SPDownloadWay) {
    SPDownloadWayResume,    // 这种下载方式支持重启app时继续上一次的下载
    SPDownloadWayRestart    // 这种下载方式不支持重启app时继续上一次的下载
    
    // SPDownloadWayResume和SPDownloadWayRestart的下载方式具体异同点如下:
    /*
    1、SPDownloadWayResume和SPDownloadWayRestart均支持断点下载
    2、SPDownloadWayResume支持重启app继续上一次下载,SPDownloadWayRestart不支持
    3、SPDownloadWayResume会自动判断是否下载完成,并保存每时每刻下载的进度值,SPDownloadWayRestart没有此功能
    4、SPDownloadWayResume支持任何时刻删除已经下载的文件数据,SPDownloadWayRestart不支持在下载过程中删除,只有下载完成时才能删除
    5、SPDownloadWayResume不依赖于AFN,SPDownloadWayRestart依赖AFN
     
     通俗的讲,SPDownloadWayResume和SPDownloadWayRestart的根本区别就是前者是沙盒模式,后者是内存模式
     */
    
};

NS_ASSUME_NONNULL_BEGIN
@interface SPHTTPSessionManager : NSObject

/** 单例对象 */
+ (instancetype)shareInstance;

/**
 *  get请求
 *
 *  @param urlString 请求地址
 *  @param params    参数字典
 *  @param success   请求成功回调的block
 *  @param failure   请求失败回调的block
 */
- (void)GET:(NSString *)urlString
     params:(NSDictionary *)params
    success:(void (^)(id responseObject))success
    failure:(void (^)(NSError *error))failure;

/**
 *  post请求
 *
 *  @param urlString 请求地址
 *  @param params    参数字典
 *  @param success   请求成功回调的block
 *  @param failure   请求失败回调的block
 */
- (void)POST:(NSString *)urlString
      params:(NSDictionary *)params
     success:(void (^)(id responseObject))success
     failure:(void (^)(NSError *error))failure;

/**
 *  下载
 *
 *  @param urlString 请求地址
 *  @param downloadProgressBlock  下载过程中回调的block
 *  @complete 下载完成回调的block
 */
- (NSURLSessionTask *)downloadWithURL:(NSString *)urlString
                                     progress:(void (^)(CGFloat progress))downloadProgressBlock
                                     complete:(void (^)(NSURLResponse *response, NSURL *filePath, NSError *error))completionHandler;


/*
 * 上传  http://www.cnblogs.com/qingche/p/5489434.html
 *
 */
- (void)uploadWithURL:(NSString *)urlString
               params:(NSDictionary *)params
             fileData:(NSData *)filedata
                 name:(NSString *)name
             fileName:(NSString *)filename
             mimeType:(NSString *) mimeType
             progress:(void (^)(NSProgress *uploadProgress))uploadProgressBlock
              success:(void (^)(id responseObject))success
              failure:(void (^)(NSError *error))failure;

// 以下这些操作在外界也可以另外做到,比如启动和暂停任务,外界在调用下载的方法时返回了一个task,开发者可以用该task去启动和暂停任务,之所以将其封装,一是:这个类能做到的尽量不让开发者去做,二是:让开发者完全面向我这个单例对象。开发者只需要做一些关于UI的事情

// 下载方式
@property (nonatomic, assign) SPDownloadWay downloadway;

/*
 *  启动任务
 *
 */
- (void)resumeTask;
/* 
 *  暂停任务
 *
 */
- (void)suspendTask;
/*
 *  取消任务
 *
 */
- (void)cancelTask;

/*
 *  移除已经下载好的文件数据 
 *
 */
- (BOOL)removeDownloadedData:(NSError * _Nullable __autoreleasing * _Nullable)error;

/** 是否正在下载,对于SPDownloadResume下载方式,该属性来源于沙盒,对于SPDownloadRestart下载方式,该属性来源于内存 */
@property (nonatomic, assign, readonly, getter=isDownloading) BOOL downloading;


// 以下两个属性只对SPDownloadResume下载方式奏效

/** 保存在沙盒中的进度值 */
@property (nonatomic, assign, readonly) CGFloat storedDownloadProgress;
/** 是否已经下载完毕 */
@property (nonatomic, assign, readonly, getter=isDownloadCompleted) BOOL downloadCompleted;

@end
NS_ASSUME_NONNULL_END


@interface NSString (MD5)

@property (nullable, nonatomic, readonly) NSString *md5String;

@end

.m文件

#import "SPHTTPSessionManager.h"
#import "AFNetworking.h"

// 文件名,MD5加密
#define SPFileName self.fileURLString.md5String

// 文件的存放路径(caches)
#define SPFileFullPath [[NSSearchPathForDirectoriesInDomains(NSCachesDirectory, NSUserDomainMask, YES) lastObject] stringByAppendingPathComponent:SPFileName]

// 存储文件信息的路径(caches)
#define SPFileInfoPath [[NSSearchPathForDirectoriesInDomains(NSCachesDirectory, NSUserDomainMask, YES) lastObject] stringByAppendingPathComponent:@"sp_fileInfo.info"]

// 文件的已下载长度
#define SPDownloadLength [[[NSFileManager defaultManager] attributesOfItemAtPath:SPFileFullPath error:nil][NSFileSize] integerValue]

@interface SPHTTPSessionManager() <NSURLSessionDelegate> {
    CGFloat _storedDownloadProgress;
    BOOL _downloadCompleted;
    BOOL _downloading;
}
/** session */
@property (nonatomic, strong) NSURLSession *session;
/** 写文件的流对象 */
@property (nonatomic, strong) NSOutputStream *stream;
/** 文件的总长度 */
@property (nonatomic, assign) NSInteger totalLength;
/** 下载任务 */
@property (nonatomic, strong) NSURLSessionTask *task;
/** 文件的url */
@property (nonatomic, strong) NSString *fileURLString;
/** 下载过程中回调的block */
@property (nonatomic, copy) void (^downloadProgressBlock)(CGFloat progress);
/** 下载完成回调的block */
@property (nonatomic,copy) void (^completionHandler)(NSURLResponse *response, NSURL *URL, NSError *error);
/** 存储文件信息的字典,该字典要写入沙盒 */
@property (nonatomic, strong) NSMutableDictionary *fileInfoDictionry;

// ------------ 上面的额属性是针对下载2,下面的属性针对下载1 -------------
/** 下载1的文件url地址 */
@property (nonatomic, copy) NSString *downloadFromZero_UrlString;
/** 下载1完成后保存的文件路径 */
@property (nonatomic, copy) NSString *downloadFromZero_filePath;
/** 下载1过程中回调的block */
@property (nonatomic, copy) void (^downloadFromZero_ProgressBlock)(CGFloat progress);
/** 下载1完成回调的block */
@property (nonatomic,copy) void (^downloadFromZero_completionHandler)(NSURLResponse *response, NSURL *URL, NSError *error);

@end

@implementation SPHTTPSessionManager

+ (instancetype)shareInstance {
    static SPHTTPSessionManager *manager = nil;
    static dispatch_once_t onceToken;
    dispatch_once(&onceToken, ^{
        manager = [[self alloc] init];
    });
    return manager;
}


// get请求
- (void)GET:(NSString *)urlString params:(NSDictionary *)params
   success:(void (^)(id responseObject))success
   failure:(void (^)(NSError *error))failure {
    
    AFHTTPSessionManager *manager = [AFHTTPSessionManager manager];
    manager.responseSerializer.acceptableContentTypes = [NSSet setWithObjects:@"text/plain",@"application/json",@"text/json",@"text/javascript",@"text/html", nil];
    
    [manager GET:urlString parameters:params progress:nil success:^(NSURLSessionDataTask * _Nonnull task, id  _Nullable responseObject) {
        
        if (success) {
            success(responseObject);
        }
    } failure:^(NSURLSessionDataTask * _Nullable task, NSError * _Nonnull error) {
        if (failure) {
            failure(error);
        };
    }];
}

// post请求
- (void)POST:(NSString *)urlString params:(NSDictionary *)params
    success:(void (^)(id responseObject))success failure:(void (^)(NSError *error))failure{
    
    AFHTTPSessionManager *manager = [AFHTTPSessionManager manager];
    manager.responseSerializer.acceptableContentTypes = [NSSet setWithObjects:@"text/plain",@"application/json",@"text/json",@"text/javascript",@"text/html", nil];
    
    [manager POST:urlString parameters:params progress:nil success:^(NSURLSessionDataTask * _Nonnull task, id  _Nullable responseObject) {
        if (success) {
            success(responseObject);
        }
    } failure:^(NSURLSessionDataTask * _Nullable task, NSError * _Nonnull error) {
        if (failure) {
            failure(error);
        }
    }];
    
}

// 下载1(重启app时从0开始开始)
- (NSURLSessionTask *)downloadFromZeroWithURL:(NSString *)urlString
                                     progress:(void (^)(CGFloat))downloadProgressBlock
                                     complete:(void (^)(NSURLResponse *, NSURL *, NSError *))completionHandler {
    
    self.downloadFromZero_UrlString = urlString;
    self.downloadFromZero_ProgressBlock = downloadProgressBlock;
    self.downloadFromZero_completionHandler = completionHandler;
  
    return self.task;
}

// 下载2(重启app时从上一次的数据开始)
- (NSURLSessionTask *)downloadWithURL:(NSString *)urlString
                                                   progress:(void (^)(CGFloat))downloadProgressBlock
                                                   complete:(void (^)(NSURLResponse *, NSURL *, NSError *))completionHandler {
    if (self.downloadway == SPDownloadWayResume) {
        self.fileURLString = urlString;
        
        // 将block参数赋值给全局block变量
        self.downloadProgressBlock = downloadProgressBlock;
        self.completionHandler = completionHandler;
        
        [self downloadFromZeroWithURL:urlString progress:downloadProgressBlock complete:completionHandler];
        
        return self.task;
    } else {
        return [self downloadFromZeroWithURL:urlString progress:downloadProgressBlock complete:completionHandler];
    }
}


// 上传
- (void)uploadWithURL:(NSString *)urlString
              params:(NSDictionary *)params
            fileData:(NSData *)filedata
                name:(NSString *)name
            fileName:(NSString *)filename
            mimeType:(NSString *) mimeType
            progress:(void (^)(NSProgress *uploadProgress))uploadProgressBlock
             success:(void (^)(id responseObject))success
             failure:(void (^)(NSError *error))failure {
    
    AFHTTPSessionManager *manager = [AFHTTPSessionManager manager];
    
    [manager POST:urlString parameters:params constructingBodyWithBlock:^(id<AFMultipartFormData>  _Nonnull formData) {
        
        [formData appendPartWithFileData:filedata name:name fileName:filename mimeType:mimeType];
        
    } progress:^(NSProgress * _Nonnull uploadProgress) {
        if (uploadProgressBlock) {
            uploadProgressBlock(uploadProgress);
        }
        
    } success:^(NSURLSessionDataTask * _Nonnull task, id  _Nullable responseObject) {
        if (success) {
            success(responseObject);
        }
        
    } failure:^(NSURLSessionDataTask * _Nullable task, NSError * _Nonnull error) {
        if (failure) {
            failure(error);
        }
    }];
}

// 启动任务
- (void)resumeTask {
    [self.task resume];
}

// 暂停任务
- (void)suspendTask {
    [self.task suspend];
}

// 取消任务
- (void)cancelTask {
    [self.task cancel];
}

// 删除
- (BOOL)removeDownloadedData:(NSError * _Nullable __autoreleasing * _Nullable)error {
    if (self.downloadway == SPDownloadWayResume) {
        if (SPDownloadLength) {
            
            BOOL isDirectory = NO;
            
            NSFileManager *manager = [NSFileManager defaultManager];
            // 删除已经下载好的文件
            if ([manager fileExistsAtPath:SPFileFullPath isDirectory:&isDirectory] && [manager fileExistsAtPath:SPFileInfoPath isDirectory:&isDirectory]) {
                // 移除
                BOOL removeFileSuccess = [manager removeItemAtPath:SPFileFullPath error:error];
                BOOL removeFileLengthSuccess = [manager removeItemAtPath:SPFileInfoPath error:error];
                if (removeFileSuccess && removeFileLengthSuccess) { // 移除成功

                    [self.task cancel];
                    self.task = nil;
                    return YES;
                } else {
                    NSLog(@"移除文件失败");
                    return NO;
                }
                
            } else {
                NSLog(@"没找到文件路径");
                return NO;
            }
        } else {
            NSLog(@"没有需要删除的数据");
            return NO;
        }
    }
    else {
        if (!_downloading) { // 说明没有正在下载(下载1)
            BOOL isDirectory = NO;
            
            NSFileManager *manager = [NSFileManager defaultManager];
            // 删除已经下载好的文件
            if ([manager fileExistsAtPath:self.downloadFromZero_filePath isDirectory:&isDirectory]) {
                // 移除
                BOOL removeSuccess = [manager removeItemAtPath:self.downloadFromZero_filePath error:error];
                if (removeSuccess) {
                    [self.task cancel];
                    self.task = nil;
                    return YES;
                } else {
                    NSLog(@"移除失败");
                    return NO;
                }
                return YES;
            } else {
                NSLog(@"没找到文件路径");
                return NO;
            }
        }
        else { // 正在下载
            NSLog(@"****** ‘SPDownloadWayRestart‘不支持在下载过程中删除");
           return NO;
        }
    }
    
}

// 从沙盒中获取下载的进度值
- (CGFloat)storedDownloadProgress {
    _storedDownloadProgress = [self.fileInfoDictionry[@"downloadProgress"] floatValue];
    return _storedDownloadProgress;
}

// 从沙盒中获取下载是否完毕的标识
- (BOOL)isDownloadCompleted {
    _downloadCompleted = self.fileInfoDictionry[@"downloadCompleted"];
    return _downloadCompleted;
}

// 从沙盒中获取是否正在下载的标识
- (BOOL)isDownloading {
    _downloading = self.fileInfoDictionry[@"downloading"];
    return _downloading;
}

- (NSOutputStream *)stream {
    if (!_stream) {
        _stream = [NSOutputStream outputStreamToFileAtPath:SPFileFullPath append:YES];
    }
    return _stream;
}

- (NSURLSession *)session {
    if (!_session) {
        _session = [NSURLSession sessionWithConfiguration:[NSURLSessionConfiguration defaultSessionConfiguration] delegate:self delegateQueue:[[NSOperationQueue alloc] init]];
    }
    return _session;
}

- (NSURLSessionTask *)task {
    if (!_task) {
        
        if (self.downloadway == SPDownloadWayResume) {
            // 取出文件的总长度
            NSInteger totalLength = [self.fileInfoDictionry[SPFileName] integerValue];
            if (totalLength && SPDownloadLength == totalLength) {
                NSLog(@"文件已经下载完成了");
                return nil;
            }
            // 创建请求
            NSMutableURLRequest *request = [NSMutableURLRequest requestWithURL:[NSURL URLWithString:self.fileURLString]];
            
            // 设置请求头
            // Range : bytes=xxx-xxx
            NSString *range = [NSString stringWithFormat:@"bytes=%zd-", SPDownloadLength];
            [request setValue:range forHTTPHeaderField:@"Range"];
            
            // 创建一个Data任务
            _task = [self.session dataTaskWithRequest:request];
        }
        else {
            AFHTTPSessionManager *manager = [AFHTTPSessionManager manager];
            
            NSURL *urlpath = [NSURL URLWithString:self.downloadFromZero_UrlString];
            NSURLRequest *request = [NSURLRequest requestWithURL:urlpath];
            
            _task = [manager downloadTaskWithRequest:request progress:^(NSProgress * _Nonnull downloadProgress) {
                
                _downloading = YES;
                
                CGFloat progress = 1.0 * downloadProgress.completedUnitCount / downloadProgress.totalUnitCount;
                
                if (self.downloadFromZero_ProgressBlock) {
                    self.downloadFromZero_ProgressBlock(progress);
                }
                
            } destination:^NSURL * _Nonnull(NSURL * _Nonnull targetPath, NSURLResponse * _Nonnull response) {
                
                NSString *cachesPath = [[NSSearchPathForDirectoriesInDomains(NSCachesDirectory, NSUserDomainMask, YES) lastObject] stringByAppendingPathComponent:response.suggestedFilename];
                
                NSURL *fileURL = [NSURL fileURLWithPath:cachesPath];
                
                return fileURL;
                
            } completionHandler:^(NSURLResponse * _Nonnull response, NSURL * _Nullable filePath, NSError * _Nullable error) {
                
                _downloading = NO;
                self.downloadFromZero_filePath = filePath.path;
                
                if (self.downloadFromZero_completionHandler) {
                    self.downloadFromZero_completionHandler(response,filePath,error);
                }
            }];
        }
    }
    return _task;
        
}

- (NSMutableDictionary *)fileInfoDictionry {
    if (_fileInfoDictionry == nil) {
        //  通过文件文件路径初始化字典,第一次取出来的必为空,因为此时还没有写进沙盒
       _fileInfoDictionry = [NSMutableDictionary dictionaryWithContentsOfFile:SPFileInfoPath];
        if (_fileInfoDictionry == nil) {
            _fileInfoDictionry = [NSMutableDictionary dictionary];
        }
    }
    return _fileInfoDictionry;
}


#pragma mark - <NSURLSessionDataDelegate>
/**
 * 1.接收到响应
 */
- (void)URLSession:(NSURLSession *)session dataTask:(NSURLSessionDataTask *)dataTask didReceiveResponse:(NSHTTPURLResponse *)response completionHandler:(void (^)(NSURLSessionResponseDisposition))completionHandler {
    
    // 打开流
    [self.stream open];
    
    // 获得服务器这次请求 返回数据的总长度
    self.totalLength = [response.allHeaderFields[@"Content-Length"] integerValue] + SPDownloadLength;
    
    // 存储总长度
    self.fileInfoDictionry[SPFileName] = @(self.totalLength);
    [self.fileInfoDictionry writeToFile:SPFileInfoPath atomically:YES];
    
    // 接收这个请求,允许接收服务器的数据
    completionHandler(NSURLSessionResponseAllow);
}

/**
 * 2.接收到服务器返回的数据(这个方法可能会被调用N次)
 */
- (void)URLSession:(NSURLSession *)session dataTask:(NSURLSessionDataTask *)dataTask didReceiveData:(NSData *)data {
    
    // 写入数据,不需要指定写入到哪个路径,因为stream在创建的那一刻就纪录好存储路径
    [self.stream write:data.bytes maxLength:data.length];
    
    // 回调block
    if (self.downloadProgressBlock) {

        // 获取进度值
        CGFloat progress = 1.0 * SPDownloadLength / self.totalLength;
        //NSLog(@"++++++%f",progress);
        self.downloadProgressBlock(progress);

        self.fileInfoDictionry[@"downloadProgress"] = @(progress);  // 进度值
        self.fileInfoDictionry[@"downloading"] = @(YES); // 正在下载的标识
        [self.fileInfoDictionry writeToFile:SPFileInfoPath atomically:YES];

    }
}

/**
 * 3.请求完毕(成功\失败)
 */
- (void)URLSession:(NSURLSession *)session task:(NSURLSessionTask *)task didCompleteWithError:(NSError *)error {
    
    if (self.completionHandler) {
        self.completionHandler(task.response,[NSURL fileURLWithPath:SPFileFullPath],error);
        // 关闭流
        [self.stream close];
        self.stream = nil;
        
        // 清除任务
        self.task = nil;
        
        if (!error) {

            self.fileInfoDictionry[@"downloadCompleted"] = @(YES);  // 下载完成的标识
            self.fileInfoDictionry[@"downloading"] = @(NO); // 正在下载的标识
            [self.fileInfoDictionry writeToFile:SPFileInfoPath atomically:YES];
        } else {
            if ([error.domain isEqualToString:@"NSURLErrorDomain"] && error.code == -999) {
                return;
            }
        }
    }
    
}

@end

#import <CommonCrypto/CommonDigest.h>

@implementation NSString (MD5)

- (NSString *)md5String {
    
    const char *string = self.UTF8String;
    int length = (int)strlen(string);
    unsigned char bytes[CC_MD5_DIGEST_LENGTH];
    CC_MD5(string, length, bytes);
    return [self stringFromBytes:bytes length:CC_MD5_DIGEST_LENGTH];
}

- (NSString *)stringFromBytes:(unsigned char *)bytes length:(NSInteger)length {
    
    NSMutableString *mutableString = @"".mutableCopy;
    for (int i = 0; i < length; i++)
        [mutableString appendFormat:@"%02x", bytes[i]];
    return [NSString stringWithString:mutableString];
}

@end

之所以不用大家普遍使用的URLSessionDownloadTask,是因为它几乎无法做到杀死app后继续下载,尽管有个resumeData,也只能实现后台或者暂停后继续下载。

github网址:https://github.com/SPStore/SPHTTPSessionManager

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 206,482评论 6 481
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 88,377评论 2 382
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 152,762评论 0 342
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 55,273评论 1 279
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 64,289评论 5 373
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,046评论 1 285
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,351评论 3 400
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,988评论 0 259
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 43,476评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,948评论 2 324
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,064评论 1 333
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,712评论 4 323
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,261评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,264评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,486评论 1 262
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,511评论 2 354
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,802评论 2 345

推荐阅读更多精彩内容