安卓中MVC模式的深度思索和实践(一)

这是一个有关安卓MVC框架模式的短系列,目的是思索和分析安卓中MVC模式更为真实的一面。

随着安卓开发这些年的发展,项目开发相关的框架模式一直是个比较火的话题,一度从传统忠实的MVC,聊到热火朝天的MVP,再到看似终极的MVVM,真有些百家争鸣的味道,是个好现象。不过呢,网上太多文章要么观点大同小异(ctrl+c+v),要么模棱两可,要么是铁杆粉丝不屑其它,而具有个人深度思考的文章却是较少。笔者呢,其实是MVC模式的支持者,感觉呢,现在安卓中MVC被人误解偏多,故经过长期思考,写成此文,以表我MVC真实面目,希望能对读者有所启发。而若有MVP或者MVVM坚定的支持者,或许不适合阅读此文,不过还是建议读一下,起码能看到些不一样的东西。
转载请标明出处:http://www.jianshu.com/p/08e461201cd4

1. MVC的常见认识

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

MVC在包括后端、前端和移动端的多个领域都用使用,不同领域的理解和应用都有些差别。对于上述这种界定,很多安卓开发童鞋,大体是这么认为的:

  • View:对应于布局等展示类文件
  • Model:实体模型 ,存取数据
  • Controllor:对应于Activity 和Fragment,业务逻辑+路由功能+视图逻辑

这种观点暂且不论,但基于上述观点的一个衍生观点是值得怀疑的,即View对应于布局文件,能做的事情特别少,关于该布局文件中的数据绑定的操作,事件处理的代码都放在Activity/Fragment中,造成了Activity既像View又像Controller,且容易臃肿;这种观点整体基于一个实践,即activity完全负责view的数据接入甚至交互,可是这个实践未必是个得当操作。比如,一个绿色进度条,当进度超过50%,将会变成红色,这个过程伴随的是数据变化,体现的是个交互过程,不过这个颜色转变过程,完全可在进度条这个View内部实现,而不必让controller控制太多--by CysionLiu。

列举上述例子,实际上是对网上很多有关MVC说法的一种怀疑:controller由于管这管那,职责太多,并且随着业务一多,其自身就会臃肿,难以维护。然而,这是MVC自身的问题吗?

2. MVC怎样一步步被玩坏的

为了说明这个问题,接下来,我们用==伪代码==的方式,看看controller是如何一步步被玩坏的。

1.首先来个实体类Person.class和数据获取接口ICall;

public class Person{
    public String name;//伪代码,仅为说明问题
    public int age;
    public String address;
}

public interface ICall{
    void act(Person person);
}

2.DemoActivity的初始代码,其实现ICall;

public class DemoActivity extends Activity implement ICall{
      //省略了一些逻辑
     private TextView mTextShow;
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        mTextShow = (TextView) findViewById(R.id.text_show);
        getData(this);//也可以由其它方式触发,例如点击button,最后回调act方法
    }
    //ICall方法
    public void act(Person person){
       //...
    }
}

3.第一份需求来了,让mTextShow展示一个Person的名称,act()为DemoActivity回调ICall的方法;

    public void act(Person person){
        mTextShow.setText(person.name);
    }

4.第二份需求来了,让mTextShow展示一个Person的年龄,大于60岁,则显示红色;

    public void act(Person person){
        String ageStr = String.valueOf(person.age);
         mTextShow.setText(ageStr);
        if(person.age>=60){
            mTextShow.setTextColor(Color.Red);
        }
    }

5.发现个问题,person的年龄不能小于0或者大于150;且鉴于颜色太少,来第三份需求,让mTextShow展示一个Person的年龄,大于50岁且小于60岁,则显示黄色;大于60岁,显示红色;非正常年龄,显示灰色的0岁;

    public void act(Person person){
        String ageStr = String.valueOf(person.age);
         mTextShow.setText(ageStr);
        if(person.age>=50&&person.age<=60){
            mTextShow.setTextColor(Color.Yellow);
        }else if(person.age>60){
             mTextShow.setTextColor(Color.Red);
        }else if(person.age<0||person.age>150){
            mTextShow.setTextColor(Color.Grey);
            mTextShow.setText("0");
        }
    }

