iOS开发架构的思考

一、论OOP与AOP编程思想

OOP为Object Oriented Programming的缩写,意为面向对象编程。OOP针对业务处理过程的实体及其属性和行为进行抽象封装和对外提供接口,以获得更加清晰高效的逻辑单元和功能划分。再通过继承和多态行为实现模块的纵向树形扩展,以达到代码的高度重用性、灵活性和可扩展性。因此,OOP关键点是模块化封装。但是随着业务的发展,我们经常会遇到一些通用性的功能操作需要在很多模块中进行使用,亦可称之为横向需求,例如日志统计,性能监控,网络请求等。如果把它们的实现代码和其他业务逻辑代码混杂在一起,并散落在项目不同的地方(直接把处理这些操作的代码加入到每个模块中),这无疑破坏了OOP的“单一职责”原则。此外,如果继承的层次过深,比如有个model,D的关系是A->B->C->D,仅仅想使用D这个model类的功能,则拔出萝卜带出泥,还要再引入A、B、C才能正常使用,模块的可重用性会大大降低,这使得软件系统的可维护性和复用性受到极大限制。
为了能够解决这种横向需求,AOP编程思想开始出现。AOP为Aspect Oriented Programming的缩写,意为面向切面编程,即通过预编译方式和运行期动态代理实现程序功能的统一维护的一种技术。AOP是针对业务处理过程中的切面进行提取,它所面对的是处理过程中的某个步骤或阶段,以获得逻辑过程中各部分之间低耦合性的隔离效果

那么问题来了,如何实现AOP功能呢?在iOS中主要有2种方法:

1. Category

Category顾名思义,就是可以将类的功能进行分组。能够实现在不创建继承类的情况下实现对已有类的扩展。在swift中,已经更新为extension,它比category功能更加强大,不仅可以做方法扩展,还可以做属性扩展。

假设我现在有这么一个需求,就是想在viewController里增加一个弹出loading界面的公共方法,如下图所示:



那么实现有两种方法,一种是实现一个baseViewController,让所有viewController继承这个基类,然后在基类增加一个showLoadingView()的方法,这样所有子类就可以使用这个方法,达到了需求的目的;另一种就是给viewController增加一个loadingView的extension。大概如下:

extension UIViewController {
    func showLoadingViewWithText(text:String) -> Void {
        let loadingView = UIView()
        loadingView.backgroundColor = UIColor.blackColor()
        ......
        self.view .addSubview(loadingView)
    }
}

在我看来,第二种方法的优点显而易见:
a. 没有增加对类的任何依赖关系而实现了对功能的扩展。
b. 对loadingView功能实现了分组,所有和loadingView相关的功能独立到一个文件中管理。
c. 方便了代码的移植,如果我将来想把viewController移植到别的项目,我可以很容易的摘除我不想要的功能,也可以很容易的加上我想要的功能。有效的避免了拔出萝卜带出泥的问题。

Category实现了对类功能的扩展,但是也有其不足的地方,就是不能对类的现有功能进行修改。因此,就引入了Method Swizzling。

2. Method Swizzling

2.1 什么是Method Swizzling?

Method Swizzling 是通过改变特定 selector(方法)与实际实现之间的映射,在 runtime 时将一个方法的实现替换成其它方法的实现。基本原理如下图:

2.2 Method Swizzling有什么风险?

a. 不能在编译时利用那些可用的安全检查;
b. 一旦发生交叉替换,那就是永久替换,并对类永久生效;
因此,这种技术还是尽量少用,只有当你的问题不能用 Swift 的方式解决,也不能用子类、协议或扩展解决时才使用。

2.3 为什么要用Method Swizzling?

当我们想实现AOP进行切面编程时,不可避免的会想到使用动态代理,在开发者不知情和不对现有函数逻辑产生影响的情况下,对现有函数动些手脚,以达到我们的目的。例如,在所有的viewDidLoad中插入一小段通用配置逻辑,对所有的url请求进行域名替换,添加参数等等。

2.4 Swift中如何使用Method Swizzling?

