iOS渣逼(2)泥淖虾渣逼看URLConnection

NSURLConnection下载

课程目标

  • NSURLConnection下载是一个网络多线程的综合性演练项目
  • 充分体会 NSURLConnection 开发中的细节
  • 虽然 NSURLConnection 在 iOS 9.0 中已经被废弃,但是作为资深的 iOS 程序员,必须要了解 NSURLConnection 的细节
  • 利用 HTTP 请求头的 Range 实现断点续传
  • 利用 NSOutputStream 实现文件流拼接
  • 自定义 NSOperation及操作缓存管理
  • Block 的综合演练
  • 利用 IB_DESIGNABLE 和 IBInspectable 实现在 Stroybaord 中自定义视图的实时渲染
  • NSURLSession 从 Xcode 6.0 到 Xcode 6.3.1 都存在内存问题,历时7个月,如下图所示


    session下载QQ内存.png

NSURLConnection 的历史

  • iOS 2.0 推出的,至今有10多年的历史
  • 苹果几乎没有对 NSURLConnection 做太大的改动
  • sendAsynchronousRequest 方法是 iOS 5.0 之后,苹果推出的
  • 在 iOS 5.0 之前,苹果的网络开发是处于黑暗时代
  • 需要使用代理方法,还需要使用运行循环,才能够处理复杂的网络请求!
  • 只提供了 启动 和 取消 两个方法,没有中间状态

使用异步方法下载

- (void)downloadWithURL:(NSURL *)url {

    // 请求
    NSURLRequest *request = [NSURLRequest requestWithURL:url cachePolicy:1 timeoutInterval:kTimeout];

    NSLog(@"start");
    // 下载
    [NSURLConnection sendAsynchronousRequest:request queue:[NSOperationQueue mainQueue] completionHandler:^(NSURLResponse *response, NSData *data, NSError *connectionError) {

        // 将文件写入磁盘
        [data writeToFile:@"/Users/liufan/Desktop/123" atomically:YES];
        NSLog(@"下载完成");
    }];
}

问题:

  1. 没有进度跟进,用户体验不好
  2. 会出现内存峰值,如果文件太大,在真机上会闪退

解决办法

  • 使用代理方法来解决下载进度跟进的问题

HEAD方法

HEAD 方法通常是用来在下载文件之前,获取远程服务器上的文件信息

  • 与 GET 方法相比,同样能够拿到响应头,但是不返回数据实体
  • 用户可以根据响应头信息,确定下一步操作
NSURL *url = [NSURL URLWithString:@"http://localhost/demo.json"];
NSMutableURLRequest *request = [NSMutableURLRequest requestWithURL:url cachePolicy:1 timeoutInterval:10.0];
request.HTTPMethod = @"HEAD";

NSURLResponse *response = nil;
[NSURLConnection sendSynchronousRequest:request returningResponse:&response error:NULL];
NSLog(@"要下载文件的长度 %tu", response.expectedContentLength);

同步方法

  • 同步方法是阻塞式的,通常只有 HEAD 方法才会使用同步方法
  • 如果在开发中,看到参数的类型是 **,就传入对象的地址

注意

  • NSURLConnectionDownloadDelegate 代理方法是为 Newsstand Kit’s(杂志包) 创建的下载服务的
  • Newsstand 主要在国外使用比较广泛,国内极少
  • 如果使用 NSURLConnectionDownloadDelegate 代理方法监听下载进度,能够监听到进度,但是:找不到下载的文件

示例代码如下:

- (void)downloadWithURL:(NSURL *)url {

    // 请求
    NSURLRequest *request = [NSURLRequest requestWithURL:url cachePolicy:1 timeoutInterval:kTimeout];

    [NSURLConnection connectionWithRequest:request delegate:self];
}

#pragma mark - NSURLConnectionDownloadDelegate
- (void)connection:(NSURLConnection *)connection didWriteData:(long long)bytesWritten totalBytesWritten:(long long)totalBytesWritten expectedTotalBytes:(long long) expectedTotalBytes {

    NSLog(@"%f", (float)totalBytesWritten / expectedTotalBytes);
}

- (void)connectionDidFinishDownloading:(NSURLConnection *)connection destinationURL:(NSURL *) destinationURL {

    NSLog(@"%@", destinationURL);
}

跟踪下载进度

#pragma mark - NSURLConnectionDataDelegate
// 1. 接收到服务器响应
- (void)connection:(NSURLConnection *)connection didReceiveResponse:(NSURLResponse *)response {
    NSLog(@"%@", response);
    self.expectedContentLength = response.expectedContentLength;
    self.fileSize = 0;
}