6.现在显示上基本没问题了,可是数据有点不适宜的地方,就是act的参数有时可能是null,针对这种情况,希望能显示之前不为null的数据或者占位数据;

    Map<String,Person> recordMap;//伪代码,省略一些语句
    public void act(Person person){
        if(person==null){
            person = recordMap.get("lastPerson");
            if(person==null){
                person = new Person("占位");
            }
        }else{
            recordMap.put("lastPerson",person);
        }
        String ageStr = String.valueOf(person.age);
         mTextShow.setText(ageStr);
        if(person.age>=50&&person.age<=60){
            mTextShow.setTextColor(Color.Yellow);
        }else if(person.age>60){
             mTextShow.setTextColor(Color.Red);
        }else if(person.age<0||person.age>150){
            mTextShow.setTextColor(Color.Grey);
            mTextShow.setText("0");
        }
    }

通过上述过程,使得原本一行的act方法工作量,赫然增长到17行。对于不同心态和技术阶段的开发童鞋来说,以上过程可能都能见到缩影。一个简单的View配上一个简单的业务,就能使代码量变大10倍以上,那么对于页面复杂逻辑也复杂些的页面,按照上述方式,作为中间调节者的controller,可以预见,定会变得臃肿且不易维护--by CysionLiu。

假使上述过程数据的获取是由点击一个button引发,从网络得到数据Person并返回。则一个经典的MVC调用链就形成了,即V->C->M:M->C:C->V;这里,箭头->代表调用;冒号:代表内部处理。

3. MVP简单介绍以及相关思考

由于上述问题的存在,MVP兴起了,打着解耦、清晰的旗号,招揽了不少支持者。其字面定义什么的就不再此处说了,就列出几点和MVC有主要差别的,并配上大家基本都见过的图。
MVC和MVP的差别主要有以下三个方面。

  1. 各部分之间的通信,都是双向的。
  2. View 与 Model 不发生联系,都通过 Presenter 传递。
  3. View 非常薄,不部署任何业务逻辑,称为"被动视图",即没有任何主动性,而 Presenter非常厚,所有逻辑都部署在那里。


    image

这里用MVP来粗略实现上述Person的例子,来看看MVP的效果。

  1. 其它大体同上,定义IView接口和Presenter,DemoActivity实现IView,如需要,Presenter也要实现IView;
public class Presenter implement ICall{
    //省略某些逻辑
    private IView callback;
    public Presenter(IView aCallBack){
        callback = aCallBack;
    }
    public void getData(){
       Model.get(this);//模拟从model获取数据,传入ICall回调
    }
    public void act(Person person){
        //...
    }
 }
public interface IVew{
    void showAge(String ageStr,Color color);
}

2.DemoActivity的代码

    private TextView mTextShow;
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        mTextShow = (TextView) findViewById(R.id.text_show);
        Presenter presenter = new Presneter(this);
        presenter.getData();
}

3.接下来就是Presenter中act代码

    public void act(Person person){
       if(person==null){
            person = recordMap.get("lastPerson");
            if(person==null){
                person = new Person("占位");
            }
        }else{
            recordMap.put("lastPerson",person);
        }
        String ageStr = String.valueOf(person.age);
        if(person.age>=50&&person.age<=60){
            callback.showAge(ageStr,Color.Yellow);
        }else if(person.age>60){
            callback.showAge(ageStr,Color.Red);
        }else if(person.age<0||person.age>150){
            callback.showAge("0",Color.Grey);
        }
    }

4.DemoActivity的设置代码

public void showAge(String ageStr,Color color){
    mTextShow.setText(ageStr);
    mTextShow.setTextColor(color);
}