Swift中使用Method Swizzling有几个原则:
a. 继承自NSObject的Swift类,其继承自父类的方法具有动态性,其他自定义方法、属性需要加dynamic修饰才可以获得动态性。
b. 若方法的参数、属性类型为Swift特有、无法映射到Objective-C的类型(如Character、Tuple),则此方法、属性无法添加dynamic修饰(会编译错误)。
c. 纯Swift类没有动态性,但在方法、属性前添加dynamic修饰可以获得动态性。

遵循这些原则,我们就可以在Swift中使用Method Swizzling。下面看一个例子:

extension UIViewController {
    public override static func initialize() {
        struct Static {
            static var token: dispatch_once_t = 0
        }
        
        // 确保不是子类
        if self !== UIViewController.self {
            return
        }
        
        dispatch_once(&Static.token) {
            let originalSelector = Selector("viewWillAppear:")
            let swizzledSelector = Selector("newViewWillAppear:")
            
            let originalMethod = class_getInstanceMethod(self, originalSelector)
            let swizzledMethod = class_getInstanceMethod(self, swizzledSelector)
            
            let didAddMethod = class_addMethod(self, originalSelector, method_getImplementation(swizzledMethod), method_getTypeEncoding(swizzledMethod))
            
            if didAddMethod {
                class_replaceMethod(self, swizzledSelector, method_getImplementation(originalMethod), method_getTypeEncoding(originalMethod))
            } else {
                method_exchangeImplementations(originalMethod, swizzledMethod);
            }
        }
    }
    
    // MARK: - Method Swizzling
    func newViewWillAppear(animated: Bool) {
        self.newViewWillAppear(animated)
        if let name = self.descriptiveName {
            print("viewWillAppear: \(name)")
        } else {
            print("viewWillAppear: \(self)")
        }
    }
}

上例中,viewWillAppear方法的实现会被替换成 initialize中 newViewWillAppear方法的实现。值得注意的是,在 swizzle 后,会递归调用 newViewWillAppear方法,以替换之前原始的 viewWillAppear方法。所有对修改方法执行的操作放在 dispatch_once中,这是为了确保这个过程只执行一次。

3. 小结

在介绍了AOP的思想后,那我们就应该倒向AOP吗?当然不是。不论是OOP还是AOP都有它适用的场景,无论什么方法都不应该过度使用。滥用Category和Method Swizzling也会导致项目混乱。OOP更适合纵向过程,适合业务模块化,比如设计Model,子Model,孙子Model等,形成一套纵向体系。AOP更适合横向过程,适合功能模块化,一个功能命中多个载体,比如在所有的页面增加一个页面出现的统计。

二、我眼中的viewController

1. viewController在做什么?

每一个viewController无非就是在做这几件事:
a. 初始化界面;
b. 请求网络数据或响应用户操作;
c. 根据数据或操作更新界面。



看起来只有三件事,但是其中的逻辑错综复杂,一不留神viewController就洋洋洒洒几千行代码出去了。那么设计一个可读性强和扩展性强的viewController就显得十分重要。

2. viewController的代码布局

我认为,viewController的结构大概是这样的:

class ALYViewController: UIViewController {
    
    //MARK: IB variables
    @IBOutlet var button : UIButton?
    @IBOutlet var label : UILabel?
    @IBOutlet var imageView : UIImageView?
    ......
    
    //instance variables
    private var variable1 : String = ""
    private var variable2 : CGFloat = 0
    private var variable3 : Array<String> = []
    ......

    //init
    override init(nibName nibNameOrNil: String?, bundle nibBundleOrNil: NSBundle?) {
        super.init(nibName: nibNameOrNil, bundle: nibBundleOrNil)
        // Custom initialization
    }
    
    required init?(coder aDecoder: NSCoder)
    {
        super.init(coder: aDecoder)
    }

    
    //MARK: life cycle
    override func viewDidLoad() {
        super.viewDidLoad()
    }
    
    override func viewWillAppear(animated: Bool) {
        super.viewWillAppear(animated)
    }
    ......
    
    //MARK: tableView dataSource & delegate
    
