Android DataBinding入门

Android DataBinding

Data Binding Library 从 2015 Google I/O 上发布到至今,已经有一年多的长足发展,目前在 Android Studio2.2 版本上已经拥有比较成熟的使用体验。可以说 Data Binding 已经是一个可用度较高,也能带来实际生产力提升的技术了。

编译环境

2.0 版本以后的 Android Studio 已经内置支持了 DataBinding ,我们只要在 gradle 文件中添加如下代码就可以使用 Databinding:

android {
    ....
    dataBinding {
        enabled = true
    }
}

xml 文件的处理

<layout>
    <data class = "CustomBinding">
    </data>
    // 原来的layout
</layout>

layout标签位于布局文件最外层,可以使原来的普通布局文件转化为 databinding layout ,同时会在build/ganerated/source/apt下相关目录下生成 ***Binding 类

默认生成规则:xml通过文件名生成,使用下划线分割大小写,即 activity_main.xml 会生成对应的 ActivityMainBinding

data标签用于申明 xml 文件中的变量用于绑定 View,可以通过对标签的修饰来指定生成 Binding 类的自定义名称,如上述的布局文件最终会生成一个 CustomBinding 类

Java 代码的处理
需要用 DataBindingUtil 类中的相关方法取代原先的 setContentView 及 inflate 获得 ***Binding 实例类

取代findViewById方法

findViewById(int id) 方法是将 View 的实例与 xml 布局文件中的 View 对应赋值的过程,需要遍历所有的 childrenView 查找。更关键的一点是如果比较复杂的页面,可能会存在数十个控件,光写 findViewById 也会让人挺崩溃的。虽说有着诸如 ButterKnife 这样优秀的第三方库,但使用数据绑定方式无疑更简洁明

private TextView mFirstNameTv;
private TextView mLastNameTv;

@Override
protected void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    setContentView(this, R.layout.activity_first);
    mFirstNameTv = (TextView) findViewById(R.id.tv_first_name);
    mLastNameTv = (TextView) findViewById(R.id.tv_last_name);
}

//********* 或者使用 *********

private ActivityFirstBinding mFirstBinding;

@Override
protected void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    // 在mBinding中有布局文件中带id的View变量
    mFirstBinding = DataBindingUtil.setContentView(this, R.layout.activity_first);
}

采用 DateBinding 后,所有的 View 会在 Binding 实例类生成对应的实例,而有 id 的 View 则会使用 public 进行修饰,而变量名的生成规则是通过下划线分割大小写,即 id = "@+id/main_view" 会生成对应的 mainView 的变量,我们可以直接通过 binding.mainView 获取,直接节省了在 activity 中声明一长串变量的步骤,也不需要再写 findViewById 方法或者加上 @BindView 的注解

<layout
    xmlns:android="http://schemas.android.com/apk/res/android">
    
    <LinearLayout
        android:orientation="vertical"
        android:layout_width="match_parent"
        android:layout_height="match_parent">
        
        <TextView
            android:id="@+id/tv_first_name"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"/>
            
        <TextView
            android:id="@+id/tv_last_name"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"/>  
    </LinearLayout>
</layout>

在 activity_first.xml 布局文件中添加 databindind 的 layout 标签后会生成 ActivityFirstBinding 类

// views
private final android.widget.LinearLayout mboundView0;
public final android.widget.TextView tvFirstName;
public final android.widget.TextView tvLastName;

带 id 的 view 最终会生成 public final 修饰的字段,而不带 id 的 view 也会生成 private final 修饰的字段。而这些则是在 ActivityLoginBinding 的构造函数中赋值的,仅仅只需要遍历一遍整个的 view 树,而不是多个 findViewById 方法遍历多次

为布局文件绑定Variable

数据绑定getter和setter

Variable 是 DataBinding 中的变量,可以在data标签中添加variable标签从而在 xml 中引入数据

