Android——MVVM之DataBinding(一)

个人博客:haichenyi.com。感谢关注

<span id = "c1"></span>

1. 目录

<span id = "c2"></span>

2.DataBinding的疑惑

  假设你已经会用databinding,不会就去看一下怎么用,很简单,

  1. 下面是新建一个xml,名字叫layout_test1根布局是layout,你重新编译一下项目之后系统就会生成LayoutTest1Binding,以你的xml的名字加binding驼峰命名
  2. AgeData类两个属性,一个名字,一个年龄。
databinding方法图.png

  如上图所示,我们只要按照DataBinding规定的写法,我们通过获取到的引用就能拿到布局里面的控件,设置完数据,就能更新页面。

  我们平常页面更新的三个流程:

  1. 在onCreate生命周期方法里面通过setContentView设置页面布局
  2. 通过findViewById获取到控件的引用
  3. 通过控件的引用调用对应的方法TextView的就是setText方法去更新

  到DataBinding这里怎么就什么都不需要做了,就直接可以用了呢?

  欢迎大家步入DataBinding的神奇世界,我来为大家一一解惑。

<span id = "c3"></span>

3.设置页面布局

  这个其实最简单,我们先来看一下它的用法,调用的一个工具类的setContentView方法,传递了一个泛型,如下代码:

val binding =
            DataBindingUtil.setContentView<LayoutTest1Binding>(this, R.layout.layout_test1)
binding.ageData = AgeData("张三","18岁")

  那么,为什么按照它这个调用方式就能设置布局呢?我们来看一下它的setContentView方法:

//我们调用的这个方法,它又调用的一个setContentView重载方法
public static <T extends ViewDataBinding> T setContentView(@NonNull Activity activity,
            int layoutId) {
    return setContentView(activity, layoutId, sDefaultComponent);
}

//内部调用这个方法
public static <T extends ViewDataBinding> T setContentView(@NonNull Activity activity,
            int layoutId, @Nullable DataBindingComponent bindingComponent) {
    //看到了嘛?等于说它实际上还是调用的activity的setContentView方法,只不过这里,它做了额外处理,拿到了传递的view
    activity.setContentView(layoutId);
    //decorView不是我们的重点,可以简单说一下,我们所写的布局只是我们自己的内容的根布局。decorView是整个界面的根布局
    View decorView = activity.getWindow().getDecorView();
    ViewGroup contentView = (ViewGroup) decorView.findViewById(android.R.id.content);
    //看整个名字,绑定views
    return bindToAddedViews(bindingComponent, contentView, 0, layoutId);
}

  上面,我们看到了实际上,他还是调用的activity的setContentView方法去设置的布局。

<span id = "c4"></span>

4.获取view引用

  我们平时设置布局,findViewById找到控件,然后通过调用控件的方法去设置内容,从而达到更新页面的功能。那么,databinding是怎么获取到控件的呢?我们接着上面的方法往下看:

//这里的parent就是我们的内容布局,也就是我们写的布局的上一层
private static <T extends ViewDataBinding> T bindToAddedViews(DataBindingComponent component,
            ViewGroup parent, int startChildren, int layoutId) {
    //获取全部子view的个数
    final int endChildren = parent.getChildCount();
    final int childrenAdded = endChildren - startChildren;
    //判断当前获取的子view是不是一个
    if (childrenAdded == 1) {
        //如果是一个,就直接获取
        final View childView = parent.getChildAt(endChildren - 1);
        //开始绑定
        return bind(component, childView, layoutId);
    } else {
        //如果不是一个,就看有多少个,创建一个数据
        //for循环把子view的引用全部放进数据里面
        final View[] children = new View[childrenAdded];
        for (int i = 0; i < childrenAdded; i++) {
            children[i] = parent.getChildAt(i + startChildren);
        }
        //开始绑定
        return bind(component, children, layoutId);
    }
}

  我们再来看看这里绑定的方法,这里调用的方法其实都是差不多的,都是重载方法,一个是传view,一个是传的view类型的数据。

static <T extends ViewDataBinding> T bind(DataBindingComponent bindingComponent, View[] roots,
            int layoutId) {
    return (T) sMapper.getDataBinder(bindingComponent, roots, layoutId);
}