// 2. 接收到数据
- (void)connection:(NSURLConnection *)connection didReceiveData:(NSData *)data {

    self.fileSize += data.length;
    float progress = (float)self.fileSize / self.expectedContentLength;
    NSLog(@"%f", progress);
}

// 3. 接收完成
- (void)connectionDidFinishLoading:(NSURLConnection *)connection {
    NSLog(@"下载完成");
}

// 4. 网络错误
- (void)connection:(NSURLConnection *)connection didFailWithError:(NSError *)error {
    NSLog(@"%@", error);
}

拼接数据

- (NSMutableData *)fileData {
    if (_fileData == nil) {
        _fileData = [NSMutableData data];
    }
    return _fileData;
}

// 2. 接收到数据
- (void)connection:(NSURLConnection *)connection didReceiveData:(NSData *)data {

    self.fileSize += data.length;
    float progress = (float)self.fileSize / self.expectedContentLength;
    NSLog(@"%f", progress);

    // 拼接数据
    [self.fileData appendData:data];
}

// 3. 接收完成
- (void)connectionDidFinishLoading:(NSURLConnection *)connection {
    NSLog(@"下载完成");

    [self.fileData writeToFile:@"/Users/liufan/Desktop/321" atomically:YES];
    self.fileData = nil;
}

存在的问题

  • 内存峰值依旧

意外发现:运行结果和 NSURLConnection 的异步方法的效果几乎一样!

NSFileHandle 拼接文件

  • NSFileManager : 主要是做文件的删除,移动,复制,检查文件是否存在等操作,类似于 Finder
  • NSFileHandle : 文件句柄(指针),操纵,提示:凡是看到 Handle 这个单词,就表示对前面一个单词(File)的独立操作
- (void)writeData:(NSData *)data {

    NSFileHandle *fp = [NSFileHandle fileHandleForWritingAtPath:self.targetPath];

    if (fp == nil) {
        [data writeToFile:self.targetPath atomically:YES];
    } else {
        [fp seekToEndOfFile];
        [fp writeData:data];
        [fp closeFile];
    }
}

问题:文件会被重复追加

// 下载前删除文件
[[NSFileManager defaultManager] removeItemAtPath:self.targetPath error:NULL];

NSOutputStream 拼接文件

定义属性

// 1. 接收到服务器响应
- (void)connection:(NSURLConnection *)connection didReceiveResponse:(NSURLResponse *)response {
    NSLog(@"%@", response);
    self.expectedContentLength = response.expectedContentLength;
    self.fileSize = 0;

    self.targetPath = [NSTemporaryDirectory() stringByAppendingPathComponent:response.suggestedFilename];
    NSLog(@"%@", self.targetPath);

    // 删除文件
    [[NSFileManager defaultManager] removeItemAtPath:self.targetPath error:NULL];

    // 打开文件流
    self.fileStream = [[NSOutputStream alloc] initToFileAtPath:self.targetPath append:YES];
    [self.fileStream open];
}

// 2. 接收到数据
- (void)connection:(NSURLConnection *)connection didReceiveData:(NSData *)data {

    self.fileSize += data.length;
    float progress = (float)self.fileSize / self.expectedContentLength;
    NSLog(@"%f", progress);

    // 拼接数据
    [self.fileStream write:data.bytes maxLength:data.length];
}

// 3. 接收完成
- (void)connectionDidFinishLoading:(NSURLConnection *)connection {
    NSLog(@"下载完成");

    // 关闭流
    [self.fileStream close];
}

// 4. 网络错误
- (void)connection:(NSURLConnection *)connection didFailWithError:(NSError *)error {
    NSLog(@"%@", error);

    [self.fileStream close];
}

文件流操作方法

打开流 - 要对文件读写之前,首先需要打开流
- (void)open;

关闭流 - 对文件读写操作完成之后,需要关闭流
- (void)close;

将数据写入到流
- (NSInteger)write:(const uint8_t *)buffer maxLength:(NSUInteger)len;

断点续传

确认思路

  1. 检查服务器文件信息
  2. 检查本地文件
    • 如果比服务器文件小,续传
    • 如果比服务器文件大,重新下载
    • 如果和服务器文件一样,下载完成
  3. 断点续传

代码实现

检查服务器文件信息

///  检查服务器文件信息
- (void)remoteInfoWithURL:(NSURL *)url {

    NSMutableURLRequest *request = [NSMutableURLRequest requestWithURL:url];
    request.HTTPMethod = @"HEAD";

    NSURLResponse *response = nil;
    [NSURLConnection sendSynchronousRequest:request returningResponse:&response error:NULL];

    self.expectedContentLength = response.expectedContentLength;
    self.targetPath = [NSTemporaryDirectory() stringByAppendingPathComponent:response.suggestedFilename];
}