<layout>
    <data>
        <variable
            name="user"
            type="com.sanousun.sh.databinding.bean.User"/>
    </data>
    
    <LinearLayout
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:orientation="vertical">

        <TextView
            android:id="@+id/tv_first_name"
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:text="@{user.firstName}"/>

        <TextView
            android:id="@+id/tv_last_name"
            android:layout_width="wrap_content"
            android:layout_height="match_parent"
            android:text="@{user.lastName}"/>

    </LinearLayout>
    
</layout>

variable 就是普通的 POJO 类,实现 getter 方法,并没有提供更新数据刷新 UI 的功能

private static class User {

    private String firstName;
    private String lastName;
   
    public User(String firstName, String lastName){
        this.firstName = firstName;
        this.lastName = lastName;
    }
   
    public String getFirstName() {
        return this.firstName;  
    }
    
    public String getLastName() {
        return this.lastName;   
    }
}

如果希望数据变更后 UI 会即时刷新,就需要继承 Observable 类

private static class User extends BaseObservable {

   private String firstName;
   private String lastName;
   
   @Bindable
   public String getFirstName() {
       return this.firstName;
   }
   
   @Bindable
   public String getLastName() {
       return this.lastName;
   }
   
   public void setFirstName(String firstName) {
       this.firstName = firstName;
       notifyPropertyChanged(BR.firstName);
   }
   
   public void setLastName(String lastName) {
       this.lastName = lastName;
       notifyPropertyChanged(BR.lastName);
   }
}

BaseObservable 提供了 notifyChange 和 notifyPropertyChanged 两个方法来刷新 UI ,前者刷新所有的值,而后者则是刷新 BR 类中有标记的属性,而 BR 类中的标记生成需要用Bindable的标签修饰对应的 getter 方法
同时 databinding 提供了 Observable** 开头的一系列基础类可以避免继承 BaseObservable

private static class User {
    public final ObservableField<String> firstName =
        new ObservableField<>();
    public final ObservableField<String> lastName =
        new ObservableField<>();
   public final ObservableInt age = new ObservableInt();
}

本质上 Observable** 也是通过继承 BaseObservable 实现的,调用set方法时会调用 BaseObservable 的 notifyChange 方法

user.firstName.set("first");
String lastName = user.lastName.get();

//********************************

public void set(T value) {
    if (value != mValue) {
        mValue = value;
        notifyChange();
    }
}

运算表达式

运算符
支持绝大部分的 Java 写法,允许变量数据访问、方法调用、参数传递、比较、通过索引访问数组,甚至还支持三目运算表达式

  • 算术 + - * / %
  • 字符串合并 +
  • 逻辑 && ||
  • 二元 & | ^
  • 一元 + - ! ~
  • 移位 >> >>> <<
  • 比较 == > < >= <=
  • instanceof
  • Grouping ()
  • 文字 - character, String, numeric, null
  • Cast
  • 方法调用
  • Field 访问
  • Array 访问 []
  • 三目运算符 ?:

尚且不支持 this,super,new 以及显式的泛型调用

空指针处理
无需判断对象是否为 null,DataBinding 会自动检查是否为 null,如果引用对象为 null,那么所有属性引用的都是 null 的,你无需判断也不会导致崩溃

空合并运算符 ??
引用的对象为 null,需要做额外的判断,DataBinding 提供了空合并运算

android:text="@{user.firstName ?? user.lastName}"
//会取第一个非空值作为结果,相当于
android:text="@{user.firstName != null ? user.firstName : user.lastName}"

集合数组的调用
对于数组,List,Map,SparseArray的访问,我们可以直接通过[]的数组下标来访问,值得注意的是数组越界的问题

资源文件的引用
值得一说的是可以直接组合字符串

android:text="@{@string/nameFormat(firstName, lastName)}"

<string name="nameFormat">%s, %s</string>

也可以对数值类应用直接进行运算

android:marginLeft="@{@dimen/margin + @dimen/avatar_size}"

需要注意的是一些资源文件需要确切的名称