    //MARK: network service
    
    //MARK: custom delegate
    
    //MARK: event response(button & gestureRecognizer action)
    
    //MARK: private & help methods
    func setViewWidth(view: UIView?, width: CGFloat) -> Void {
        if view == nil {
            return
        }        
        var frame = view?.frame
        frame?.size.width = width
        view?.frame = frame!
    }
    
    //MARK: lazy initialize variables
    lazy var messageBarButton : UIButton = {
        let button = UIButton(frame: CGRectMake(0, 0, 38, 38))
        
        return button
    } ()
}

上述代码布局基本是按照代码的阅读先后顺序来写的:
a. 在代码前几行,简明的列出一些基本变量,并不是这些变量对阅读会带来什么帮助,一般人一上来也不会看变量命名,而是C语言编程风格的沿袭,这也是大多数代码的编程风格;
b. viewController的初始化工作,最开始要做的事情;
c. view生命周期函数,了解一个界面流程最重要的地方;
d. tableView初始化的逻辑,因为tableView太重要了,基本上每个页面都会用到,所以单独写出来;
e. 网络服务函数,并不是每个页面都需要用到;
f. 自定义的delegate,用于实现一些界面间回调功能;
g. 按键和手势等的响应行为;
h. 自己封装的一些帮助执行的方法;
i. 变量的lazy初始化。原因摘自别人的文章,getter & setter类似于lazy load。

因为一个ViewController很有可能会有非常多的view,就像上面给出的代码样例一样,如果getter和setter写在前面,就会把主要逻辑扯到后面去,其他人看的时候就要先划过一长串getter和setter,这样不太好。然后要求业务工程师写代码的时候按照顺序来分配代码块的位置,先是life cycle,然后是Delegate方法实现,然后是event response,然后才是getters and setters。这样后来者阅读代码时就能省力很多。

3. 如何简化viewController?

现在流行的iOS架构大概有MVC,MVCS,MVVM。

3.1 MVC

最经典的软件结构设计模式,也是曾经Apple推荐的设计模式,我们在苹果的官方文档中还能够看到MVC的痕迹Apple MVC。MVC基本模型是

就像C语言在编程语言的地位, 现在的设计模式都是基于MVC的演进。但是MVC在现今的APP设计中,已经变得臃肿不堪,很容易使得viewController变的非常庞大和笨重。因为viewController既要负责页面的所有生命周期,又要负责网络处理,还要处理用户的交互操作,导致大量的代码逻辑堆积在viewController,极大的影响了代码的可读性和扩展性。因此,为了解决上述问题,又衍生出了MVCS和MVVM等模式。

3.2 MVCS

MVCS是在MVC的基础上衍生出来的架构模式,S是Store的意思。可以简单的理解为,MVCS是把网络层从Controller中剥离出来,使用独立的Store类进行网络数据的请求和数据处理,从而达到减轻viewController压力的目的。在苹果很多的官方示例中都有这种结构的写法。下面来看一个简单的例子:

class ALYViewController: UIViewController {
    override func viewDidLoad() {
        super.viewDidLoad()
        //处理页面逻辑
        self.view.addSubview(self.displayView)
        //处理网络逻辑
        self.fetchDataFromServer()
    }
    
    func fetchDataFromServer() -> Void {
        self.dataStore.fetchDataFromServiceWithCompletion { [weak self]() in
            if self?.dataStore.dataSource.count > 0 {
                self?.displayView.updateViewWithData((self?.dataStore.dataSource)! )
            } else {
                //默认处理
            }
        }
    }
    
    lazy var dataStore : ALYDataStore = {
        var _dataStore = ALYDataStore()
        return _dataStore
    }()
    
    lazy var displayView : ALYDisplayView = {
        var _displayView = ALYDisplayView()
        return _displayView
    }()
}

class ALYDisplayView: UIView {
    let label = UILabel()
    
    override init(frame: CGRect) {
        super.init(frame: frame)
        self.addSubview(label)
    }
    
    required init?(coder aDecoder: NSCoder) {
        fatalError("init(coder:) has not been implemented")
    }
    
