iOS 你真的了解MVC吗?(附详细例子说明)

目录
  • MVC的简介
  • MVC三者的职责和关系
  • 标准MVC与非标准MVC的区别及利弊
  • 项目实战应用(标准MVC写法与非标准MVC写法对比)
    • view上面的用户行为事件如何处理?
    • 如何更新对应的数据模型?
    • 数据模型更新了后如何处理?
    • 如何更新 view 上面的视图元素?
简介

摘自百度百科对MVC的解释 MVC

MVC (全名是Model View Controller,是模型(model)-视图(view)-控制器(controller)的缩写,一种软件设计典范,用一种业务逻辑、数据、界面显示分离的方法组织代码,将业务逻辑聚集到一个部件里面,在改进和个性化定制界面及用户交互的同时,不需要重新编写业务逻辑。MVC被独特的发展起来用于映射传统的输入、处理和输出功能在一个逻辑的图形化用户界面的结构中。

三者的职责

  • Model(模型)是应用程序中用于处理应用程序数据逻辑的部分,通常模型对象负责在数据库中存取数据。

  • View(视图)是应用程序中处理数据显示的部分,通常视图是依据模型数据创建的。

  • Controller(控制器)是应用程序中处理用户交互的部分,通常控制器负责从视图读取数据,控制用户输入,并向模型发送数据。

或许很多小伙伴感觉很疑惑,下面我们就结合例子详细说说MVC

一 初识 MVC

我们先看一副图,很清晰明了的描述了三者之间的关系

MVC.png

三者职责解释说明

  • Model层 数据处理层,包括网络请求,数据加工

  • View层 所有App上看得到的界面

  • Controller层 Model 与 View层的中介,把Model数据在View上展示出来

对应箭头解释

  1. view将用户交互通知给controller,通常使用代理。
  2. controller通过更新model来反应状态的改变。
  3. model(通常使用KVO)通知controller来更新他们负责的view
二 变异的MVC

网上还有另外一幅图也很形象的表现iOS实际开发中MVC架构图

image.png

这张图是iOS的MVC架构中最经常出现的图,因为IOS中的Controlller 是UIViewController,所以导致很多人会把视图写在Controller中,这样无疑会导致VC很臃肿。

因此,M-VC 可能是对 iOS 开发中的 MVC模式更为准确的解读,同时更也准确地描述了我们日常开发可能已经编写的 MVC 代码,但它并没有做太多事情来解决 iOS 应用中日益增长的重量级视图控制器的问题。

三 MVC的利与弊

在 iOS 开发中,MVC(Model View Controller)是构建iOS App的标准模式,是苹果推荐的一个用来组织代码的权威范式。

Apple甚至是这么说的。在MVC下,所有的对象被归类为一个Model,一个View,和一个Controller。Model持有数据,View显示与用户交互的界面,而ViewController调解Model和View之间的交互。现在,MVC 依然是目前主流客户端编程框架,但同时它也被调侃成Massive View Controller(重量级视图控制器),想必开发者在开发中无可避免被下面几个问题所困扰:

  • 厚重的ViewController
  • 遗失的网络逻辑(无立足之地)
  • 较差的可测试性

接下来就让我们一起探讨MVC的弊端,剖析问题产生原因,打造一个轻量级的ViewController,明确MVC设计模式中各个角色的职责。

3.1 厚重的View Controller

Model:模型model的对象通常非常的简单。根据Apple的文档,model应包括数据操作数据的业务逻辑。而在实践中,model层往往非常薄,不管怎样,model层的业务逻辑不应被拖入到controller。

View:视图view通常是UIKit控件,View不应该直接引用model(PS:现实中,使用了),并且仅仅通过IBAction事件引用controller。业务逻辑很明显不归入view,视图本身没有任何业务。

Controller:Controller是app的胶水代码:协调模型和视图之间的所有交互。控制器负责管理他们所拥有的视图的视图层次结构,还要响应视图的loading、appearing、disappearing等等,同时往往也会充满我们不愿暴露的model的模型逻辑以及不愿暴露给视图的业务逻辑。

网络数据的请求及后续处理,本地数据库操作,以及一些带有工具性质辅助方法都加大了Massive View Controller的产生。

3.2 遗失(无处安放)的网络逻辑

苹果使用的MVC的定义是这么说的:所有的对象都可以被归类为一个model,一个view,或是一个controller

你可能试着把它放在Model对象里,但是也会很棘手,因为网络调用应该使用异步,这样如果一个网络请求比持有它的model生命周期更长,事情将变的复杂。显然View里面做网络请求那就更格格不入了,因此只剩下Controller了。若这样,这又加剧了Massive View Controller的问题。若不这样,何处才是网络逻辑的家呢?

3.3 较差的可测试性

由于View Controller混合了视图处理逻辑和业务逻辑,分离这些成分的单元测试成了一个艰巨的任务。

四 项目实战
4.1 实际开发中MVC的使用

我们仿头条主页样式写了一个测试用例,然后来讲解实际开发的用法。

1.gif

核心类讲解

  • NewsModel 新闻模型数据类
@interface NewsModel : NSObject

/** id */
@property(nonatomic, copy)NSString *newsId;
/** icon */
@property(nonatomic, copy)NSString *icon;
/** title */
@property(nonatomic, copy)NSString *title;
/** subTitle */
@property(nonatomic, copy)NSString *subTitle;
/** content */
@property(nonatomic, copy)NSString *content;
/** if attention */
@property(nonatomic, assign, getter=isAttention)BOOL attention;
/** imgList */
@property(nonatomic, copy)NSArray *imgs;
/** share number */
@property(nonatomic, assign)NSUInteger shareNum;
/** discuss num */
@property(nonatomic, assign)NSUInteger discussNum;
/** like */
@property(nonatomic, assign)NSUInteger likeNum;
/** if like */
@property(nonatomic, assign,getter=isLike)BOOL like;

@end
  • NewsCell.h 新闻视图类
@class NewsModel;

@interface NewsCell : UITableViewCell

/** model */
@property(nonatomic, strong)NewsModel *model;

@end
  • NewsCell.m 实现类
// 1.我们使用懒加载的形式加载视图
/** icon */
@property(nonatomic, strong)UIImageView *iconImgView;
/** title */
@property(nonatomic, strong)UILabel *titleLbe;
/** subTitle */
@property(nonatomic, strong)UILabel *subTitleLbe;
......  // 粘贴部分代码

// 2.采用masonry约束布局
- (instancetype)initWithStyle:(UITableViewCellStyle)style reuseIdentifier:(NSString *)reuseIdentifier {
    self = [super initWithStyle:style reuseIdentifier:reuseIdentifier];
    if (self) {
        self.contentView.backgroundColor = [UIColor whiteColor];
        [self drawUI];
    }
    return self;
}

#pragma mark - drawUI

- (void)drawUI {
    [self.contentView addSubview:self.iconImgView];
    [self.contentView addSubview:self.titleLbe];
    [self.contentView addSubview:self.subTitleLbe];

    [self.iconImgView mas_makeConstraints:^(MASConstraintMaker *make) {
        make.size.mas_equalTo(CGSizeMake(44, 44));
        make.top.equalTo(self.contentView.mas_top).offset(10);
        make.leading.equalTo(self.contentView.mas_leading).offset(10);
    }];

    [self.titleLbe mas_makeConstraints:^(MASConstraintMaker *make) {
        make.leading.equalTo(self.iconImgView.mas_trailing).offset(10);
        make.bottom.equalTo(self.iconImgView.mas_centerY).offset(-2);
    }];
    
    [self.subTitleLbe mas_makeConstraints:^(MASConstraintMaker *make) {
        make.leading.equalTo(self.titleLbe.mas_leading);
        make.top.equalTo(self.iconImgView.mas_centerY).offset(2);
    }];
    ......  // 粘贴部分代码
}

// 3.设置数据
#pragma mark - set

- (void)setModel:(NewsModel *)model {
    _model = model;
    [self.iconImgView sd_setImageWithURL:[NSURL URLWithString:model.icon]];
    
    self.titleLbe.text = model.title;
    [self.titleLbe sizeToFit];
    
    self.subTitleLbe.text = model.subTitle;
    [self.subTitleLbe sizeToFit];
    ......  // 粘贴部分代码
}

// 4.懒加载形式加载控件
#pragma mark - lazy

- (UIImageView *)iconImgView {
    if (_iconImgView == nil) {
        _iconImgView = [[UIImageView alloc] initWithFrame:CGRectMake(0, 0, 44, 44)];
        _iconImgView.layer.cornerRadius = 22;
        _iconImgView.layer.masksToBounds = YES;
    }
    return _iconImgView;
}

- (UILabel *)titleLbe {
    if (_titleLbe == nil) {
        _titleLbe = [self getLbeWithFont:16 textColor:[UIColor blackColor]];
    }
    return _titleLbe;
}

- (UILabel *)subTitleLbe {
    if (_subTitleLbe == nil) {
        _subTitleLbe = [self getLbeWithFont:14 textColor:[UIColor grayColor]];
    }
    return _subTitleLbe;
}
  • ViewController 控制器
// tableView也使用懒加载的形式
- (UITableView *)tableView {
    if (_tableView == nil) {
        _tableView = [[UITableView alloc] initWithFrame:CGRectMake(0, 64, kScreenWidth, kScreenHeight - 64) style:UITableViewStyleGrouped];
        _tableView.delegate = self;
        _tableView.dataSource = self;
        _tableView.scrollsToTop = true;
        _tableView.backgroundColor = [UIColor whiteColor];;
        _tableView.separatorStyle = UITableViewCellSeparatorStyleNone;
        _tableView.showsVerticalScrollIndicator = NO;
        _tableView.scrollsToTop = YES;
        _tableView.estimatedRowHeight = 250;//预估高度
        _tableView.rowHeight = UITableViewAutomaticDimension;
        [_tableView registerClass:[NewsCell class] forCellReuseIdentifier:cellId];
        __weak typeof(self) weakSelf = self;
        [_tableView addPullToRefreshWithActionHandler:^{
            [weakSelf refreshData];
        }];
        [_tableView addInfiniteScrollingWithActionHandler:^{
            [weakSelf loadNextPage];
        }];
    }
    return _tableView;
}

// 核心代码
#pragma mark - UITableViewDataSource

- (NSInteger)numberOfSectionsInTableView:(UITableView *)tableView {
    return 1;
}

- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section {
    return self.dataSource.count;
}

- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath {
    NewsModel *model = [self.dataSource objectAtIndex:indexPath.row];
    NewsCell *cell = [tableView dequeueReusableCellWithIdentifier:cellId];
    cell.selectionStyle = UITableViewCellSelectionStyleNone;
    cell.model = model;
    return cell;
}

注解说明

  1. 一个基本的数据展示就完成了,相信很多开发小伙伴也是这样做的,如果没有涉及到用户的交互行为,纯粹是做展示,那基本就完工了。但是实际开发中往往不是这样的。
  2. 比如用户点击了分享评论点赞关注删除等按钮,那我们应该如何处理数据及更新页面呢?

接下来我们就从以下几点出发来阐述标准的MVC和非标准的MVC两者之间的区别。

mvc.png
4.1 view上面的用户行为事件如何处理?

当用户在视图上做了点击,那我们应该如何处理用户的点击事件?以本文实例中点赞为例子说明。

说明:当用户点击赞或者取消赞,我们需要告知后台用户的行为,同时更新对应的视图。

4.1.1 标准MVC写法

viewuser action传给VC,一共有三种方式可以将view上的行为传递给VC,分别是代理block通知。本文介绍如果使用代理将用户的行为告知VC。

  • NewsCell.h (声明一个cell的协议,并定义一些方法)
@protocol NewsCellDelegate <NSObject>
// tap like
- (void)didTapNewsCellLike:(NewsModel *)newsModel;

@end

@interface NewsCell : UITableViewCell
/** delegate */
@property(nonatomic,weak)id<NewsCellDelegate> delegate;

@end
  • NewsCell.m (在点击回调方法中调用该协议方法)
// 用户点击了点赞按钮
- (void)tapLike {
    if ([self.delegate respondsToSelector:@selector(didTapNewsCellLike:)]) {
        [self.delegate didTapNewsCellLike:self.model];
    }
}
  • ViewController.m (设置代理并实现代理方法即可)
@interface ViewController ()<UITableViewDataSource, UITableViewDelegate, NewsCellDelegate>

- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath {
    NewsModel *model = [self.dataSource objectAtIndex:indexPath.row];
    NewsCell *cell = [tableView dequeueReusableCellWithIdentifier:cellId];
    cell.selectionStyle = UITableViewCellSelectionStyleNone;
    cell.model = model;
    cell.delegate = self;   // VC作为Cell视图的代理对象
    return cell;
}

- (void)didTapNewsCellLike:(NewsModel *)newsModel {
    // 处理view上的点击事件
}
4.1.2 非标准的MVC写法

有些小伙伴或许想直接在view视图中处理网络,数据,然后更新对应视图,和VC没有关系。

实例代码如下

  • NewsCell.m (直接在视图中调用网络请求并处理数据)
// 用户点击了点赞按钮
- (void)tapLike {
   // 直接发起网络请求并处理回调事件
    __weak typeof(self) weakSelf = self;
    [self.model addLike:^(NSDictionary *json) {
        weakSelf.model.like = !weakSelf.model.isLike;
        if (weakSelf.model.isLike) {
            [weakSelf.likeActionView updateImgName:@"like_red"];
            weakSelf.model.likeNum++;
        } else {
            [weakSelf.likeActionView updateImgName:@"like"];
            weakSelf.model.likeNum--;
        }
        [weakSelf.likeActionView updateTitle:[NSString stringWithFormat:@"%lu",(unsigned long)weakSelf.model.likeNum]];
    }];
}

运行结果

1.gif
1.gif

如果在用户不拖拽的情况下,该方法是可以实现效果的,但是一旦用户进行了拖拽,我们知道UITableView是采用重用机制,所以对应的视图和模型数据都会发生变化,我们可以将请求前和请求后的对象地址打印一下就知道了。

2019-04-14 10:04:16.496819+0800 MVC-Demo[65856:2082156] old model 0x6000011d9680
2019-04-14 10:04:18.745787+0800 MVC-Demo[65856:2082156] new model 0x6000011d9860

由打印结果可知,模型对象发生了变化,所以此种方法不可取。

第二种方法

或许有的小伙伴想,在view中完成网络请求,然后将结果告知VC,然后由VC来更新对应的数据及视图,此种方法可以,但是不推荐这样做,因为UITableViewCell重用机制的原因。

4.2 如何更新对应的数据模型?
4.2.1 标准的MVC写法

根据苹果官方的推荐,模型包含数据处理层,包括网络请求,数据加工及处理。

实例代码如下

  • NewsModel.m (新闻模型对象,里面封装了点赞的网络请求及数据处理)
/// 添加点赞
- (void)addLike:(void(^)(NSDictionary *json))callback {
    NSURLSessionConfiguration *config = [NSURLSessionConfiguration defaultSessionConfiguration];
    NSURLSession *session = [NSURLSession sessionWithConfiguration:config];
    NSURLSessionTask *task = [session dataTaskWithURL:[NSURL URLWithString:API_GetGaoShiList] completionHandler:^(NSData *data, NSURLResponse *response, NSError *error) {
        dispatch_async(dispatch_get_main_queue(), ^{
            if (error == nil) {
                self.like = !self.like;
                if (self.like) {
                    self.likeNum += 1;
                } else {
                    self.likeNum -= 1;
                }
            }
            if (callback) {
                callback(nil);
            }
        });
    }];
    [task resume];
}

  • viewController.m (外界直接调用,监听回调block即可)
- (void)didTapNewsCellLike:(NewsModel *)newsModel {
    __weak typeof(NewsModel *) weakNewsModel = newsModel;
    [newsModel addLike:^(NSDictionary *json) {
        // 更新对应的视图
        [self updateNewsView:weakNewsModel];
    }];
}

运行结果

1.gif

我们将点赞的网络请求处理,数据处理都封装到了模型里面,外界直接调用并监听结果的回调即可。

4.2.2 非标准的MVC写法

有些小伙伴喜欢将网络请求之间写在VC里面,然后在VC里面处理请求和数据的处理。

实例代码

  • ViewController.m (之间在VC中发网络请求,然后通过newId更新对应的数据模型)
- (void)didTapNewsCellLike:(NewsModel *)newsModel {
     // 非标准的MVC写法
    [self postLikeNetwork:newsModel];
}

#pragma mark - like network + data dealwith

- (void)postLikeNetwork:(NewsModel *)newsModel {
    NSString *api = @"http://rap2api.taobao.org/app/mock/163155/gaoshilist"; // 告示
    NSURLSessionConfiguration *config = [NSURLSessionConfiguration defaultSessionConfiguration];
    NSURLSession *session = [NSURLSession sessionWithConfiguration:config];
    NSURLSessionTask *task = [session dataTaskWithURL:[NSURL URLWithString:api] completionHandler:^(NSData *data, NSURLResponse *response, NSError *error) {
        dispatch_async(dispatch_get_main_queue(), ^{
            if (error == nil) {
                [self dealwithLikeData:newsModel.newsId];
            }
        });
    }];
    [task resume];
}

- (void)dealwithLikeData:(NSString *)newsId {
    __block NewsModel *newsModel;
    [self.dataSource enumerateObjectsUsingBlock:^(NewsModel *obj, NSUInteger idx, BOOL *stop) {
        if ([obj.newsId isEqualToString:newsId]) {
            newsModel = obj;
            *stop = YES;
        }
    }];
    if (newsModel) {
        newsModel.like = !newsModel.like;
        if (newsModel.like) {
            newsModel.likeNum += 1;
        } else {
            newsModel.likeNum -= 1;
        }
        [self updateNewsView:newsModel];
    }
}

运行结果

1.gif

这种方法也可以实现功能,而且不会出问题,但是很明显,代码行数增加了,并且会增加VC的臃肿,所以不是很推荐。

4.3 数据模型更新了后如何处理?

一般数据模型更新了后都需要更新对应的视图,那是直接去更新视图操作还是先通知VC,然后让VC去更新对应的视图。

4.3.1 标准的MVC写法

苹果官方推荐当模型数据更新后告知VC,方法主要有三种,分别是delegateblock和通知。根据使用场景,三种方法各有优缺点,本文以block为例讲解。

使用场景:用户更新点赞状态后,需要告知后台,然后更新对应的模型数据,然后在更新视图。

  • NewsModel.m (处理网络请求,更新数据然后回调给VC)
/// 添加点赞
- (void)addLike:(void(^)(NSDictionary *json))callback {
    NSURLSessionConfiguration *config = [NSURLSessionConfiguration defaultSessionConfiguration];
    NSURLSession *session = [NSURLSession sessionWithConfiguration:config];
    NSURLSessionTask *task = [session dataTaskWithURL:[NSURL URLWithString:API_GetGaoShiList] completionHandler:^(NSData *data, NSURLResponse *response, NSError *error) {
        dispatch_async(dispatch_get_main_queue(), ^{
            if (error == nil) {
                self.like = !self.like;
                if (self.like) {
                    self.likeNum += 1;
                } else {
                    self.likeNum -= 1;
                }
            }
            if (callback) {
                callback(nil);
            }
        });
    }];
    [task resume];
}

viewController.m (在回调中更新视图)

- (void)didTapNewsCellLike:(NewsModel *)newsModel {
    // 标准的MVC写法
    __weak typeof(NewsModel *) weakNewsModel = newsModel;
    [newsModel addLike:^(NSDictionary *json) {
        // 更新对应的视图
        [self updateNewsView:weakNewsModel];
    }];
}

标准的MVC写法是在模型对象内部完成数据的处理,然后再告知VC。

4.3.1 非标准的MVC写法

有些小伙伴喜欢在view视图中直接对模型进行操作,数据的处理等操作。例子和之前的类似

  • NewsCell.m(当用户点赞后,直接在视图中发起网络请求,处理数据,然后发通知更新对应的视图)
- (instancetype)initWithStyle:(UITableViewCellStyle)style reuseIdentifier:(NSString *)reuseIdentifier {
    self = [super initWithStyle:style reuseIdentifier:reuseIdentifier];
    if (self) {
        self.contentView.backgroundColor = [UIColor whiteColor];
        [self drawUI];
        [self addNotify];  // 监听通知
    }
    return self;
}

#pragma mark - notify

- (void)addNotify {
    [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(onNotifyModelUpdate:) name:kNotifyModelUpdate object:nil];
}

- (void)onNotifyModelUpdate:(NSNotification *)notify {
    NewsModel *model = (NewsModel *)notify.object;
    if (model == nil) {
        return;
    }
    if (![self.model.newsId isEqualToString:model.newsId]) {
        return;
    }
    // 更新视图操作
    if (self.model.isLike) {
        [self.likeActionView updateImgName:@"like_red"];
    } else {
        [self.likeActionView updateImgName:@"like"];
    }
    [self.likeActionView updateTitle:[NSString stringWithFormat:@"%lu",(unsigned long)self.model.likeNum]];
}

// 用户点击了点赞按钮
- (void)tapLike {
    /**
     * 数据模型更新了后如何处理?
     * 直接发通知,然后视图监听通知并刷新视图
     */
    __weak typeof(self) weakSelf = self;
    [self.model addLike:^(NSDictionary *json) {
        // 发通知
        [[NSNotificationCenter defaultCenter] postNotificationName:kNotifyModelUpdate object:weakSelf.model];
    }];
}

运行结果

1.gif

结果运行正常,没有任何问题,这种写法就只是viewmodel之间进行交互,没有涉及到vc,虽然可以实现功能,但是违背了MVC的设计初衷,在视图中做了很多不该视图做的事情,而且代码也比较多,复杂。

4.4 如何更新 view 上面的视图元素

当用户点赞之后或者取消点赞视图,需要将对应的视图图标替换,那当数据更新后,我们如何更新View上面的视图元素呢?

4.4.1 标准的MVC写法

由VC来更新对应的视图

  • viewController.m (在VC中更新视图)
- (void)updateNewsView:(NewsModel *)newsModel {
    __block NSUInteger index = NSNotFound;
    [self.dataSource enumerateObjectsUsingBlock:^(NewsModel *obj, NSUInteger idx, BOOL *stop) {
        if ([newsModel.newsId isEqualToString:obj.newsId]) {
            index = idx;
            *stop = YES;
        }
    }];
    if (index == NSNotFound) {
        return;
    }
    NSIndexPath *indexPath = [NSIndexPath indexPathForRow:index inSection:0];
    [self.tableView reloadRowsAtIndexPaths:@[indexPath] withRowAnimation:UITableViewRowAnimationNone];
}

通过newsId,找到该模型在数据源中的索引,然后tableView更新对应位置的``cell`即可。

4.4.1 非标准的MVC写法

视图中监听对应通知,然后更新对应的视图,前面例子已经提到过了

  • NewsCell.m (直接在视图中监听模型数据的变化,然后更新对应视图的元素)
#pragma mark - notify

- (void)addNotify {
    [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(onNotifyModelUpdate:) name:kNotifyModelUpdate object:nil];
}

- (void)onNotifyModelUpdate:(NSNotification *)notify {
    NewsModel *model = (NewsModel *)notify.object;
    if (model == nil) {
        return;
    }
    if (![self.model.newsId isEqualToString:model.newsId]) {
        return;
    }
    // 更新视图操作
    if (self.model.isLike) {
        [self.likeActionView updateImgName:@"like_red"];
    } else {
        [self.likeActionView updateImgName:@"like"];
    }
    [self.likeActionView updateTitle:[NSString stringWithFormat:@"%lu",(unsigned long)self.model.likeNum]];
}

1.给cell添加监听通知,当模型数据发生变化后,全局发通知,每一个cell监听到通知后都需要去做判断,即当前cell对应的model是否是需要更新的model,通过newsId来区分。
2.很明显,这种方法显得比较low,而且每一个cell都要添加监听,然后做判断,性能较低。


本文参考
iOS 关于MVC和MVVM设计模式的那些事
杂谈: MVC/MVP/MVVM


本文是我对MVC的一些理解及认知,如果有问题欢迎提问,如有错误,欢迎指正,水平有限,犯错难免,不喜勿喷。本文为原著,如果转载请注明出处,谢谢。


项目连接地址 - MVC-Demo

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

推荐阅读更多精彩内容