static <T extends ViewDataBinding> T bind(DataBindingComponent bindingComponent, View root,
            int layoutId) {
    return (T) sMapper.getDataBinder(bindingComponent, root, layoutId);
}



public abstract class DataBinderMapper {
    public abstract ViewDataBinding getDataBinder(DataBindingComponent bindingComponent, View view,
            int layoutId);
    public abstract ViewDataBinding getDataBinder(DataBindingComponent bindingComponent,
            View[] view, int layoutId);
    public abstract int getLayoutId(String tag);
    public abstract String convertBrIdToString(int id);
    @NonNull
    public List<DataBinderMapper> collectDependencies() {
        // default implementation for backwards compatibility.
        return Collections.emptyList();
    }
}

  我们点进去发现跳转到了一个抽象类的方法。然后,怎么办呢?方法调用,不是静态的,肯定得看对象呀。静态的是类.方法名,不是静态的,变量.方法名。我们回过头来看这个调用的对象的类型。

public class DataBindingUtil {
    //他的实际类是DataBinderMapperImpl,我们点到这个类里面去看一下
    private static DataBinderMapper sMapper = new DataBinderMapperImpl();
    //省略掉不需要的
    ...
}

public class DataBinderMapperImpl extends MergedDataBinderMapper {
  DataBinderMapperImpl() {
    //熟悉嘛?这里有我们的包名,还有一个实现类,我们点到这个实现类里面去看,放到后面再说
    addMapper(new com.haichenyi.hcy.DataBinderMapperImpl());
  }
}

//这个方法addMapper里面 干什么了呢?
public void addMapper(DataBinderMapper mapper) {
    Class<? extends DataBinderMapper> mapperClass = mapper.getClass();
    //判断有没有添加过
    if (mExistingMappers.add(mapperClass)) {
        //然后把这个mapper放进mMappers遍历里面
        mMappers.add(mapper);
        final List<DataBinderMapper> dependencies = mapper.collectDependencies();
        for(DataBinderMapper dependency : dependencies) {
            addMapper(dependency);
        }
    }
}

  回到最开始的位置sMapper.getDataBinder方法,其中sMapper是DataBinderMapperImpl类型的,但是,它的类里面并没有getDataBinder方法,所以,这里调用的就是DataBinderMapperImpl父类的getDataBinder方法。也就是MergedDataBinderMapper。我们来看一下

public class MergedDataBinderMapper extends DataBinderMapper {
    ...
    @Override
    public ViewDataBinding getDataBinder(DataBindingComponent bindingComponent, View view,
            int layoutId) {
        //这里遍历mMappers获取里面的每一项,然后调用getDataBinder方法。这个list从哪里赋值的呢?这个遍历不眼熟吗?
        for(DataBinderMapper mapper : mMappers) {
            ViewDataBinding result = mapper.getDataBinder(bindingComponent, view, layoutId);
            if (result != null) {
                return result;
            }
        }
        if (loadFeatures()) {
            return getDataBinder(bindingComponent, view, layoutId);
        }
        return null;
    }
    ...
}

  没错,就是这里上面构造方法里面添加的addMapper(new com.haichenyi.hcy.DataBinderMapperImpl()),我们再来看看这个DataBinderMapperImpl,看看它的getDataBinder,因为前面sMapper.getDataBinder调用的就是它

//简单的贴我们需要的
public class DataBinderMapperImpl extends DataBinderMapper {
  private static final int LAYOUT_LAYOUTTEST1 = 1;

  private static final SparseIntArray INTERNAL_LAYOUT_ID_LOOKUP = new SparseIntArray(1);
  //首先是有一个静态的SparseIntArray,不知道是啥,反正,我基本上没用过这个东西,通过他这个存数据的方式来看,类似于map,应该是优化过后的,用在这里性能比较高,不然为啥不用map?
  static {
    //以我们的布局id为key,以他自己定义的对应的int数据为值,来存
    INTERNAL_LAYOUT_ID_LOOKUP.put(com.haichenyi.hcy.R.layout.layout_test1, LAYOUT_LAYOUTTEST1);
  }