    func updateViewWithData(dataSource:Array<ALYModel>) -> Void {
        self.label.text = dataSource[0].text
    }
}

class ALYDataStore: NSObject {
    //用来存储取回来的数据,字典或数组
    var dataSource : [ALYModel] = []
    
    func fetchDataFromServiceWithCompletion(completion:(Void)->Void) -> Void {
        let url = "http://alibaba-inc.com"
        let parameters = ["username":"zhaizun", "password":"123456"]
        
        self.fetchServiceListWithUrl(url, parameters: parameters, completion: completion)
        
    }
    
    func fetchServiceListWithUrl(url:String, parameters:NSDictionary, completion:(Void)->Void) -> Void {
        let request = NSURLRequest(URL:  NSURL.init(string: url)!)
        let connection = NSURLConnection.init(request: request, delegate: self)
        connection?.start()
        
        let finishCallBack : (Void)->Void = { [weak self](Void)->Void in
            //伪造的json数据
            let json = [["a":"1231"], ["b":"32131"], ["c":"312312"]]
            //从服务器回来的model处理
            for object in json {
                let model = ALYModel()
                model.jsonToModel(object)
                self?.dataSource.append(model)
            }
            completion()
        }
        
        finishCallBack()
    }
}

class ALYModel: NSObject {
    var text : String = ""
    
    func jsonToModel(data:[String:AnyObject]) -> ALYModel {
        let model = ALYModel()
        //Model转化逻辑
        //......
        return model
    }
}

上面这个例子对于MVCS的展现是M->ALYModel,V->ALYDisplayView,C->ALYViewController和S->ALYDataStore。可以看出,大部分的网络服务和数据处理都被移交到了ALYDataStore中去做,而ALYViewController只需要把握请求网络和更新界面的时机。这样就在一定程度上达到了为Controller减负的目的。

3.3 MVVM

MVVM是Model,View和ViewModel的缩写,那么Controller去哪了?ViewModel就是Controller吗?在别的语言可能是,但是对于iOS这是不可能的,所有的页面都是依赖于Controller。那么ViewModel等同于viewController吗?也不是,理解起来并非如此简单,我们需要构造出一个ViewModel的概念,而MVVM比较难以理解和实现也正是因为此概念,ViewModel不是Controller,它所做的事情依然是帮助Controller减负。一个页面有不止一个View,所以也会存在不止一个ViewModel,那么这么多的ViewModel肯定需要有人去管理它,这个工作就需要Controller去完成。

对于一个APP,用户主要需求是看到界面,至于后面有多少控制逻辑和数据处理,用户完全不在乎,所以我们所有的工作都是为界面服务的。因此,我们每个View都有一个Controller和至少一个Model为其服务。而ViewModel的思想正是由此而生,把服务于View的逻辑从View,Controller和Model中剥离出来,放到ViewModel中,让ViewModel去处理View和Model之间的交互,加工和展示工作,而Controller只是在其中起到一个桥梁连接的工具。不过正是因为这个桥梁,使得Controller和ViewModel之间需要大量的粘合代码,这也正是ViewModel开发成本高的地方。其示意图大概如下:



为了便于理解,还是来看一个例子:

class ALYViewController: UIViewController {
    override func viewDidLoad() {
        super.viewDidLoad()
        
        //处理页面逻辑
        self.view.addSubview(self.displayView)
        //网络处理服务
        self.displayViewModel.fetchDataFromServiceWithCompletion { [weak self](success, error) in
            if success == true {
                self?.displayView.updateViewWithViewModel(self!.displayViewModel)
            } else {
                //默认处理
                print(error)
            }
        }
    }
    
    lazy var displayViewModel : ALYDisplayViewModel = {
        var _displayViewModel = ALYDisplayViewModel()
        return _displayViewModel
    }()
    
    lazy var displayView : ALYDisplayView = {
        var _displayView = ALYDisplayView()
        
        return _displayView
    }()
}

class ALYDisplayView : UIView {
    