Type Normal Reference Expression Reference
String[] @array @stringArray
int[] @array @intArray
TypedArray @array @typedArray
Animator @animator @animator
StateListAnimator @animator @stateListAnimator
color int @color @color
ColorStateList @color @colorStateList

属性关联
DataBinding 库通过解析 View 的 setter 方法来完成赋值过程,android:text = "@user.firstName"就相关于调用了
TextView 的 tv.setText(user.firstName)

甚至可以调用 View 未提供的布局属性,只要 View 提供了对应的 setter 方法。
举个例子:

<android.support.v4.widget.DrawerLayout
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"
    app:scrimColor="@{@color/scrim}"/>

DrawerLayout 有个 setScrimColor(int color)方法,所以可以在布局中使用未定义的app:scrimColor属性,通过 app 命名空间修饰的属性会自动关联到对应的方法

属性扩展

BindingMethods 和 BindingAdapter 注解
但是部分 View 的布局属性并没有完整对应的方法提供,比如说 ImageView 的"android:tint"布局属性的对应方法是setImageTintList(@Nullable ColorStateList tint),这时就需要使用 DataBinding 提供的处理方法,使用BindingMethods注解

@BindingMethods({
    @BindingMethod(type = android.widget.ImageView.class, attribute = "android:tint", method = "setImageTintList"),
    @BindingMethod(type = android.widget.ImageView.class, attribute = "android:tintMode", method = "setImageTintMode"),
})
public class ImageViewBindingAdapter {
    @BindingAdapter("android:src")
    public static void setImageUri(ImageView view, String imageUri) {
        if (imageUri == null) {
            view.setImageURI(null);
        } else {
            view.setImageURI(Uri.parse(imageUri));
        }
    }

    @BindingAdapter("android:src")
    public static void setImageUri(ImageView view, Uri imageUri) {
        view.setImageURI(imageUri);
    }

    @BindingAdapter("android:src")
    public static void setImageDrawable(ImageView view, Drawable drawable) {
        view.setImageDrawable(drawable);
    }
}

这是系统提供的 ImageViewBindingAdapter,可以在引入了 DataBinding 后全局搜索查看详情,通过BindingMethod注解将两者关联起来,但是如果 View 甚至没有实现对应方法或者需要绑定自定义方法,这是可以使用BindingAdapter注解

BindingConversion 注解
有时在 xml 中绑定的属性,未必是最后的set方法需要的,比如想用color(int),但是 view 需要 Drawable,比如我们想用String,而 view 需要的是 Url 。这时候就可以使用BindingConversion注解

<View
    android:background=“@{isError ? @color/red : @color/white}”
    android:layout_width=“wrap_content”
    android:layout_height=“wrap_content”/>
@BindingConversion
public static ColorDrawable convertColorToDrawable(int color) {
    return new ColorDrawable(color);
}

链式表达式

<ImageView android:visibility=“@{user.isAdult ? View.VISIBLE : View.GONE}”/>
<TextView android:visibility=“@{user.isAdult ? View.VISIBLE : View.GONE}”/>
<CheckBox android:visibility="@{user.isAdult ? View.VISIBLE : View.GONE}"/>

代码可以优化成

<ImageView android:id=“@+id/avatar”
           android:visibility=“@{user.isAdult ? View.VISIBLE : View.GONE}”/>
<TextView android:visibility=“@{avatar.visibility}”/>
<CheckBox android:visibility="@{avatar.visibility}"/>

在系统生成的 Bindinng 类中,会被解析成这三个控件可见性都跟随着 user.isAdult 的状态而改变

使用Callback

事件绑定

DataBinding 不仅可以在布局文件中为控件绑定数值,也可以在布局文件中为控件绑定监听事件

  • android:onClick
  • android:onLongClick
  • android:onTouch
  • ......

通常会在java代码中定义一个名为Handler或者Presenter的类,然后set进来