  //这里我直接贴出来了一个类型的,就是传view的,那个传view类型数组的,实际上就是循环调用的这个方法。
  //可以看DataBinderMapperImpl的父类MergedDataBinderMapper里面有逻辑
  @Override
  public ViewDataBinding getDataBinder(DataBindingComponent component, View view, int layoutId) {
    //这里,就能够获取到值了,就是全局变量1
    int localizedLayoutId = INTERNAL_LAYOUT_ID_LOOKUP.get(layoutId);
    //1>0,判断就进去了
    if(localizedLayoutId > 0) {
      //到这里,view.getTag,我们逻辑走到这里,我们没有设置tag啊?这里岂不是判断一直都是空?就抛异常了?
      //我们可以打断点看一下,就会发现这里是能拿到值的,以我这个为例,这里获取到的tag是:layout/layout_test1_0,为什么呢?
      final Object tag = view.getTag();
      if(tag == null) {
        throw new RuntimeException("view must have a tag");
      }
      switch(localizedLayoutId) {
        case  LAYOUT_LAYOUTTEST1: {
          if ("layout/layout_test1_0".equals(tag)) {
            return new LayoutTest1BindingImpl(component, view);
          }
          throw new IllegalArgumentException("The tag for layout_test1 is invalid. Received: " + tag);
        }
      }
    }
    return null;
  }
  //省略
  ...
}

  到这里,我们来聊一下,为什么他这里获取到的tag有值呢?项目重新编译的时候,databinding会根据xml的根布局是否是layout布局来重新生成xml。如下图:

databinding生成xml的图.png

  如上图所示,databinding传进来的layout其实就是这个layout,并不是我们自己写的布局。所以,这个位置getTag能获取到值。

<?xml version="1.0" encoding="utf-8"?>
<layout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto">

    <data>

        <variable
            name="ageData"
            type="com.haichenyi.hcy.data.AgeData" />
    </data>

    <androidx.constraintlayout.widget.ConstraintLayout
        android:layout_width="match_parent"
        android:layout_height="match_parent">

        <TextView
            android:id="@+id/tvName"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:layout_marginTop="30dp"
            android:text="@{ageData.name}"
            android:textColor="@color/black"
            app:layout_constraintEnd_toEndOf="parent"
            app:layout_constraintStart_toStartOf="parent"
            app:layout_constraintTop_toTopOf="parent" />

        <TextView
            android:id="@+id/tvAge"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:layout_marginTop="30dp"
            android:text="@{ageData.age}"
            android:textColor="@color/black"
            app:layout_constraintEnd_toEndOf="parent"
            app:layout_constraintStart_toStartOf="parent"
            app:layout_constraintTop_toBottomOf="@id/tvName" />

        <LinearLayout
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:gravity="center_horizontal"
            android:orientation="vertical"
            app:layout_constraintEnd_toEndOf="parent"
            app:layout_constraintStart_toStartOf="parent"
            app:layout_constraintTop_toBottomOf="@id/tvAge">

            <ImageView
                android:id="@+id/img"
                android:layout_width="wrap_content"
                android:layout_height="wrap_content"
                android:src="@mipmap/ic_launcher_round" />

            <ImageView
                android:layout_marginTop="10dp"
                android:layout_width="wrap_content"
                android:layout_height="wrap_content"
                android:src="@mipmap/ic_launcher_round" />

            <TextView
                android:layout_width="wrap_content"
                android:layout_height="wrap_content"
                android:text="@{ageData.name}"/>
        </LinearLayout>
    </androidx.constraintlayout.widget.ConstraintLayout>
</layout>

对比一下我们自己的布局:就会发现规律

  1. 在我们写的根布局会加上tag,名字叫:layout/(你的layout)_0
  2. 在你有用bean类属性(@{ageData.name}之类)的位置,他会给你的view加上tag,名字叫:binding_i,这个i,从1开始,依次+1

  结合总结的这两条规律,对比一下上面的两个。

  我们再返回到上面,switch语句里面,就返回了LayoutTest1BindingImpl类,这个是不是很眼熟?没错,我们调用databinding的时候传递了一个泛型LayoutTest1Binding,他俩什么关系呢?Impl结尾,一看就知道是实现类。我们点到LayoutTest1BindingImpl看一下。

  我们从前面获取到布局的实现类LayoutTest1BindingImpl,上面的流程就走完了,我们还是没有看到怎么获取view的方法,一个类的创建先走的它自己的构造方法,那我们就来看一下这个实现类构造方法里面是什么,或许就有答案了。