检查本地文件

///  检查本地文件大小
- (long long)localFileSize {

    NSFileManager *manager = [NSFileManager defaultManager];

    long long fileSize = 0;
    // 1. 文件是否存在
    if ([manager fileExistsAtPath:self.targetPath]) {
        fileSize = [[manager attributesOfItemAtPath:self.targetPath error:NULL] fileSize];
    }

    // 2. 判断是否大于服务器大小
    if (fileSize > self.expectedContentLength) {
        [manager removeItemAtPath:self.targetPath error:NULL];
        fileSize = 0;
    }

    return fileSize;
}

断点续传

///  从偏移位置下载文件
- (void)downloadWithURL:(NSURL *)url offset:(long long)offset {

    self.fileSize = offset;

    NSMutableURLRequest *request = [NSMutableURLRequest requestWithURL:url cachePolicy:1 timeoutInterval:kTimeout];

    NSString *rangeStr = [NSString stringWithFormat:@"bytes=%lld-", offset];
    [request setValue:rangeStr forHTTPHeaderField:@"Range"];

    NSURLConnection *conn = [NSURLConnection connectionWithRequest:request delegate:self];
    [conn start];
}

修改代理方法

// 1. 接收到服务器响应
- (void)connection:(NSURLConnection *)connection didReceiveResponse:(NSURLResponse *)response {
    // 打开文件流
    self.fileStream = [[NSOutputStream alloc] initToFileAtPath:self.targetPath append:YES];
    [self.fileStream open];
}

下载主方法

- (void)downloadWithURL:(NSURL *)url {

    // 1. 检查服务器文件信息
    [self remoteInfoWithURL:url];

    // 2. 检查本地文件大小
    long long fileSize = [self localFileSize];

    if (fileSize == self.expectedContentLength) {
        NSLog(@"下载完成");
        return;
    }

    // 3. 从偏移位置下载文件
    [self downloadWithURL:url offset:fileSize];
}

多线程

  • 异步下载
- (void)downloadWithURL:(NSURL *)url {

    dispatch_async(dispatch_get_global_queue(0, 0), ^{
        // 1. 检查服务器文件信息
        [self remoteInfoWithURL:url];

        // 2. 检查本地文件大小
        long long fileSize = [self localFileSize];

        if (fileSize == self.expectedContentLength) {
            NSLog(@"下载完成");
            return;
        }

        // 3. 从偏移位置下载文件
        [self downloadWithURL:url offset:fileSize];
    });
}
  • 启动运行循环
NSURLConnection *conn = [NSURLConnection connectionWithRequest:request delegate:self];
[conn start];

// NSURLConnection 会在网络请求结束后,自动停止运行循环
[[NSRunLoop currentRunLoop] run];

NSLog(@"come here %@", [NSThread currentThread]);

完成回调

回调细节

  1. 进度回调,通常在异步执行

    1. 通常进度回调的频率非常高!如果界面上有很多文件,同时下载,又要更新 UI,可能会造成界面的卡顿
    2. 让进度回调,在异步执行,可以有选择的处理进度的显示,例如:只显示一个指示器!
    3. 有些时候,如果文件很小,调用方通常不关心下载进度!(SDWebImage)
    4. 异步回调,可以降低对主线程的压力
  2. 完成回调,通常在主线程执行

    1. 调用方不用考虑线程间通讯,直接更新UI即可
    2. 完成只有一次

增加类方法

///  实例化下载操作
///
///  @param url      下载文件的URL
///  @param progress 进度回调
///  @param finised  完成回调
///
///  @return 下载操作
+ (instancetype)downloadWithURL:(NSURL *)url progress:(void (^)(float progress))progress finised:(void (^)(NSString *filePath, NSError *error))finised;

///  开始下载
- (void)download;

利用属性记录block

  • 如果本方法可以直接调用,就不需要使用属性记录
  • 如果本方法不能直接调用,就需要使用属性记录,然后在需要的时候执行

定义 block 属性

///  下载文件 URL
@property (nonatomic, strong) NSURL *url;
///  进度回调
@property (nonatomic, copy) void (^progressBlock)(float);
///  完成回调
@property (nonatomic, copy) void (^finishedBlock)(NSString *, NSError *);

类方法实现