经过上述过程,可以看出,Activiity的确轻量不少,而且由于多了P层,代码也清晰了不少。由于笔者对MVP不是很熟悉,仅是写过几个demo,研究过github上那几个国内外的MVP-Retrofit-RxJava等类似技术的开源项目的源码;这里只说几点疑问和感受吧,未必正确,仅抛砖引玉:

  • 解耦,基本就是解耦V和M吧,那么也就是这么认为,M变化,V可能不变;V变,M也可能不变。但此处,注意,只是 可能而已;例如,原来V是个progressbar,现在成了ratebar或者imageview+textView,那么IView要变,则M不变也不大可能,顺带着,作为中间者的presenter也难保持不变,实现那个IView接口的那些子类--activity也要跟着变。也就是说,这个解耦并不完全,只是更多的从小粒度的变化实现了解耦,对于大的视图/业务逻辑的改变,解耦作用有限。

  • 清晰,View(包括实现了IView的acty和fragment)展示和轻度UI交互,M存取数据,P处理各路交互;中间通过接口协议,各自开发,思路清晰。上述Person例子中,使用MVP的确清晰了些,但是我们模拟上述Person的例子的调用链,V->P:P->M:M->P:P->V:V,发现比MVC的调用链变的复杂。也就是说,层内部的代码清晰带来了层间的复杂度。

  • 大业务,这点也是笔者最为疑惑的地方。因为读者并未实操过业务比较复杂的MVP模式的项目,而网上所有可见的MVP项目的业务,就如他们的作者说言,学习和锻炼而已,业务实在简单,对于这样的项目,MVP的确比着==深度耦合MVC用法==更为清晰和解耦,但其本身也未突出明显的好处,因为如前所述,MVC是==被==用成了深度耦合。比如再说下,解耦的那块,还记得MVC实现Person例子的3,4的需求过度吧,原来设置person.name;后来变成了设置person.age;也就是说,这些是很正常的需求变更,那MVP中IView的确应该有pserson.name的协议,甚至person.address的协议。这里,问题来了,person,如此小的对象,就对应了个一个3个方法大小的接口。若Person复杂到数十个属性,岂不是接口也要扩大到数十个;伴随这种情况的后果是,Person属性变化和UI变化,也会出现粒度较粗的耦合。再者,当复杂到如此程度的接口出现,那么设计者不得不考虑减小接口的体积和复用,好吧,这二者又带来了学习成本和复用耦合度。另外,复杂的业务也会使使用者也不得不考虑P层太厚带来的问题--by CysionLiu。

上述也不是笔者危言耸听,毕竟MVP早已提出但并未广泛使用和标准化;笔者更多的是疑惑,比如说个笔者做的项目的一个页面吧,若==单纯==是用MVP替换MVC,笔者真是难以看出会有什么好处。该页面需求如下:

  • 头部UI+ webview+ 中部UI + 两个数据和风格不相干的List;
  • 3个动画动效;
  • 头部+中部UI有10+个元素需要填充数据;
  • 5个url请求,对应2个url-host;
  • 结果json中有数字作为key
  • 分享
  • webview中有js交互,有视频播放需要优化,有下载图片
  • 统计,5个埋点以上,最多的点传递数据多达13个;
  • 底部List有上拉加载事件;
  • ...

上述情况,使用MVP估计也不会多清晰、解耦和轻量吧。因为本文主要是来探索MVC的,对MVP更多的只是用来佐证,以探索和回归MVC,所以此处不再继续MVP了。

4. 思索回归,MVC的真实面目

