Android JetPack DataBinding原理分析

一.简介

       DataBinding是谷歌发布的一个实现数据和UI绑定的框架,从字面意思来看即为数据绑定,是 MVVM 模式在Android上的一种实现,用于降低布局和逻辑的耦合性,使代码逻辑更加清晰。
       相对于 MVP,MVVM将Presenter层替换成了ViewModel层,关于MVC、MVP、MVVM架构比较,可以参考文章Android MVC、MVP、MVVM比较分析
       DataBinding 能够省去我们一直以来的 findViewById() 步骤,大量减少 Activity 内的代码,数据能够单向或双向绑定到 layout 文件中。
       接下来本文通过一个例子来学习一下DataBinding的用法及原理分析。

二.实例分析

a.加入dataBinding支持

       使用DataBinding框架需要在对应Model的build.gradle文件Android{}内加入以下代码,同步后就能引入对 DataBinding的支持:

dataBinding {
    enabled = true
}

       AndroidStudio高版本引入方式如下:

buildFeatures {
    dataBinding = true
}
b.xml布局文件转化

       将常规的布局文件转化为对应的数据绑定布局文件,在布局文件的根布局上输入Alt+Enter后,选择Convert to data binding layout会将常规布局转换为以下布局文件形式:

<?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>
        //布局文件用到了View相关的方法,需要引入
        <import type="android.view.View" />
        <variable
            //任意取名字,来作为type对应的引用,可以通过guide代替类取可变变量
            //在view中通过setGuide()建立引用关系
            name="guide"
            //定义可变变量的类
            type="com.hly.learn.ViewModel" />

    </data>

    <RelativeLayout
        android:layout_width="match_parent"
        android:layout_height="match_parent">

        <TextView
            android:id="@+id/name"
            android:layout_width="100dp"
            android:layout_height="50dp"
            android:layout_centerHorizontal="true"
            android:layout_marginTop="10dp"
            android:gravity="center"
            android:textColor="@android:color/holo_red_light"
            android:text="@{guide.rightImageName}" />

        <ImageView
            android:layout_width="200dp"
            android:layout_height="160dp"
            android:layout_below="@+id/name"
            android:layout_centerHorizontal="true"
            android:layout_marginTop="10dp"
            android:scaleType="fitXY"
            android:src="@{guide.rightImageRes}" />

        <TextView
            android:layout_width="300dp"
            android:layout_height="wrap_content"
            android:layout_below="@+id/name"
            android:layout_centerHorizontal="true"
            android:layout_marginTop="200dp"
            android:textColor="@color/colorAccent"
            android:text="@{guide.rightImageDescription}" />

        <TextView
            android:layout_width="100dp"
            android:layout_height="50dp"
            android:layout_below="@+id/name"
            android:layout_marginLeft="50dp"
            android:layout_marginTop="350dp"
            android:gravity="center"
            android:onClick="@{guide::upStep}"
            android:text="@string/up"
            android:textStyle="bold"
            android:textColor="@color/cardview_shadow_start_color"
            android:visibility="@{guide.step!=1?View.VISIBLE:View.INVISIBLE}" />

        <TextView
            android:layout_width="100dp"
            android:layout_height="50dp"
            android:layout_below="@+id/name"
            android:layout_marginLeft="230dp"
            android:layout_marginTop="350dp"
            android:gravity="center"
            android:onClick="@{guide::nextStep}"
            android:text="@string/next"
            android:textStyle="bold"
            android:visibility="@{guide.step!=3?View.VISIBLE:View.INVISIBLE}" />
    </RelativeLayout>
</layout>

      通过以上转化,已经将常规布局转化为数据绑定布局文件,那么下面就一步一步的来讲解实现。

c.View逻辑实现

      将之前常规的UI操作从view里面移到数据绑定布局xml文件中,可以大大减少view的代码量及相关的UI操作逻辑。
      使用前,动态设置ImageView的显示,在view里面的操作如下:

ImageView mImg = findViewById(R.id.img);
mImg.setImageResource(R.drawable.rse);

      使用databinding后,可以在布局文件中将布局变量赋值以@{}形式给ImageView的src属性。当rightImageRes变化时,ImageView会动态加载对应的资源文件。