    let nameLabel = UILabel()
    let ageLabel = UILabel()
    let dateLabel = UILabel()

    override init(frame: CGRect) {
        super.init(frame: frame)
        self.addSubview(nameLabel)
        self.addSubview(ageLabel)
        self.addSubview(dateLabel)
    }
    
    required init?(coder aDecoder: NSCoder) {
        fatalError("init(coder:) has not been implemented")
    }
    
    func updateViewWithViewModel(viewModel:ALYDisplayViewModel) -> Void {
        
        let model = viewModel.dataSource
        
        self.nameLabel.text = model.name
        self.ageLabel.text = model.age
        self.dateLabel.text = model.date
    }
}

class ALYDisplayViewModel: NSObject {
    
    var dataSource : ALYModel = ALYModel()
    
    func fetchDataFromServiceWithCompletion(completion:(success:Bool, error:NSError?)->Void) -> Void {
        let url = "http://alibaba-inc.com"
        let parameters = ["username":"zhaizun", "password":"123456"]
        
        self.fetchServiceListWithUrl(url, parameters: parameters, completion: completion)
        
    }
    
    func fetchServiceListWithUrl(url:String, parameters:NSDictionary, completion:(success:Bool, error:NSError?)->Void) -> Void {
        let request = NSURLRequest(URL:  NSURL.init(string: url)!)
        let connection = NSURLConnection.init(request: request, delegate: self)
        connection?.start()
        
        let finishCallBack : (Void)->Void = { [weak self](Void)->Void in
            //伪造的json数据
            let json = ["name":"1231", "age":"32131", "date":"2016-04-12"]
            //从服务器回来的model处理
            self!.dataSource.jsonToModel(json)
            
            completion(success: true, error: nil)
        }
        
        finishCallBack()
    }
}

class ALYModel: NSObject {
    var name : String = ""
    var age : String = ""
    var date : String = ""

    func jsonToModel(data:[String:AnyObject]) -> ALYModel {
        let model = ALYModel()
        //Model转化逻辑
        //......
        return model
    }
}

上面的例子中,View和Model的部分逻辑都移动到了ViewModel,减轻了Controller的负担。但大家一定会感觉怪怪的,因为model更新了之后还要手动的去让Controller用ViewModel去更新View,感觉多次一举。如果能够实现Model更新了自动去更新View,或者View变化了可以直接去更新Model,那么MVVM就显得更加优雅了。基于这种双向绑定机制的想法,ReactiveCocoa就出现了。但是ReactiveCocoa的使用门槛很高,而且调试困难,建议大家慎重使用,不要为了MVVM而MVVM。

3.4 小结

讲了这么多,那么之前说的笔者认为的viewController呢,我想到的viewController就是MVCS和MVVM的结合,既有Store,又有ViewModel。Store负责网络数据处理,ViewModel负责将数据转化为View需要的内容展示出来。


三、胖model与瘦model

上面介绍了viewController,下面来说说Model。当我们使用网络请求的时候,自然而然的就会涉及到数据处理。那么提供什么样Model才是合适的呢?现在主流的Model有2种,胖Model和瘦Model。

1. 胖model

正常情况下,我们很难从服务器拿到可以直接展示到View上的数据,我们从网络接口解析出来的数据,往往需要再加工才能够让View进行使用。胖Model就是不仅做了承载数据的工作,也做了加工数据的工作。Controller从胖Model这里拿到数据之后,不用额外做操作或者只要做非常少的操作,就能够将数据直接应用在View上。举个例子:

/* 服务器数据:
 json = {
    "name" = "小明",
    "birthDate" = "19990909"
 }
 */

class ALYModel: NSObject {
    var name : String = ""
    var birthDate : String = ""

    func jsonToModel(data:[String:String]) -> ALYModel {
        let model = ALYModel()
        //Model转化逻辑
        self.name = data["name"]!
        self.birthDate = data["birthDate"]!
        
        return model
    }
    