我觉得 MVC 在今天,已经不是一个统一的概念了,很多都是把自己的想法强加到 MVC 上,什么叫 MVC?Android 里的 MVC 又是什么?首先官方貌似没给出这种架构规范。按很多博客列举图中画的 MVC ,Model 层 更新 View,然后把 layout xml 定义为 view 层,那么 Model 层是怎么更新的 layout xml ?如果要说 Android 中的 MVC,应该是 controller 更新 view 吧,怎么会是 model 更新 view。MVC 本来就是一个很老的概念,在 MVC 这个概念出现的时候 和 现在的情况已经不一样了,还有什么 MVP ,可能只是换个名字罢了,我觉得介绍这些模式的时候 说 思想就可以了,怎么分层,说什么 MVP 从 MVC 演化而来,然后各种对比 Android 里 MVC 是什么样子,MVP 又是什么样子,或许全都只是一些技术爱好者的个人偏好而已,因为 Android 里的 MVC 并没有所谓的标准写法,MVC 也不是因为 Android 而提出的,而是很多人非要把某种写法强加上 MVX 这么一个概念,而很多人的说法还不一致,画的图 和 写法都不一样,越看越懵,也就是说,我们完全可以撇开旧情节,直接把Presenter叫做Controller,也就没有MVP这种提法了。

好了,上述大段其实说到底就在两个字--思想。那么所谓MVP,MVVM甚至MVC都体现了什么思想呢?是分层,也就是说,不管MVX,体现的都是分层思想,甚至MVC本身在安卓开发中,代表的即使分层思想,也就是说,MVX只是差别于概念和几种语法使用不同而已。
那么,分层意味着什么呢,意味着职责单一和清晰化。那么职责如何单一和清晰化呢,上述MVP的person例子倒能体现出一些,不过其分层思想没有变化;也就是可以认为,对于MVC,使用一些语法和结构上的技巧,就能实现职责单一和清晰化,进一步实现分层,打造易维护,易拓展的结构,而之前MVC的各种被提出的缺点,某种程度上,大部分都与使用者自身的分层思想和技术水平相关。而一些使用了类似接口分层方式的,便给起了个不同的名字,叫做MVP,本质上,还是MVC,还是分层思想,并没有使数据-展示型应用在结构上有本质的不同。

那么MVC怎么体现分层思想的呢,这又要回归到最初的提法了,视图--控制--数据,对,还是这三个;不过本着职责单一又清晰的观点,我们对这三层,来个新的认识。

  • 视图V,即展示,什么是展示呢?举个例子,打开开关,灯泡亮了,这个亮了就是展示;那么,若电压低了,灯泡变暗呢?没电了,灯泡不亮呢?这些,都是展示,有些童鞋肯定会认为这是控制,是交互。这就牵扯到交互控制和展示之间的一些区别了,这里,笔者认为,固有的,基本不受外界控制者改变而改变的行为,都可认为展示。也就是说,灯泡的变亮,变暗甚至不亮,都与控制者的改变(注意,不是控制的改变)无关,而是当发生这种控制行为,必然会发生的--by CysionLiu。
    对应到安卓中,以LinearLayout为例,不管使用者怎么改变,它要么纵向布局,要么横向布局,其本身就是这样。而LinearLayout是什么呢,一个官方提供的View,也是我们认为的一种视图。由此可以知道,MVC中V不仅仅包括xml文件,其应该包括的是众多官方提供的View和具有特殊性质的自定义View,这点也比较容易理解,很多开源框架不就是自定义view吗?使用者只需稍稍配置一下,很多炫酷特性便在View内部实现了,根本不需要M或者C干什么多余的事情。整体来说,V层,就是要好好的担起展示的职责,不要把属于展示的职责,交给其它层去做。
    理论总是有些枯燥,这里,还是以Person那个例子说明一下吧。
    这里,定义个AgeTextView.class;
public class AgeTextView extend TextView{
      //省略某些逻辑
      public void setAge(int age){
            String ageStr = String.valueOf(age);
            setText(ageStr);
            if(person.age>=50&&person.age<=60){
                setTextColor(Color.Yellow);
            }else if(person.age>60){
                 setTextColor(Color.Red);
            }else if(person.age<0||person.age>150){
                setTextColor(Color.Grey);
                setText("0");
            }
      }
  }