<ImageView
     android:layout_width="200dp"
     android:layout_height="160dp"
     android:layout_below="@+id/name"
     android:layout_centerHorizontal="true"
     android:layout_marginTop="10dp"
     android:scaleType="fitXY"
     android:src="@{guide.rightImageRes}" />

      使用数据绑定布局文件后,view对应的类变为:

package com.hly.learn.fragments;

import android.os.Bundle;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;

import com.hly.learn.ViewModel;
import com.hly.learn.databinding.KeyboardLayoutBinding;

import com.hly.learn.R;

import androidx.databinding.DataBindingUtil;

public class KeyboardFragment extends BaseFragment {

    private ViewModel mViewModel;

    @Override
    public int getLayoutId() {
        return R.layout.keyboard_layout;
    }

    @Override
    public void initData(View view) {

    }

    @Override
    public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
        //-------------------分析1------------------------------------
        KeyboardLayoutBinding binding = DataBindingUtil.inflate(inflater, R.layout.keyboard_layout, container, false);
        //获取布局对应的view
        //--------------------分析2-----------------------------------
        View view = binding.getRoot();
        //-------------------分析3----------------------------------
        mViewModel = new ViewModel(mContext);
        binding.setGuide(mViewModel);
        //-------------------分析4----------------------------------
        view.viewpager.setAdapter(xx);
        return view;
    }
}

      通过以上可以发现,主要的分析点如下:
      分析1:通过DataBindingUtil来替代inflater来解析数据绑定布局文件,KeyboardLayoutBinding是在编译时根据keyboard_layout生成的(后面会有详细分析);如果为Activity,通过setContentView()方法来解析生成KeyboardLayoutBinding;
      分析2:通过getRoot()获取布局文件对应的View;
      分析3:创建ViewModel,并通过setGuide()(名字不是固定的)将View与ViewModel里面的变量建立关联;
      分析4:直接通过view.idName来获取对应的view,然后进行操作;
      使用了数据绑定布局后,view里面除了加载布局后,没有任何相关的findViewById操作,确实减少了不少UI处理逻辑。

d.VM实现

      数据绑定框架提供了可观察的变量,像ObservableInt,ObservableBoolean来替代原始数据类型int,boolean,使其具备可观察能力。提供了ObservableField来替代引用数据类型,使其具备可观察能力。具体的代码实现如下:

package com.hly.learn;

import android.content.Context;
import android.graphics.drawable.Drawable;
import android.view.View;

import androidx.databinding.ObservableField;
import androidx.databinding.ObservableInt;

public class ViewModel {

    private Context mContext;
    public ViewModel(Context context) {
        step.set(1);
        mContext = context;
        updateImageInfo(step.get());
    }
    //定义用到的可变变量类型
    public ObservableInt step = new ObservableInt();
    public ObservableField<Drawable> rightImageRes = new ObservableField<>();
    public ObservableField<String> rightImageName = new ObservableField<>();
    public ObservableField<String> rightImageDescription = new ObservableField<>();

    public void nextStep(View view) {
        step.set(step.get() + 1);
        updateImageInfo(step.get());
    }

    public void upStep(View view) {
        step.set(step.get() - 1);
        updateImageInfo(step.get());
    }
    //更新ObservableField对应的变量值
    private void updateImageInfo(int step) {
        rightImageRes.set(ModalData.getDrawable(mContext, step));
        rightImageName.set(ModalData.getImageName(mContext, step));
        rightImageDescription.set(ModalData.getImageDes(mContext, step));
    }
}

      DataBinding框架用在MVVM模式下,View是加载布局,ViewModel来处理布局对应的交互,Model是来加载数据。ViewModel从Model里面去数据,供UI显示。
      最后加入Model模块:

e.Model实现
package com.hly.learn;

import android.content.Context;
import android.graphics.drawable.Drawable;

public class ModalData {