public class LayoutTest1BindingImpl extends LayoutTest1Binding  {
    //表示布局里面有没有include标签,我们布局里面没有
    @Nullable
    private static final androidx.databinding.ViewDataBinding.IncludedLayouts sIncludes;
    //这个变量是干什么的呢?
    @Nullable
    private static final android.util.SparseIntArray sViewsWithIds;
    static {
        sIncludes = null;
        sViewsWithIds = new android.util.SparseIntArray();
        //为什么要把这个id叫img的控件这样放呢?为啥其他的不放在这里?
        //这里就可以把答案给出来,我们这个img并没有在xml里面用bean类的属性,所以就单独这样放的。
        //只要是在xml里面用了bean的属性,不管你有没有id,都是通过tag去获取的
        //有个印象就行,下面还会说这个view的获取流程
        sViewsWithIds.put(R.id.img, 4);
    }
    // views
    @NonNull
    private final androidx.constraintlayout.widget.ConstraintLayout mboundView0;
    @NonNull
    private final android.widget.TextView mboundView3;
    // variables
    // values
    // listeners
    // Inverse Binding Event Handlers

    public LayoutTest1BindingImpl(@Nullable androidx.databinding.DataBindingComponent bindingComponent, @NonNull View root) {
        //正常的调用自己的三个参数的构造方法,主要,看一下第三个参数,获取的是Object[],下面贴出来了这个方法
        this(bindingComponent, root, mapBindings(bindingComponent, root, 5, sIncludes, sViewsWithIds));
    }
    private LayoutTest1BindingImpl(androidx.databinding.DataBindingComponent bindingComponent, View root, Object[] bindings) {
        //调用父类的构造方法
        super(bindingComponent, root, 0
            , (android.widget.ImageView) bindings[4]
            , (android.widget.TextView) bindings[2]
            , (android.widget.TextView) bindings[1]
            );
        this.mboundView0 = (androidx.constraintlayout.widget.ConstraintLayout) bindings[0];
        this.mboundView0.setTag(null);
        this.mboundView3 = (android.widget.TextView) bindings[3];
        this.mboundView3.setTag(null);
        this.tvAge.setTag(null);
        this.tvName.setTag(null);
        setRootTag(root);
        // listeners
        invalidateAll();
    }

    @Override
    public void invalidateAll() {
        synchronized(this) {
                mDirtyFlags = 0x2L;
        }
        requestRebind();
    }
    ...
    public void setAgeData(@Nullable com.haichenyi.hcy.data.AgeData AgeData) {
        this.mAgeData = AgeData;
        synchronized(this) {
            mDirtyFlags |= 0x1L;
        }
        notifyPropertyChanged(BR.ageData);
        super.requestRebind();
    }
    ...
}

  重点就在这里了,mapBindings这个方法里面就是获取view的流程,我们,可以来看一下。

protected static Object[] mapBindings(DataBindingComponent bindingComponent, View root,
            int numBindings, IncludedLayouts includes, SparseIntArray viewsWithIds) {
    //这个numbindings的大小是你布局里面控件的个数
    Object[] bindings = new Object[numBindings];
    mapBindings(bindingComponent, root, bindings, includes, viewsWithIds, true);
    return bindings;
}