那么,对于之前MVC实现该例子中的第6点来说,DemoActivity中代码就变成了以下方式。

    Map<String,Person> recordMap;//伪代码,省略一些语句
    private AgeTextView mTextShow;
    public void act(Person person){
        if(person==null){
            person = recordMap.get("lastPerson");
            if(person==null){
                person = new Person("占位");
            }
        }else{
            recordMap.put("lastPerson",person);
        }
        mTextShow.setAge(person.age);
    }

通过以上变化,act方法中代码一下少了一半;而对于V的职责上,也更清晰了,C层也变的轻量起来。毕竟我们定义的这个AgeTextView控件,本身就是对应这种显示age的属性。上述方式只是从V的角度去进行分层职责化,本身是基于一个前提的,即一个页面中只有少数的UI才具有比较复杂的展示属性,而这本身,的确也符合一般产品的开发过程。

  • 数据M,即数据存取和操作;这里,数据的操作往往是一些开发同学反对的,认为数据的操作属于业务逻辑,不该存在于M层。但是,我们看名字也能看出来,数据操作和业务逻辑是有差别的。比如上述例子中的age吧,age<0的判断是属于业务逻辑呢,还是数据操作呢?我想,绝大多数童鞋应该会赞同是数据操作吧,因为无论什么业务,age<0,都属于应该避免的情况。除了这种,M就不能出现业务逻辑了吗?也未必,如果按照上述例子中age不同,text颜色不同来认为是业务逻辑,的确不能在M中出现业务逻辑;然而在实际开发中,很多时候都是M可以处理的逻辑,比如缓存操作,当C需要M提供数据时,比较好的职责划分是,C只管拿到数据,而不用管M内部是如何管控的数据。依据这个思路,可以进一步修改上述Person例子,比如之前DemoActivity和IPresenter中都出现了类似代码:
 public void getData(){
       Model.get(this);//模拟从model获取数据,传入ICall回调
    }

这里,我们补充下model类中的逻辑,让其进行校验,缓存等操作;

public class Model{
       Map<String,Person> recordMap;//伪代码,省略一些语句
       public void get(ICall call){
        Person person = fromRemoteOperation();//从远程/数据库等获得数据
        if(person==null){
            person = recordMap.get("lastPerson");
            if(person==null){
                person = new Person("占位");
            }
        }else{
            recordMap.put("lastPerson",person);
        }
        call.act(person);
    }
}

通过以上操作,DemoActvity中act方法变成了:

AgeTextView mTextShow;
public void act(Person person){
    mTextShow.setAge(person.age);
}

看吧,没有通过MVP,也实现了Controller的轻量化,仅留了一行代码;另外如前所述,对于V,将一些交互展示类逻辑放在view中处理,不仅使View显得更具有血肉,也使C层职责缩小,更为清晰;对于M,其自身把控数据的校验和缓存等操作,也使得其职责更为合理,也能使C层轻量和清晰,这里补充下,对于M来说,很多时候是充当请求的处理者,而作为请求的发送者有时候需要通过发送命令来配置和选择处理者,这本身也体现了命令模式的特点,操作得当,可较好的使请求的发送和处理方解耦--by CysionLiu。

以上,也只是从这个简单例子说明了另一种实现MVC的方式,主要是想给MVC正名,它并不一定臃肿。可能有些童鞋认为demo比较简单,但从笔者自身的项目来看,是比较可行的,也不会有MVP那种接口乱飞的复杂化情况。当然,C层的处理也非常重要。

  • 控制器C,其实更像调度器,在最开始的描述中,很多人倾向于认为其职责是业务逻辑+路由功能+视图逻辑,这,真的有些重。其实经过上述V和M的介绍,基本这里得出结论,很多业务逻辑和视图逻辑,都可以给V和M来处理,对,是还给。而难以交还的那些业务和视图逻辑,多半是由于这些逻辑往往组成了视图业务逻辑,难以分开。此时,我认为应该采用代理的思想(非模式),即业务工具类(Helper)。将一个Activity(View)中的视图业务逻辑“委托”给该工具类(View持有)。我们可以在View中实现针对View的动作的处理,比如数据刷新等,在工具类中对业务进行真正的处理。比如此处,再回到Person例子,不用自定义AgeTextView,而是委托给以下帮助类;