    public static Drawable getDrawable(Context context, int index) {
        if (index == 1) {
            return context.getResources().getDrawable(R.drawable.ic_chuancai);
        } else if (index == 2) {
            return context.getResources().getDrawable(R.drawable.ic_lucai);
        } else {
            return context.getResources().getDrawable(R.drawable.ic_xiangcai);
        }
    }

    public static String getImageName(Context context, int index) {
        if (index == 1) {
            return context.getResources().getString(R.string.chuancai);
        } else if (index == 2) {
            return context.getResources().getString(R.string.lucai);
        } else {
            return context.getResources().getString(R.string.xiangcai);
        }
    }

    public static String getImageDes(Context context, int index) {
        if (index == 1) {
            return context.getResources().getString(R.string.chuancaides);
        } else if (index == 2) {
            return context.getResources().getString(R.string.lucaides);
        } else {
            return context.getResources().getString(R.string.xiangcaides);
        }
    }
}
f.事件绑定

      事件绑定也是一种变量绑定,只不过设置的变量是回调接口而已。
      点击TextView响应,在view里面的操作如下:

TextView tv = view.findViewById(R.id.tv);
tv.setOnClickListener(new View.OnClickListener() {
      @Override
      public void onClick(View v) {
          upStep();
      }
});

      使用databinding后,可以在布局文件中将TextView的onClick属性加入执行方法即可,当TextView点击后,会执行相应的方法。

<TextView
    android:layout_width="100dp"
    android:layout_height="50dp"
    android:onClick="@{guide::upStep}"
    android:text="@string/up"
    android:visibility="@{guide.step!=1?View.VISIBLE:View.INVISIBLE}"

三.DataBinding原理分析

      DataBinding实现了数据与UI绑定,那是如何绑定的呢?数据变化后是如何对应UI进行更新的呢?一起看一下实现过程。
      使用AndroidStudio进行开发时,对应用编译后,会在对应目录下生成额外的文件:
      在build/generated/ap_generated_sources/debug/out目录下会生成对应的文件:DataBinderMapperImpl.java(两个),KeyboardLayoutBindingImpl.java文件;
      在build/generated/data_binding_base_class_source_out/debug/out目录下会生成对应的文件:KeyboardLayoutBinding.java;
      在build/intermediates/incremental/mergeDebugResources/stripped.dir/layout目录下会生成对应的布局文件:keyboard_layout.xml;
      接下来会一一进行分析。
      上面我们看到,在KeyBoardFragment内部的onCreateView()内部执行的逻辑,再一起看一下:

public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
    KeyboardLayoutBinding binding = DataBindingUtil.inflate(inflater, R.layout.keyboard_layout, container, false);
    View view = binding.getRoot();
    mViewModel = new ViewModel(mContext);
    binding.setGuide(mViewModel);
    return view;
}
a.DataBindingUtil.java

      在Fragement的onCreateView()内部通过DataBindingUtil.inflate()来创建KeyboardLayoutBinding对象binding,先看一下DataBindingUtil的inflate()实现:

private static DataBinderMapper sMapper = new DataBinderMapperImpl();

public static <T extends ViewDataBinding> T inflate(@NonNull LayoutInflater inflater,
            int layoutId, @Nullable ViewGroup parent, boolean attachToParent) {
    return inflate(inflater, layoutId, parent, attachToParent, sDefaultComponent);
}

public static <T extends ViewDataBinding> T inflate(
            @NonNull LayoutInflater inflater, int layoutId, @Nullable ViewGroup parent,
            boolean attachToParent, @Nullable DataBindingComponent bindingComponent) {
     final boolean useChildren = parent != null && attachToParent;
     final int startChildren = useChildren ? parent.getChildCount() : 0;
     final View view = inflater.inflate(layoutId, parent, attachToParent);
     if (useChildren) {
         return bindToAddedViews(bindingComponent, parent, startChildren, layoutId);
     } else {
         return bind(bindingComponent, view, layoutId);
     }
}

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

      inflate()内部会通过inflater,inflate()来加载对应layoutId的view,由于attachToParent为false,所以在inflate()内部直接执行bind()方法,在bind()内部会通过sMapper.getDataBinder()来返回ViewDataBinding的实现类对象,sMapper是DataBinderMapperImpl对象,接下来看一下DataBinderMapperImpl实现。