<layout
    xmlns:android="http://schemas.android.com/apk/res/android">

    <data>
        <import type="android.view.View"/>

        <variable
            name="user"
            type="com.sanousun.sh.databinding.bean.User"/>

        <variable
            name="presenter"
            type="com.sanousun.sh.databinding.activity.SecondActivity.Presenter"/>

    </data>

    <LinearLayout
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:orientation="vertical">

        <TextView
            android:id="@+id/tv_mobile"
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:onClick="@{presenter::mobileClick}"
            android:text="@{user.firstName}"/>

        <TextView
            android:id="@+id/tv_pwd"
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:onClick="@{()->presenter.pwdClick()}"
            android:text="@{user.lastName}"/>

    </LinearLayout>

</layout>

在java代码中:

public class Presenter {
    public void mobileClick(View view) {
        Toast.makeText(SecondActivity.this, "mobile click", Toast.LENGTH_LONG).show();
    }

    public void pwdClick() {
        Toast.makeText(SecondActivity.this, "pwd click", Toast.LENGTH_LONG).show();
    }
}

@Override
protected void onCreate(@Nullable Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    mSecondBinding = DataBindingUtil.setContentView(this, R.layout.activity_second);
    mSecondBinding.setUser(new User("da", "shu"));
    mSecondBinding.setPresenter(new Presenter());
}

事件绑定使用 lambda 表达式,绑定形式主要是有两种形式:

Method References

需要方法参数及返回值与对应的 listener 一致,在编译时生成对应的 listenerImpl 并在放置 presenter 时为对应控件添加监听,如上面的 mobileClick

// Listener Stub Implementations
public static class OnClickListenerImpl implements android.view.View.OnClickListener{
     private com.sanousun.sh.databinding.activity.SecondActivity.Presenter value;
    public OnClickListenerImpl setValue(com.sanousun.sh.databinding.activity.SecondActivity.Presenter value) {
        this.value = value;
        return value == null ? null : this;
    }
    
    @Override
    public void onClick(android.view.View arg0) {
        this.value.mobileClick(arg0);
    }
}

代码中会做 presenter 的空判断

Listener Bindings

无需匹配对应 listener 的参数,只需要保证返回值的一致即可(除非是void)。与 Method References 的最大的不同点在于
它是在点击事件发生时相应的

// callback impls
public final void _internalCallbackOnClick(int sourceId , android.view.View callbackArg_0) {
    // localize variables for thread safety
    // presenter
    com.sanousun.sh.databinding.activity.SecondActivity.Presenter presenter = mPresenter;
    // presenter != null
    boolean presenterObjectnull = false;

    presenterObjectnull = (presenter) != (null);
    if (presenterObjectnull) {
        presenter.pwdClick();
    }
}

这个方法会在页面有点击时间时调用,同样也会做空判断

当然你也可以通过@BindingMethods@BindingAdapter进行自定义的扩展

双向绑定

有别于单向绑定使用的@{}符号,双向绑定使用@={}符号用于区别,目前支持的属性有 text,checked,year,month,day,hour,rating,progress 等

InverseBindingListener

实现双向绑定需要归功于 DataBinding 库中的 InverseBindingListener 接口,这个监听器的作用是监听目标控件的属性改变

private android.databinding.InverseBindingListener mboundView1androidCh = new android.databinding.InverseBindingListener() {
    @Override
    public void onChange() {
        // Inverse of user.male
        //         is user.setMale((boolean) callbackArg_0)
        boolean callbackArg_0 = mboundView1.isChecked();
        // localize variables for thread safety
        // user.male
        boolean maleUser = false;
        // user
        com.sanousun.sh.databinding.bean.User user = mUser;
        // user != null
        boolean userObjectnull = false;
        userObjectnull = (user) != (null);
        if (userObjectnull) {
            user.setMale((boolean) (callbackArg_0));
        }
    }
};

对应 DataBinding 类中有根据双向绑定生成的 Inverse Binding Event Handlers