public class DemoHelper{
        public static void showAge(TextView ageShow,int age){
            String ageStr = String.valueOf(age);
            ageShow.setText(ageStr);
            if(person.age>=50&&person.age<=60){
                ageShow.setTextColor(Color.Yellow);
            }else if(person.age>60){
                 ageShow.setTextColor(Color.Red);
            }else if(person.age<0||person.age>150){
                ageShow.setTextColor(Color.Grey);
                ageShow.setText("0");
            }
        }
}

在DemoActivity的act方法中,调用上述方法:

TextView mTextShow;
public void act(Person person){
      DemoHelper.showAge(mTextShow,age);
}

这种委托思想,对业务的优化整合更为集中以及复用,也免去了频繁的数据UI变动对二次接口的调用流程。但这种方式会带来C和这个Helper类的耦合,不过鉴于这种视图业务逻辑不会太多,这种思路也是比较可行的。
当然,有关C的重头戏当然不是这个,刚才也提到C中还有调度功能,而这个调度功能的理解和使用也是一个非常重要的优化方式。先来聊聊调度吧,调度,可认为是在合适的时间,将合适的资源安排给合适的用户。表现在代码上,即V->C->M和M->C->V;当然这里的M和V都是经过充分职责化的,也就是说,调度时尽量避免有抢夺M和V职责的现象。另外,提到调度,自然会带来一个管理体系,这个从任何管理/服务体系都可以看出来,比如公司的组织结构关系等。也就是说,调度没必要甚至说,尽量不要,只维护一个大调度器,而是要构建一个调度体系,上级调度管理下层调度,下层调度管理下下层调度。而每层调度器的职责,基本可认为有两项:一是管理下层调度器,二是处理本层的一些必须的视图业务逻辑。
说是这样说,可是怎样在代码中做呢?当然,这个官方提供了思路,我们也基本都已在使用,那就是Fragement和复用View,可是大家会说,这个我们早知道了,而且不管用啊,Fragment也容易变臃肿啊。是的,现在Fragment已经被用做了大的Controller,它的确已经可以被当做顶级的C来对待。
Fragment不合适,那么,复用View呢?也不适合,或者说应该要经过改造过才适合。毕竟一个单View一般只是个视图载体,没法携带数据和进行业务交互。那么给他一个载体,让其既能体现视图,也能处理业务逻辑和调度,不就变成了一个小Controller了吗?怎样才能实现这样呢?聪明的读者可能想到了--ViewHolder,对,就是它,这个为复用布局和列表item逻辑的一个小载体,是一个比较理想的小型C。那怎么使用它呢?没什么高大上的,大家都用过,无论是对ListView还是对RecyclerView,官方都提供了对ViewHolder(Vh)的支持,更为关键的是,它们都支持根据不同的itemType,可对应不同的Vh,设计多么好啊,既实现了复用,也实现了模块化。此时,有童鞋可能有疑问了,我们用了那么多次这些List或是recycler啦,也没见Vh有怎么好的。针对这个疑问,我想借助类似组合模式的思想来解释下,组合模式能使用户对单个对象和组合对象的使用具有一致性;那么对于页面来说,我们可以将一个单布局item,看成多个列表item罗列而成,也可以将多个列表item看做单一布局item,怎么实现,将在本系列第二篇文章来介绍,这里不再继续了。
总体来说,对于C层的优化,是来自于分而治之的思想,而对于复杂页面来说,通过这种分而治之,也就实现了页面级别的模块化思想;使得C层清晰且轻量。

写在本篇最后,本文主要目的是,希望通过一步步分析和demo示例,揭示MVC更为真实和精致的一面。篇幅较长,可能看着乏味也不好理解。在接下来的一篇中,将主要针对C层的优化思想来场实战,以使得笔者的观点更容易理解。

篇幅较长,码字不易。

欢迎交流,共同进步!

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

推荐阅读更多精彩内容