b.DataBinderMapperImpl.java

      前面说到,DataBinderMapperImpl.java是编译生成的文件,继承了MergedDataBinderMapper类,内部就一个构造方法内执行了addMapper()方法:

package androidx.databinding;

public class DataBinderMapperImpl extends MergedDataBinderMapper {
  DataBinderMapperImpl() {
    addMapper(new com.hly.learn.DataBinderMapperImpl());
  }
}

      在创建DataBinderMapperImpl对象的时候,在构造方法内执行addMapper()将创建com.hly.learn.DataBinderMapperImpl()对象作为参数传入,后续通过sMapper.getDataBinder()来获取对应的ViewDataBinding的实现类时,会最终调用到com.hly.learn.DataBinderMapperImpl.java中的getDataBinder()方法,接下来看一下com.hly.learn.DataBinderMapperImpl.java这个类的逻辑实现:

c.DataBinderMapperImpl.java

      此DataBinderMapperImpl.java不同于b步的DataBinderMapperImpl.java,注意区分,该类是主要的实现类:

public class DataBinderMapperImpl extends DataBinderMapper {
    private static final int LAYOUT_KEYBOARDLAYOUT = 1;
    private static final SparseIntArray INTERNAL_LAYOUT_ID_LOOKUP = new SparseIntArray(1);
    static {
        INTERNAL_LAYOUT_ID_LOOKUP.put(com.hly.learn.R.layout.keyboard_layout, LAYOUT_KEYBOARDLAYOUT);
    }

  @Override
  public ViewDataBinding getDataBinder(DataBindingComponent component, View view, int layoutId) {
    int localizedLayoutId = INTERNAL_LAYOUT_ID_LOOKUP.get(layoutId);
    if(localizedLayoutId > 0) {
      final Object tag = view.getTag();
      if(tag == null) {
        throw new RuntimeException("view must have a tag");
      }
      switch(localizedLayoutId) {
        case  LAYOUT_KEYBOARDLAYOUT: {
          if ("layout/keyboard_layout_0".equals(tag)) {
            return new KeyboardLayoutBindingImpl(component, view);
          }
          throw new IllegalArgumentException("The tag for keyboard_layout is invalid. Received: " + tag);
        }
      }
    }
    return null;
  }
  ......
}

      DataBinderMapperImpl实现了DataBinderMapper类,定义了静态变量LAYOUT_KEYBOARDLAYOUT和静态数组INTERNAL_LAYOUT_ID_LOOKUP(可能有多个使用databinding的layout),在静态代码块内将R.layout.keyboard_layout与LAYOUT_KEYBOARDLAYOUT建立映射关系存入INTERNAL_LAYOUT_ID_LOOKUP内,在执行getDataBinder时,通过创建时传入的layoutId从INTERNAL_LAYOUT_ID_LOOKUP找到对应的localizedLayoutId,然后根据view的tag关系来创建KeyboardLayoutBindingImpl对象,该tag体现在对应生成的布局文件:

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

        <TextView
            android:id="@+id/name"
            .....
            android:tag="binding_1"  />

        <ImageView
            .......
            android:tag="binding_2" />

        <TextView
            .......
            android:tag="binding_3" />

        <TextView
            .......
            android:tag="binding_4"           
            ....../>

        <TextView
            ........
            android:tag="binding_5"             
            ......./>

        <TextView
            .........
            android:tag="binding_6"  />

    </RelativeLayout>

      简单总结一下:在onCreateView()内部执行DataBindingUtil.inflate(),通过一步一步的调用后,最终返回的是KeyboardLayoutBindingImpl对象,该类是KeyboardLayoutBinding的实现类。
      接下来看一下KeyboardLayoutBindingImpl.java这个类:

d.KeyboardLayoutBindingImpl.java
public class KeyboardLayoutBindingImpl extends KeyboardLayoutBinding  {
    private KeyboardLayoutBindingImpl(androidx.databinding.DataBindingComponent bindingComponent, View root, Object[] bindings) {
        super(bindingComponent, root, 5
            , (android.widget.TextView) bindings[1]
            );
        this.mboundView0 = (android.widget.RelativeLayout) bindings[0];
        this.mboundView0.setTag(null);
        this.mboundView2 = (android.widget.ImageView) bindings[2];
        this.mboundView2.setTag(null);
        this.mboundView3 = (android.widget.TextView) bindings[3];
        this.mboundView3.setTag(null);
        this.mboundView4 = (android.widget.TextView) bindings[4];
        this.mboundView4.setTag(null);
        this.mboundView5 = (android.widget.TextView) bindings[5];
        this.mboundView5.setTag(null);
        this.mboundView6 = (android.widget.TextView) bindings[6];
        this.mboundView6.setTag(null);
        this.name.setTag(null);
        setRootTag(root);
        // listeners
        invalidateAll();
    }

    @Override
    public void invalidateAll() {
        synchronized(this) {
                mDirtyFlags = 0x40L;
        }
        requestRebind();
    }

    public void setGuide(@Nullable com.hly.learn.viewmodel.ViewModel Guide) {
        this.mGuide = Guide;
        synchronized(this) {
            mDirtyFlags |= 0x20L;
        }
        notifyPropertyChanged(BR.guide);
        super.requestRebind();
    }

    @Override
    protected boolean onFieldChange(int localFieldId, Object object, int fieldId) {
        switch (localFieldId) {
            case 0 :
                return onChangeGuideRightImageDescription((androidx.databinding.ObservableField<java.lang.String>) object, fieldId);
       ......
    }

    @Override
    protected void executeBindings() {
        ......
        //注册观察者
        updateRegistration(0, guideRightImageDescription);
        ......
        //回调后进行UI更新
        androidx.databinding.adapters.TextViewBindingAdapter.setText(this.mboundView3, guideRightImageDescriptionGet);
        ......
    }
}

      该类继承了KeyboardLayoutBinding类,在构造方法内,会调用父类的构造方法,将root view[即对应R.layout.keyboard_layout进行inflate生成的view]传给父类,在Fragment内部的onCreateView()中通过getRoot()返回对应layoutId创建的View,其他方法的逻辑执行会在接下来的数据与UI绑定时进行介绍。

e.KeyboardLayoutBinding.java
public abstract class KeyboardLayoutBinding extends ViewDataBinding {
  @NonNull
  public final TextView name;

  @Bindable
  protected ViewModel mGuide;

  protected KeyboardLayoutBinding(Object _bindingComponent, View _root, int _localFieldCount,
      TextView name) {
    super(_bindingComponent, _root, _localFieldCount);
    this.name = name;
  }

  public abstract void setGuide(@Nullable ViewModel guide);

  @Nullable
  public ViewModel getGuide() {
    return mGuide;
  }
  .......
  .......
}

      KeyboardLayoutBinding是个抽象类,继承了ViewDataBinding。

f.ViewDataBinding.java
public abstract class ViewDataBinding extends BaseObservable implements ViewBinding {
    .......
    .......
    private final View mRoot;
    .......
    protected ViewDataBinding(DataBindingComponent bindingComponent, View root, int localFieldCount) {
        mBindingComponent = bindingComponent;
        mLocalFieldObservers = new WeakListener[localFieldCount];
        this.mRoot = root;
        if (Looper.myLooper() == null) {
            throw new IllegalStateException("DataBinding must be created in view's UI Thread");
        }
       .......
    }

    public View getRoot() {
        return mRoot;
    }
    .......
    .......

      在创建KeyboardLayoutBindingImpl后,构造方法会一步一步的向上传,最终将root view保存在ViewDataBinding中,然后通过getRoot()获取到view。

四.数据与UI绑定分析

      通过上述分析可以看到了DataBindingUtil.inflate创建KeyboardLayoutBinding的整个过程,那数据与UI任何绑定的呢?
      在KeyboardLayoutBindingImpl的构造方法内,会调用 invalidateAll(),接下来看一下绑定流程:

a.ObservableField进行observe()