    func formatDate(date:String) -> String {
        let formatter = NSDateFormatter()
        formatter.dateFormat = "yyyyMMdd"
        let date = formatter.dateFromString(date)
        formatter.dateFormat = "yyyy-MM-dd"
        let str = formatter.stringFromDate(date!)
        
        return str
    }
}

2. 瘦model

瘦Model就是只专注于数据的解析,其他的功能都在Adaptor中完成,从而达到和业务脱离的目的。举个例子:

/*
 服务器数据:
 json = {
    "name" = "小明",
    "birthDate" = "19990909"
 }
 */

class ALYModel: NSObject {
    var name : String = ""
    var birthDate : String = ""

    func jsonToModel(data:[String:String]) -> ALYModel {
        let model = ALYModel()
        //Model转化逻辑
        self.name = data["name"]!
        self.birthDate = data["birthDate"]!
        
        return model
    }
}

class ALYModelAdaptor: NSObject {
    
    func formatDate(date:String) -> String {
        let formatter = NSDateFormatter()
        formatter.dateFormat = "yyyyMMdd"
        let date = formatter.dateFromString(date)
        formatter.dateFormat = "yyyy-MM-dd"
        let str = formatter.stringFromDate(date!)
        
        return str
    }
}

3. 小结

在这两种Model中,我更倾向于瘦Model,然后提供Adaptor,进行数据转化。因为如果把转化逻辑写进了Model中,那么就等于Model中耦合进了业务逻辑,那么将来的可复用性就会有所降低。而瘦Model中,业务逻辑都存在于Adaptor中,将来如果业务变动,只要改动,增加和删除Adaptor就好了。

四、我对代码规范的理解

  1. 代码整齐,有必要的换行缩进,看起来舒服;
  2. 函数功能分类,每个功能用#pragram mark - 或//MARK:区分开,并附有说明;
  3. 函数命名可读性强,让别人根据函数名即可知道函数的功能。如果函数的功能比较复杂,应有关键步骤的注释,有函数参数,返回值和函数功能的注释说明;建议使用VVDocumenter写函数注释。
  4. 函数中的变量要有意义。不要出现int a,var b这样的字样。尽量采用text,number,date,width等有意义的文字。如果零时使用,可以采用tmp,local等前缀字样,让别人快速的明白是零时使用。
  5. 一个类单独形成一个文件。如果一个文件中包含多个类,那么这些类之间必须要有相互关系或一个体系,比如纯虚基类和实现子类,一个类和仅仅为该类服务的help类。
  6. 类的功能由自己内部实现,而不是通过外部设置,万不得已不对外暴露变量,对外只提供更新的接口。外部只需要提供简单的设置和数据就能够让类完成自己的大部分功能。
  7. 资源文件按业务功能模块分开。比如Images.xcassets拆分出多个,每个模块有自己独立的Images.xcassets文件,类似ModuelA.xcassets,ModuelB.xcassets等等,通用的icon文件放在Images.xcassets中。
  8. 看代码时,不用文档,或很少文档,就能让其他人上手。模块目录按照功能进行分类,比如联系人页面所有涉及到的Controller,View、Model和资源文件一起放在Contacts文件夹里。我见过有的代码是将所有Controllers放在一个文件夹中,Views放在一个文件夹中,Models放在一个文件夹中,这样的文件目录读起来感觉非常不方便。
  9. 粒度够细,集成度高,复用度高。功能函数化,参数变量化。能够达到改动一个地方,所有地方生效的结果。
  10. 尽量使用自定义View,View封装的粒度要细,自己能够完成大部分功能,对外仅暴露初始化接口和回调,便于移植和修改。
  11. image使用时,icon使用imageNamed函数初始化,图片文件采用imageWithContentsOfFile函数初始化,用以防止不必要的缓存。
  12. 网络请求采用统一的接口设置,便于将来接口的修改。

五、引用的文章

https://segmentfault.com/a/1190000004715337
http://casatwy.com/iosying-yong-jia-gou-tan-kai-pian.html
http://blog.devtang.com/2015/11/02/mvc-and-mvvm/
http://gracelancy.com/blog/2016/01/06/ape-ios-arch-design/

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

推荐阅读更多精彩内容