+ (instancetype)downloadWithURL:(NSURL *)url progress:(void (^)(float))progress finised:(void (^)(NSString *, NSError *))finised {

    HMDownloadOperation *d = [[HMDownloadOperation alloc] init];

    // 记录属性
    d.url = url;
    d.progressBlock = progress;
    d.finishedBlock = finised;

    return d;
}

在视图控制器中准备块代码

- (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event {
    NSURL *url = [NSURL URLWithString:@"http://dldir1.qq.com/qqfile/QQforMac/QQ_V4.0.2.dmg"];

    HMDownloadOperation *down = [HMDownloadOperation downloadWithURL:url progress:^(float progress) {
        NSLog(@"%f %@", progress, [NSThread currentThread]);
    } finised:^(NSString *filePath, NSError *error) {
        NSLog(@"%@ %@ %@", filePath, error, [NSThread currentThread]);
    }];

    [down download];
}

进度回调

if (self.progressBlock) {
    self.progressBlock(progress);
}

完成回调

dispatch_async(dispatch_get_main_queue(), ^{
    self.finishedBlock(self.filePath, nil);
});

失败回调

dispatch_async(dispatch_get_main_queue(), ^{
    self.finishedBlock(nil, error);
});

暂停下载

暂停下载

- (void)pause {
    [self.conn cancel];
}
  • Cancels an asynchronous load of a request.
    After this method is called, the connection makes no further delegate method calls. If you want to reattempt the connection, you should create a new connection object.

  • 取消一个异步请求,调用此方法后,connection不会再调用代理方法。如果要再次尝试连接,需要建立一个新的连接对象

下载进度视图

属性

IB_DESIGNABLE
@interface ProgressButton : UIButton

@property (nonatomic, assign) IBInspectable float progress;
@property (nonatomic, strong) IBInspectable UIColor *lineColor;
@property (nonatomic, assign) IBInspectable CGFloat lineWidth;

@end

代码实现

@implementation ProgressButton

- (void)setProgress:(float)progress {
    _progress = progress;

    [self setTitle:[NSString stringWithFormat:@"%.02f%%", progress * 100] forState:UIControlStateNormal];

    [self setNeedsDisplay];
}

- (void)drawRect:(CGRect)rect {

    CGPoint center = CGPointMake(rect.size.width * 0.5, rect.size.height * 0.5);
    CGFloat r = (MIN(rect.size.width, rect.size.height) - self.lineWidth) * 0.5;
    CGFloat startAngle = - M_PI_2;
    CGFloat endAngle = self.progress * 2 * M_PI + startAngle;

    UIBezierPath *path = [UIBezierPath bezierPathWithArcCenter:center radius:r startAngle:startAngle endAngle:endAngle clockwise:YES];

    path.lineWidth = self.lineWidth;
    path.lineCapStyle = kCGLineCapRound;

    [self.lineColor setStroke];

    [path stroke];
}

@end

Storyboard 技巧

在 SB 中直接设置自定义视图属性

下载管理器

单例

+ (instancetype)sharedDownloadManager {
    static id instance;

    static dispatch_once_t onceToken;
    dispatch_once(&onceToken, ^{
        instance = [[self alloc] init];
    });

    return instance;
}

移植下载方法

- (void)downloadWithURL:(NSURL *)url progress:(void (^)(float))progress finised:(void (^)(NSString *, NSError *))finised {

    HMDownloadOperation *downloader = [HMDownloadOperation downloadWithURL:url progress:progress finised:finised];

    [downloader download];
}

下载缓冲池

缓冲池属性

///  下载缓冲池
@property (nonatomic, strong) NSMutableDictionary *downloaderCache;

// MARK: - 懒加载
- (NSMutableDictionary *)downloaderCache {
    if (_downloaderCache == nil) {
        _downloaderCache = [[NSMutableDictionary alloc] init];
    }
    return _downloaderCache;
}

修改下载方法

- (void)downloadWithURL:(NSURL *)url progress:(void (^)(float))progress finised:(void (^)(NSString *, NSError *))finised {

    // 1. 判断下载操作缓冲池中是否存在下载操作
    if (self.downloaderCache[url]) {
        NSLog(@"正在玩命下载中...");
        return;
    }

    // 2. 实例化下载操作
    HMDownloadOperation *downloader = [HMDownloadOperation downloadWithURL:url progress:progress finised:finised];

    // 3. 添加到下载操作缓冲池
    [self.downloaderCache setObject:downloader forKey:url];

    // 4. 开始下载
    [downloader download];
}

下载完成后,将操作从缓冲池中删除

// 2. 实例化下载操作
HMDownloadOperation *downloader = [HMDownloadOperation downloadWithURL:url progress:progress finised:^(NSString *filePath, NSError *error) {

    // 将操作从缓冲池中删除
    [self.downloaderCache removeObjectForKey:url];

    // 执行调用方准备的 finished
    finised(filePath, error);
}];

NSOperation

使用 NSOperation 改造 HMDownloader

修改父类

@interface HMDownloadOperation : NSOperation

重写 main 方法

  • 自定义操作,重写了main方法,在当操作被添加到队列的时候,会自动被执行
  • 不要忘记自动释放池
- (void)main {
    // 自定义操作千万不要忘记自动释放池
    @autoreleasepool {
        // 执行下载
        [self download];
    }
}

修改管理器代码

操作队列

@property (nonatomic, strong) NSOperationQueue *downloaderQueue;

- (NSOperationQueue *)downloaderQueue {
    if (_downloaderQueue == nil) {
        _downloaderQueue = [[NSOperationQueue alloc] init];
    }
    return _downloaderQueue;
}

修改开始下载代码

// 4. 开始下载
[self.downloaderQueue addOperation:downloader];

取消下载操作

- (void)pauserWithURL:(NSURL *)url {
    // 1. 在缓冲池中查找下载操作
    HMDownloadOperation *downloader = self.downloaderCache[url];

    // 2. 判断是否存在下载操作
    if (downloader == nil) {
        NSLog(@"%@", self.downloaderQueue.operations);
        return;
    }

    // 3. 暂停操作,操作队列会认为操作已经完成,会自动将操作从操作队列中删除
    [downloader pause];

    // 4. 将下载操作从缓冲池中删除
    [self.downloaderCache removeObjectForKey:url];
}

重构步骤笔记

重构的目的

  • 相同的代码不要出现两次
  • 相同功能的代码可以及时抽取,以备日后复用,不要重复创建轮子

重构的原则

  • 明确每一步的目标
  • 小步走
  • 测试(每一个改动都有可能出现错误)

抽取代码的步骤

  • 新建方法
  • 复制代码
  • 根据代码调整参数和返回值
  • 调整调用位置代码
  • 测试

抽取类的步骤

  • 示意图
下载目标1.png

抽取主方法

  • 新建类
  • 抽取主方法
    • .h 中定义方法接口,明确该方法是否适合被外部调用
    • .m 中增加方法实现
  • 将主方法复制到新方法中
  • 复制相关的方法
  • 复制相关属性
  • 检查代码的有效性
    • 调整内部变量,让 NSURL 由调用方传递,保证代码的灵活性
  • 复制代理方法,
    • 注释更新 UI 部分的代码
    • 使用 #warning TODO 提醒自己此处有未完成的工作
    • 这样做可以不影响重构的节奏
  • 调整视图控制器 测试重构方法执行
  • 调整视图控制器代码,删除被移走代码
  • 再次测试,确保调整没有失误!

确认接口

  • 确认重构的接口
    • 需要进度回调
    • 需要完成&错误回调
  • 定义类方法,传递回调参数
  • 实现类方法,记录住回调 block
  • 调整调用方法
  • 增加 block 实现
  • 测试
  • 增加已经下载完成的回调
    • 进度回调(100%)
    • 完成回调(路径)
  • 断言
  • 暂停操作
  • 测试,测试,测试!

新问题:如果连续点击,会重复下载,造成错乱!<br /><br />解决办法:建立一个下载管理器的单例,负责所有的文件下载,以及下载操作的缓存!

  • 示意图
下载目标2.png

抽取下载管理器

  • 建立单例
  • 接管下载操作
    • 定义接口方法
    • 实现方法
    • 替换方法
    • 测试
  • 操作缓存
  • 暂停实现
  • 最大并发数,NSOperationQueue+NSOperation

block 小结

  • block 是 C 语言的数据结构
  • 是预先准备好的代码,在需要时执行,类似于匿名函数指针
  • 可以被当作参数传递
  • 在需要时,可以对 block 进行扩展
  • 如果当前方法不执行 block,需要使用 属性 记录
  • block 属性需要使用 copy 描述符
  • 对于必须传递的 block 回调,可以使用 断言
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 217,907评论 6 506
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 92,987评论 3 395
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 164,298评论 0 354
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 58,586评论 1 293
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 67,633评论 6 392
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 51,488评论 1 302
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 40,275评论 3 418
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 39,176评论 0 276
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 45,619评论 1 314
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,819评论 3 336
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,932评论 1 348
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,655评论 5 346
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 41,265评论 3 329
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,871评论 0 22
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,994评论 1 269
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 48,095评论 3 370
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,884评论 2 354

推荐阅读更多精彩内容