      DataBinding使用的是观察者模式,ObservableField数据注册观察者是在创建DataBinding的时候在构造方法中就执行了,先创建了对应ObservableField数量的WeakListener数组,然后执行流程如下:
      ------>invalidateAll()
            ------>requestRebind()
                  ------>executePendingBindings()
                        ------>executeBindingsInternal()
                              ------> executeBindings()[Impl]
                                    ------>updateRegistration(localFieldId, Observable observable)
                                          ------>updateRegistration(localFieldId, observable, CREATE_PROPERTY_LISTENER)[创建WeakPropertyListener]
                                                ------>registerTo()[将上步中创建的WeakPropertyListener赋值给执行创建的WeakListener对应的数组值]
                                                      ------>listener.setTarget(observable)
                                                            ------>WeakPropertyListener.addListener(Observable)
                                                                  ------>Observable.addOnPropertyChangedCallback(this);
      经过以上逻辑执行,Observable[ObservableField]注册了OnPropertyChanged callback,如果数据变化后,会回调OnPropertyChanged()方法,流程图如下:


image.png

      以上就是数据与UI绑定过程,那数据变化后,是如何反馈到UI上呢?接下来看一下数据变化后UI更新流程:

b.ObservableField数据变化后UI更新

      ObservableFiled数据变化后,最终UI更新执行流程如下:
      ------>set(value)
            ------>notifyChange()
                  ------>mCallbacks.notifyCallbacks(this, 0, null)[mCallbacks是PropertyChangeRegistry,通过上述addOnPropertyChangedCallback()加入mCallbacks列表]
                        ------>mNotifier.onNotifyCallback(mCallbacks.get(i), sender, arg, arg2)
                              ------>callback.onPropertyChanged(sender, arg)
                                    ------>WeakPropertyListener.onPropertyChanged()
                                          ------>handleFieldChange()
                                                ------>onFieldChange()------>requestRebind()----->......
                                                      ------>executeBindings()[Impl]
      在ObservableField数据变化后,最终会调用到Impl类里面的executeBindings()来更新UI,流程图如下:


image.png

      以上就是数据变化后UI更新的整个流程。

五.BindingAdapter

      DataBinding提供了BindingAdapter这个注解用于支持自定义属性,或者是修改原有属性。注解值可以是已有的 xml 属性,例如 android:src、android:text等,也可以自定义属性然后在 xml 中使用。
      例如,对于一个TextView ,希望在某个变量值发生变化时,可以动态改变显示的文字,此时就可以通过 BindingAdapter来实现。
      需要先定义一个静态方法,为之添加 BindingAdapter 注解,注解值是为TextView控件自定义的属性名,而该静态方法的两个参数可以这样来理解:当TextView控件的 step属性值发生变化时,DataBinding 就会将TextView实例以及新的step值传递给setProperty() 方法,从而可以根据此动态来改变TextView的相关属性。

<TextView
     android:layout_width="200dp"
     android:layout_height="50dp"
     android:layout_below="@+id/name"
     android:layout_centerHorizontal="true"
     android:layout_marginTop="400dp"
     android:gravity="center"
     android:textStyle="bold"
     app:step="@{guide.step}"/>

      BindingAdapter实现如下:

package com.hly.learn;

import android.util.Log;
import android.widget.TextView;

import androidx.databinding.BindingAdapter;
//可以单独写一个类,统一处理所有使用BindingAdapter注解的控件 
public class ViewBinding {
    @BindingAdapter(value = {"app:step"})
    public static void setProperty(TextView textView, int step) {
        Log.e("Seven", "step is: " + step);
        //可以根据step来设置textView的属性,例如改变文字,设置宽高等...
        textView.setText(xxx);
    }

    @BindingAdapter(value = {"app:url"})
    public static void updateImg(ImageView imageView, String url) {
        Glide.with(imageView.getContext()).load(url).into(imageView);
    }
}

六.效果图

1.jpg

2.jpg

      以上是结合实例及源码对DataBinding及ObservableField进行UI绑定及更新进行了分析,详细过程还需阅读ViewDataBinding、BaseObservable、PropertyChangeRegistry、CallbackRegistry等类文件来进一步分析。

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