//来咯来咯,它来咯,本篇的重点
private static void mapBindings(DataBindingComponent bindingComponent, View view,
            Object[] bindings, IncludedLayouts includes, SparseIntArray viewsWithIds,
            boolean isRoot) {
    final int indexInIncludes;
    //这个getBinding方法可以点进去看一下,通过这个view的tag(R.id.dataBinding),来获取ViewDataBinding对象。
    //这个设置tag是在绑定完这个view后面才设置的,可以回过头去看LayoutTest1BindingImpl的构造方法里面,有个setRootTag方法,就是设置tag的位置
    //所以,这里,我们还没有设置,这里获取到的就是空,这里判断就进不去,接着往下面走
    final ViewDataBinding existingBinding = getBinding(view);
    if (existingBinding != null) {
        return;
    }
    //就到了这里,这里这个tag就是前面说过的DataBinding自己生成的xml,添加的tag
    //就是我们前面说的那两点规律,跟布局是layout/xxx_0,控件是bind_i,i是累加的
    Object objTag = view.getTag();
    final String tag = (objTag instanceof String) ? (String) objTag : null;
    boolean isBound = false;
    //判断是不是根布局,并且tag不为空,并且tag是以layout开头。
    //结合我们的布局来看,layout/layout_test1_0满足这个条件
    if (isRoot && tag != null && tag.startsWith("layout")) {
        //最后一个下划线的下标,我们的这里是19
        final int underscoreIndex = tag.lastIndexOf('_');
        //19大于0,后面这个判断是判断下划线的后一位,是否是数字,我们这里是0,也满足
        if (underscoreIndex > 0 && isNumeric(tag, underscoreIndex + 1)) {
            //取出最后一位,我们这里是0
            final int index = parseTagInt(tag, underscoreIndex + 1);
            //这个数组里面0号位置之前没有放过,肯定是空的
            if (bindings[index] == null) {
                //所以,就把0号位置放这个根布局
                bindings[index] = view;
            }
            //includes,当前布局有没有include标签,我们布局没有,一直都是空,这里indexInIncludes就等于-1
            indexInIncludes = includes == null ? -1 : index;
            isBound = true;
        } else {
            indexInIncludes = -1;
        }
        //这个else 是判断当前tag是不是binding打头
    } else if (tag != null && tag.startsWith(BINDING_TAG_PREFIX)) {
        int tagIndex = parseTagInt(tag, BINDING_NUMBER_START);
        //并且取出tag的最后一位,也就是那个数字位,
        //判断这个位置有没有元素,没有元素,就把当前元素放到这个位置
        //也就是我们的bindings[1]=TextView等
        if (bindings[tagIndex] == null) {
            bindings[tagIndex] = view;
        }
        isBound = true;
        indexInIncludes = includes == null ? -1 : tagIndex;
    } else {
        // Not a bound view
        indexInIncludes = -1;
    }
    //isBound这个值,是在前面有tag的时候,判断进去了,才会赋值成true
    //可以理解成,没有自动生成tag的元素(也就是xml里面没有用@{}赋值),前面判断进不去,这里判断会进去。
    if (!isBound) {
        //要判断当前这个view有没有id,没有id就不用管,有id也要赋值
        final int id = view.getId();
        if (id > 0) {
            int index;
            //viewsWithIds.get(id, -1),在viewsWithIds这个数组里面通过id获取它的索引
            //我们再回过头去看一下LayoutTest1BindingImpl的static代码块,它那里提前就存好了对应的下标
            //它这个下标是怎么来的呢?为什么我们这里img对应的是4呢?
            //我个人理解是,它先循环完有tag元素的索引,我们这里到3了,然后,我们布局里面,有id没有设置用@{}赋值的,就依次累加,所以,我们这里img对应的索引是4
            if (viewsWithIds != null && (index = viewsWithIds.get(id, -1)) >= 0 &&
                    bindings[index] == null) {
                bindings[index] = view;
            }
        }
    }
    //判断当前这个元素是不是ViewGroup,如果是ViewGroup,它里面可能还有子元素
    if (view instanceof  ViewGroup) {
        final ViewGroup viewGroup = (ViewGroup) view;
        final int count = viewGroup.getChildCount();
        int minInclude = 0;
        //循环它的子元素,去获取
        for (int i = 0; i < count; i++) {
            final View child = viewGroup.getChildAt(i);
            boolean isInclude = false;
            //这里是表示我们的布局里面有没有嵌套include标签,如果DataBinding的布局可能存在嵌套的情况,先循环里层的,再循环外层的
            if (indexInIncludes >= 0 && child.getTag() instanceof String) {
                //不是本篇的重点就不说这里面了
                ...
            }
            //上面循环玩之后,就走到这里,绑定下一个子view
            if (!isInclude) {
                mapBindings(bindingComponent, child, bindings, includes, viewsWithIds, false);
            }
        }
    }
}

  经过上面这个方法的调用,就把布局里面定义了id的,生成了tag的view放进了binding里面,Object类型的数组返回回去。如下图所示:

databinding绑定后的数组.png

  从上面循环也看出来了,优先通过tag找view,然后通过findViewById找view,两个都没有,就被丢弃了。所以,这里只有5个。我们布局一共7个view;LinearLayout和其中一个ImageView没有id,没有tag。循环的时候就没有找到。

  再强调一遍DataBinding生成tag,是因为你再xml里面通过它规定的方式设置了值,即使,你没有给view设置id,这里也会绑定上,因为,它优先通过tag绑定view的。

  流程走到这里,DataBinding已经把需要的view都找到了,并且放到了Object数组里面,接下来就是绑定引用了。我们再回过头去看重载的构造方法的代码:

public LayoutTest1BindingImpl(@Nullable androidx.databinding.DataBindingComponent bindingComponent, @NonNull View root) {
    this(bindingComponent, root, mapBindings(bindingComponent, root, 5, sIncludes, sViewsWithIds));
}
//上面的mapBindings绑定好了view,传递到第三个参数,Object[] bindings。
private LayoutTest1BindingImpl(androidx.databinding.DataBindingComponent bindingComponent, View root, Object[] bindings) {
    //super调用父类的方法,我们发现代码点不过去,我们就看类上面的继承
    //CTRL+鼠标左键会直接指向xml,我们全局搜一下LayoutTest1Binding类,下面有图
    super(bindingComponent, root, 0
        //看这里获取view,强转,都是通过bindings前面绑定的
        , (android.widget.ImageView) bindings[4]
        , (android.widget.TextView) bindings[2]
        , (android.widget.TextView) bindings[1]
        );
    this.mboundView0 = (androidx.constraintlayout.widget.ConstraintLayout) bindings[0];
    this.mboundView0.setTag(null);
    this.mboundView3 = (android.widget.TextView) bindings[3];
    this.mboundView3.setTag(null);
    this.tvAge.setTag(null);
    this.tvName.setTag(null);
    setRootTag(root);
    // listeners
    invalidateAll();
}

  就是这个类,点进去看

LayoutTest1Binding.png
public abstract class LayoutTest1Binding extends ViewDataBinding {
  @NonNull
  public final ImageView img;

  @NonNull
  public final TextView tvAge;

  @NonNull
  public final TextView tvName;

  @Bindable
  protected AgeData mAgeData;

  protected LayoutTest1Binding(Object _bindingComponent, View _root, int _localFieldCount,
      ImageView img, TextView tvAge, TextView tvName) {
    super(_bindingComponent, _root, _localFieldCount);
    this.img = img;
    this.tvAge = tvAge;
    this.tvName = tvName;
  }

  public abstract void setAgeData(@Nullable AgeData ageData);

  @Nullable
  public AgeData getAgeData() {
    return mAgeData;
  }
  ...
}

  这里只贴出来了几个我们想要知道的东西,我们从前面通过tag,id把view找到之后,传递到构造方法里面,然后,这里创建的全局的变量,分别赋值给这些变量。这样,就拿到了这些引用了。

  一直到这里,获取view的引用,绑定流程就走完了。再回过头去看最开始的问题binding.tvName,binding.tvAge等等,就是拿的这里的引用,也可以看到我们最开始拿到的binding这个变量的类型就是ViewDataBinding,我们这里的LayoutTest1Binding继承的就是ViewDataBinding,就全部串起来了。

<span id = "c5"></span>

5.更新(界面)流程

  可以先说结论,用的就是观察者模式。绑定view的后面它他先注册监听,我们设置数据的时候,触发回调,更新界面。

文章有点长了,更新流程放到下一章吧。这个view的绑定东西还是比较长的。最后总结一下绑定view的流程图

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

推荐阅读更多精彩内容