@Override
protected void executeBindings() {
    ......
    android.databinding.adapters.CompoundButtonBindingAdapter.setListeners(this.mboundView1, (android.widget.CompoundButton.OnCheckedChangeListener)null, mboundView1androidCh);
}

在绑定时,设置到对应的控件中,当监听控件属性改变时,就会触发重绑定,更新属性值

InverseBindingMethods 和 InverseBindingAdapter 注解

如果你想做自定义的双向绑定,你必须充分理解这几个注解的含义。

@Target({ElementType.ANNOTATION_TYPE})
public @interface InverseBindingMethod {
    Class type();
    String attribute();
    String event() default ""; // 默认会根据attribute name获取get
    String method() default "";// 默认根据attribute增加AttrChanged
}

以系统定义的 CompoundButtonBindingAdapter 为例

@BindingMethods({
    @BindingMethod(type = CompoundButton.class, attribute = "android:buttonTint", method = "setButtonTintList"),
    @BindingMethod(type = CompoundButton.class, attribute = "android:onCheckedChanged", method = "setOnCheckedChangeListener"),
})
@InverseBindingMethods({
    @InverseBindingMethod(type = CompoundButton.class, attribute = "android:checked"),
})
public class CompoundButtonBindingAdapter {

    @BindingAdapter("android:checked")
    public static void setChecked(CompoundButton view, boolean checked) {
        if (view.isChecked() != checked) {
            view.setChecked(checked);
        }
    }

    @BindingAdapter(value = {"android:onCheckedChanged", "android:checkedAttrChanged"},
            requireAll = false)
    public static void setListeners(CompoundButton view, final OnCheckedChangeListener listener,
            final InverseBindingListener attrChange) {
        if (attrChange == null) {
            view.setOnCheckedChangeListener(listener);
        } else {
            view.setOnCheckedChangeListener(new OnCheckedChangeListener() {
                @Override
                public void onCheckedChanged(CompoundButton buttonView, boolean isChecked) {
                    if (listener != null) {
                        listener.onCheckedChanged(buttonView, isChecked);
                    }
                    attrChange.onChange();
                }
            });
        }
    }
}

双向绑定需要为属性绑定一个监听器,这里就是需要为"android:checked"属性绑定监听器,通过 @InverseBindingMethod(type = CompoundButton.class, attribute = "android:checked"),databinding 可以通过 checkedAttrChanged 找到 OnCheckedChangeListener,设置 OnCheckedChangeListener 来通知系统生成的 InverseBindingListener 调用 onChange 方法,从而通过 getter 方法来获取值。值得注意的是为了防止无限循环调用,setter 方法必须要去进行重判断

同样如果没有对应方法,可以自定义 InverseBindingAdapter 来实现,详情见系统TextViewBindingAdapter

隐式调用

实现了双向绑定的属性就可以隐式调用,而不用写繁琐的 listener

<CheckBox android:id="@+id/cb"/>
<ImageView android:visibility="@{cb.checked ? View.VISIBLE : View.GONE}"/>

属性改变监听

当然我们可以通过 Observable.OnPropertyChangedCallback 来监听属性的改变,从而实现具体的业务逻辑

user.addOnPropertyChangedCallback(new Observable.OnPropertyChangedCallback() {
    @Override
    public void onPropertyChanged(Observable observable, int i) {
        if (i== BR.firstName){
            Toast.makeText(ThirdActivity.this, user.getFirstName(), Toast.LENGTH_LONG).show();
        }
    }
});

RecyclerView的处理

只要简单的定义 ViewHolder

public class BindingViewHolder<T extends ViewDataBinding> extends RecyclerView.ViewHolder {

    protected final T mBinding;

    public BindingViewHolder(T binding) {
        super(binding.getRoot());
        mBinding = binding;
    }

    public T getBinding() {
        return mBinding;
    }
}

因为逻辑和属性的绑定在xml中就已经处理好,adapter 的创建变得十分的容易,一般情况下可以直接使用,如果需要额外的更改可以继承。而点击事件的监听可以在 onBindViewHolder 中设置
对于含有多种 viewType 的列表适配器,在不同 xml 布局文件中 variable 的 name 可以全部写为 item,那么在绑定数据时
无需特殊处理

@Override
public void onBindViewHolder(BindingViewHolder holder, int position) {
    final Data data = mData.get(position);
    holder.getBinding().setVariable(item, data);
    holder.getBinding().executePendingBindings();
}

在生成的代码中会去检查它的类型,并将其赋值

高级用法

component 注入

Data Binding Component详解 - 换肤什么的只是它的一个小应用!

原理简述

解析

编译时,系统会将 xml 文件拆分为两部分,数据部分的 xml 和布局部分的 xml,分别存放于app/build/intermediates/data-binding-infoapp/build/intermediates/data-binding-layout-out之中,数据部分的 xml 文件记录 view 对应的赋值表达式,而布局部分的 xml 则是普通的布局如下

<Button
            android:id="@+id/btn_btn"
            android:layout_width="match_parent"
            android:layout_height="56dp"
            android:tag="binding_1"/>

特殊在于每个控件都会生成 tag,作用是生成 DataBinding 时可以绑定对应控件,因此在布局文件中需要避免书写tag
解析xml -> 解析表达式 -> java编译 —> 解析依赖 -> setter

public ActivityMainBinding(android.databinding.DataBindingComponent bindingComponent, View root) {
    super(bindingComponent, root, 1);
    final Object[] bindings = mapBindings(bindingComponent, root, 4, sIncludes, sViewsWithIds);
    this.activityMain = (android.widget.LinearLayout) bindings[0];
    this.activityMain.setTag(null);
    this.btnBtn = (android.widget.Button) bindings[1];
    this.btnBtn.setTag(null);
    setRootTag(root);
    // listeners
    invalidateAll();
}

在生成的 binding 类中,构造函数会为所有的控件赋值,此时会将 tag 值去除,所以说为 View 的赋值需要在获取 DataBinding 实例之后。初始化时遍历 view 赋值比 findViewById 效率高得多

绑定

绑定的代码都在生成的 DataBinding 类中的 executeBindings 方法中,不管任何涉及到更新 ui 的地方最终都会调用这个方法

@Override
protected void executeBindings() {
    long dirtyFlags = 0;
    synchronized(this) {
        dirtyFlags = mDirtyFlags;
        mDirtyFlags = 0;
    }
    //一些变量的定义
    ......

    if ((dirtyFlags & 0x5L) != 0) {
        //根据flag的值判断是否需要做相应的改变
        ......
    }
    ......
}

databinding 使用位标记来检验更新(dirtyFlags),每一个标志位都有自己的含义,生成的规则由内部解析表达式后确定,在ViewDataBinding 中我们可以看到

if (USE_CHOREOGRAPHER) {
    mChoreographer = Choreographer.getInstance();
    mFrameCallback = new Choreographer.FrameCallback() {
        @Override
        public void doFrame(long frameTimeNanos) {
            mRebindRunnable.run();
        }
    };
} else {
    mFrameCallback = null;
    mUIThreadHandler = new Handler(Looper.myLooper());
}

批量刷新会发生在系统的帧布局刷新时,系统帧布局刷新回调 -> mRebindRunnable -> executePendingBindings -> executeBindings,此时才会触发数据更改的操作

更新

刷新布局最终都会调用 executeBindings 方法,而在父类 ViewDataBinding 类是由 executePendingBindings 调用方法,我们可以直接调用此方法来加载挂起的属性变更,而不用等待下一次的帧布局刷新
而所有的 Variable 内部属性的改变则会注册监听器,监听改变 -> handleFieldChange -> requestRebind -> executePendingBindings -> executeBindings 最终改变属性

参考

从零开始的Android新项目7 - Data Binding入门篇
棉花糖给 Android 带来的 Data Bindings(数据绑定库)

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

推荐阅读更多精彩内容