安卓数据存储方案

持久化技术简介

数据持久化就是指将那些内存中的瞬时数据保存在存储设备中,保证即使在手机或电脑关机的情况下,这些数据仍然不回丢失。保存在内存中的数据是处于瞬时状态的,而保存在存储设备中的数据是处于持久状态的,持久化技术则提供一种机制可以让数据在瞬时状态和持久状态之间进行转换。
Android 系统中主要提供了3种方式,即文件存储、SharedPreference 存储以及数据存储。

1、文件存储

文件存储是 Android 中最基本的一种数据存储方式,它不对存储的内容进行任何的格式化处理,所有数据都是原封不动地保存到文件当中的,因而比较适合用于存储一些简单的文本数据或二进制数据。如果你想使用文件存储的方式来保存一些较为复杂的文本数据,就需要定义一套自己的格式,方便之后从文件中读取并解析。

1.1、将数据存储到文件中

下面以一个实例完成基本的文件存储功能。
新建一个页面,在布局中加入一个 EditText ,用户在文本框中输入内容后,按下
Back 键,将它存储到文件当中。布局页面如下:

<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:orientation="vertical">
    <EditText
        android:id="@+id/et_edit"
        android:layout_width="match_parent"
        android:layout_height="wrap_content" />
</LinearLayout>
public class MainActivity extends AppCompatActivity {
    private EditText ed_edit;
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        ed_edit = (EditText) findViewById(R.id.et_edit);
    }
    @Override
    protected void onDestroy() {
        super.onDestroy();
        String inputText = ed_edit.getText().toString();
        save(inputText);
    }
    public void save(String inputText) {
        FileOutputStream outputStream = null;
        BufferedWriter writer = null;
        try {
            outputStream = openFileOutput("data", Context.MODE_PRIVATE);
            writer = new BufferedWriter(new OutputStreamWriter(outputStream));
            writer.write(inputText);
        } catch (IOException e) {
            e.printStackTrace();
        } finally {
            try {
                if (writer != null) {
                    writer.close();
                }
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
    }
}

Context 类中提供了一个 openFileOutput() 方法,可以用于将数据存储到指定的文件中。这个方法接收两个参数,第一个参数是文件名,第二个参数是文件的操作模式,主要有两种模式可选,MODE_PRIVATE 和 MODE_APPEND。其中 MODE_PRIVATE 是默认的操作模式,表示当前指定同样文件名的适合,所写入的内容将会覆盖原文件中的内容,而 MODE_APPEND 则表示如果该文件已存在,就往文件里面追加内容,不存在就新建文件。
openFileOutput() 方法返回的是一个 FileOutputStream 对象,然后再借助它构建出一个 OutputStreamWriter 对象,接着再使用 OutputStreamWriter 构建出一个 BufferedWriter 对象,这样就可以通过 BufferedWriter 将文本内容写入到文件中。

1.2、从文件中读取数据

类似于将数据存储到文件中,Context 类中还提供了一个 openFileInput() 方法,用于从文件中读取数据。它只接收一个 参数,即要读取的文件名,并返回一个
FileInputStream 对象。如下:

public String load() {
        FileInputStream inputStream = null;
        BufferedReader reader = null;
        StringBuilder content = new StringBuilder();
        try {
            inputStream = openFileInput("data");
            reader = new BufferedReader(new InputStreamReader(inputStream));
            String line = "";
            while ((line = reader.readLine()) != null) {
                content.append(line);
            }
        } catch (IOException e) {
            e.printStackTrace();
        } finally {
            if (reader != null) {
                try {
                    reader.close();
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
        }
        return content.toString();    //返回得到的数据
    }

首先通过 openFileInput() 得到一个 FileInputStream 对象,然后借助它又构建出一个 InputStreamReader 对象,接着再使用 InputStreamReader 构建出一个 BufferedReader 对象,这样我们就可以通过 BufferedReader 进行一行行地读取,把文件中所有的文本内容全部读取出来,并放到一个 StringBuilder 对象中,最后将读取的内容返回就可以了。

2、SharedPreference 存储

SharedPreference 是使用键值对的方式来存储数据的。当保存一条数据的时候,需要给这条数据提供一个对应的键,这样在读取数据的时候就可以通过 这个键把相应的值取出来。而且 SharedPreference 还支持多种不同的数据类型存储,如果存储的数据类型是整型,那么读取出来的数据也是整型,如果是字符串,读取出来的数据仍是字符串。

2.1、将数据存储到 SharedPreference 中

要想使用 SharedPreference 存储数据,首先要得到 SharedPreference 对象。Android 中主要提供了3种方法来得到 SharedPreference 对象。

  1. Context 类中的 getSharedPreferences() 方法
    此方法接收两个参数,第一个参数用于指定 SharedPreference 文件的名称,如果指定的文件不存在则会创建一个。第二个参数用于指定操作模式,MODE_PRIVATE 默认的操作模式,和直接传入0效果是相同的,表示只有当前的应用程序才可以对这个 SharedPreference 文件进行读写,其余均被废弃。
  2. Activity 类中的 getPreferences() 方法
    此方法与上面方法很相似,不过它只接收一个操作模式参数,因为使用这个方法时会自动将当前活动的类名作为 SharedPreference 的文件名。
  3. PreferenceManager 类中的 getDefaultSharedPreferences() 方法
    这是一个静态方法,它接收一个 Context 参数,并自动使用当前应用程序的包名作为前缀来命名 SharedPreference。得到了 SharedPreference 对象之后,就可以开始向 SharedPreference 文件中存储数据了,主要分3部实现。
    • (1)调用 SharedPreference 对象的 edit() 方法来获取一个 SharedPreference.Editor 对象。
    • (2)向 SharedPreference.Editor 对象中添加数据,比如添加一个布尔类型数据就使 用 putBoolean() 方法,以此类推。
    • (3)调用 SharedPreference 对象的 edit() 方法来获取一个 SharedPreference.Editor 对象。调用 apply() 方法将添加的数据提交,从而完成数据的存储。
public void save() {
        SharedPreferences.Editor editor = getSharedPreferences("data",MODE_PRIVATE).edit();
        editor.putString("name","zhangsan");
        editor.putInt("age",18);
        editor.apply();
}

通过 getSharedPreferences() 方法指定 SharedPreferences 的文件名为 data ,并得到了 SharedPreferences.Editor 对象。接着向这个对象中添加数据,最后调用 apply() 方法提交。

2.2、从 SharedPreference 中读取数据

SharedPreferences 对象中提供了一系列的 get 方法,用于对存储的数据精选读取,每种 get 方法都对应了 SharedPreferences.Editor 中的一种put 方法,比如读取一个字符串就使用 getString() 方法。这些 get 方法都接收两个参数,第一个参数是键,第二个参数是默认值,即表示当传入的键找不到对应的值时会以什么样的默认值进行返回。

SharedPreferences preferences = getSharedPreferences("data", MODE_PRIVATE);
String name = preferences.getString("name", "");
int age = preferences.getInt("age", 0);

这里首先通过 getSharedPreferences() 方法得到了 SharedPreferences 对象,然后分别调用 getString() 和 getInt() 方法获得前面存储的姓名和年纪。

3、SQLite 数据库存储

SQLite 是一款轻量级得关系型数据库,它的运算速度非常快,占用资源很少,通常只需要几百kb的内存就足够了。SQLite 不仅支持标准的 SQL 语法,还遵循了数据库的 ACID 事务。
数据库中 事务的四大特性(ACID),事务的隔离级别。

3.1、创建数据库

Android 提供了一个 SQLiteOpenHelper 帮助类,借助这个类就可以简单地对数据库进行创建和升级。
SQLiteOpenHelper 是一个抽象类,如果我们需要使用它,就需要创建一个自己的帮助类去继承它。SQLiteOpenHelper 中有两个抽象方法,分别是 onCreate() 和 onUpgrade() ,我们必须在自己的类中重写这两个方法,然后分别在这两个方法中去实现创建、升级数据库的逻辑。
SQLiteOpenHelper 中还有两个非常重要的实例方法:getReadableDatabase() 和 getWritableDatabase() 。这两个方法都可以创建或打开一个现有的数据库(如果存在则直接打开、否则创建一个新的数据库),并返回一个可对数据库进行读写操作的对象。不同的是,当数据库不可写入的时候(如磁盘空间已满),getReadableDatabase() 方法返回的对象将以只读饿方式打开数据库,而 getWritableDatabase() 方法则将出现异常。
SQLiteOpenHelper 中有两个构造函数可供重写,一般选用参数较少的即可。其中包含4个参数,第一个参数是 Context。第二个参数是数据库名,创建数据库时使用的就是这里指定的名称。第三个参数允许我们在查询数据的时候返回一个自定义的 Cursor,一般都是传入null。第四个参数表示当前数据库的版本号,可用于对数据库进行升级操作。构建出 SQLiteOpenHelper 的实例之后,再调用它的 getReadableDatabase() getWritableDatabase()方法就能够创建数据库了。此时,重写的 onCreate() 方法也会得到执行,所以通常会在这里去处理一些创建表的逻辑。
下面建一个项目,实现基本功能。新建项目之后,在新建 MyDatabaseHelper 类去继承 SQLiteOpenHelper 如下:

public class MyDatabaseHelper extends SQLiteOpenHelper {
    public static final String CREATE_BOOK = "create table Book ("
            + "id integer primary key autoincrement, "
            + "author text, "
            + "price real, "
            + "pages integer, "
            + "name text)";

    private Context mContext;

    public MyDatabaseHelper(Context context, String name,
                            SQLiteDatabase.CursorFactory factory, int version) {
        super(context, name, factory, version);
        mContext = context;
    }

    @Override
    public void onCreate(SQLiteDatabase db) {
        db.execSQL(CREATE_BOOK);
        Toast.makeText(mContext, "Create succeeded", Toast.LENGTH_SHORT).show();
    }

    @Override
    public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) {

    }
}

在 onCreate() 方法中调用了 SQLiteDatabase 的 execSQL() 方法去执行这条建表语句,这样可以保证在数据库建立完成的同时还能创建 Book 表。
在布局文件中添加一个按钮,用于创建数据库,如下:

<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:orientation="vertical">
   <Button
       android:id="@+id/create_database"
       android:layout_width="wrap_content"
       android:layout_height="wrap_content"
       android:text="Create database"/>
</LinearLayout>

修改 MainActivity 中的代码如下:

public class MainActivity extends AppCompatActivity {
    private MyDatabaseHelper dbHelper;
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        dbHelper = new MyDatabaseHelper(this, "BookStore.db", null, 1);
        Button createDatabase = (Button) findViewById(R.id.create_database);
        createDatabase.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                dbHelper.getWritableDatabase();
            }
        });
    }
}

在 onCreate() 方法中构建了一个 MyDatabaseHelper 对象,并通过其构造函数设置了数据库名以及版本号,然后在按钮的点击事件中调用了 getWritableDatabase() 方法。这样当第一次点击按钮时就会检测到当前程序中并没有 BookStore.db 数据库,于是会创建该[图片上传中...(QQ截图20181206203802.png-4189ea-1544100355387-0)]
数据库并调用 MyDatabaseHelper 中的 onCreate() 方法,这样 Book 表也会得到创建,然后弹出成功提示。


效果图
3.2、升级数据库

修改 MyDatabaseHelper 中的代码,新增一张 Category 表

public static final String CREATE_CATEGORY = "create table Category ("
            + "id integer primary key autoincrement, "
            + "category_name text, "
            + "category_code integer)";

@Override
    public void onCreate(SQLiteDatabase db) {
        db.execSQL(CREATE_BOOK);
        db.execSQL(CREATE_CATEGORY);
        Toast.makeText(mContext, "Create succeeded", Toast.LENGTH_SHORT).show();
    }

再次点击按钮时,发现并不会创建成功,这是因为数据库已经存在了,MyDatabaseHelper 中的 onCreate() 方法不会再次执行了,因此新添加的表也不会创建了。
其实只需要使用 SQLiteOpenHelper 的升级功能。修改代码:

@Override
    public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) {
        db.execSQL("drop table if exists Book");
        db.execSQL("drop table if exists Category");
        onCreate(db);
    }

在 onUpgrade() 方法中执行两天DROP语句,如果发现数据库中已经存在 Book 或 Category 表,就将这两张表删掉,然后再调用 onCreate() 方法重新创建。
MyDatabaseHelper 构造函数中,一个参数是版本号,修改代码,如下:

dbHelper = new MyDatabaseHelper(this, "BookStore.db", null, 2 );
3.3、添加数据

调用 SQLiteOpenHelper 的 getReadableDatabase() 或getWritableDatabase() 方法会返回一个 SQLiteDatabase 对象,借助这个对象就可以对数据库进行CRUD操作了。
在 SQLiteDatabase 中提供了一个 insert() 方法专门用于添加数据。它接收3个参数,第一个参数是表名,第二个参数用于在未指定添加数据的情况下给某些可为空的列自动赋值NULL,一般直接传入null,第三个参数是一个 ContentValues 对象,它提供了一系列的 put() 方法重载,用于向 ContentValues 中添加数据,只需要将表中的每个列名以及相应的待添加数据传入即可。
在页面布局中新增一个按钮,实现点击按钮添加一组数据,如下:

protected void onCreate(Bundle savedInstanceState) {
        ...
        Button addData = (Button) findViewById(R.id.add_data);
        addData.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                SQLiteDatabase db = dbHelper.getWritableDatabase();
                ContentValues values = new ContentValues();
                // 添加第一条数据
                values.put("name", "The Da Vinci1");
                values.put("author", "zhang san");
                values.put("pages", 456);
                values.put("price", 199);
                db.insert("Book", null, values);
                values.clear();
                // 添加第二条数据
                values.put("name", "The Da Vinci2");
                values.put("author", "zhang san");
                values.put("pages", 456);
                values.put("price", 199);
                db.insert("Book", null, values);
                db.insert("Book", null, values);
            }
        });
    }
}

在点击按钮的事件中,外卖先获取到了 SQLiteDatabase 对象,然后使用 ContentValues 来对要添加的数据进行组装。

3.4、更新数据

SQLiteDatabase 中提供了 update() 方法,用于对数据进行更新,这个方法接收4个参数,第一个参数是表名,第二个参数是 ContentValues 对象,第三、四个参数用于约束更新某一行或几行中的数据,不指定的话默认就是更新所有行。
在页面布局中新增一个按钮,实现点击按钮更新数据,如下:

protected void onCreate(Bundle savedInstanceState) {
        ...
        Button uodateData = (Button) findViewById(R.id.update_data);
        uodateData.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                SQLiteDatabase db = dbHelper.getWritableDatabase();
                ContentValues values = new ContentValues();
                values.put("price", 19.9);
                db.update("Book",values,"name=?",new String[] {"The Da Vinci1"});
            }
        });
    }

在这里先构建了一个 ContentValues 对象,并给他制定了一组数据,然后调用了 SQLiteDatabase 的 updae() 方法取执行具体的更新操作。

3.5、删除数据

SQLiteDatabase 中提供了 delete() 方法,这个方法接收3个参数,第一个是表名,第二、三个参数用于约束删除某一行或某几行的数据,不指定的话默认删除所有行。
在页面布局中新增一个按钮,实现点击按钮删除数据,如下:

    protected void onCreate(Bundle savedInstanceState) {
        ...
        Button deleteData = (Button) findViewById(R.id.delete_data);
        deleteData.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                SQLiteDatabase db = dbHelper.getWritableDatabase();
                db.delete("Book", "pages>?", new String[]{"500"});
            }
        });
    }
3.6、查询数据

SQLiteDatabase 中提供了 query() 方法用于对数据进行查询。参数如下:


query() 参数
3.7、使用 SQL 操作数据库
  • 添加数据的方法
db.execSQL("insert into Book (name,author,pages,price)values (?,?,?,?)",
                        new  String[] {"The Da","Dan","454","16.69"});
  • 更新数据的方法
db.execSQL("update Book set prise=? where name=?",new String[] {
                        "10.99","The Da"
                });
  • 删除数据的方法
db.execSQL("delete from Book where pages>?",new String[]{"500"});
  • 查询数据的方法
db.rawQuery("select * from Book",null);

4、使用 LitePal 操作数据库

1、LitePal 简介

LitePal 是一款开源的 Android 数据库框架,它采用了对象关系映射的模式,并将外卖平时开发最常用的一些数据库功能进行了封装,使得不用写 SQL 语句就可以完成各种建表和增删改查的操作。地址
目前新版本是3.0.0,相较于2.0.0版本,结构上有了质的变化,但是在功能的基本使用上一样。
参考:新版LitePal发布,一次不可思议的升级

2、配置 LitePal

在 build.gradle 中添加:

dependencies {
    ...
    implementation 'org.litepal.android:java:3.0.0'
}

然后在 mian 目录下新建 assets 目录,然后在 assets 目录下新建 一个 litepal.xml 文件,编辑里面的内容,其中 <dbname> 标签表示数据库名,<version> 标签标识版本号,<list> 标签用于指定所有的映射模型,如下:

  

最后,还需要配置 AndroidManifest.xml 中的代码,

<application
        android:name="org.litepal.LitePalApplication"
        ...
</application>
3、创建和升级数据库

LitePal 采取的是对象关系映射(ORM)的模式,简单点说,就是我们使用的编程语言是面向对象语言,而使用的数据库则是关系型数据库,那么将面向对象的语言和面向关系的数据库之间建立一种映射关系,这就是对象关系映射。
定义一个 Book 类:

public class Book extends LitePalSupport {
    private int id;
    private String author;
    private double price;
    private int pages;
    private String name;
    private String press;

    public int getId() {
        return id;
    }

    public void setId(int id) {
        this.id = id;
    }

    public String getAuthor() {
        return author;
    }

    public void setAuthor(String author) {
        this.author = author;
    }

    public double getPrice() {
        return price;
    }

    public void setPrice(double price) {
        this.price = price;
    }

    public int getPages() {
        return pages;
    }

    public void setPages(int pages) {
        this.pages = pages;
    }

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public String getPress() {
        return press;
    }

    public void setPress(String press) {
        this.press = press;
    }
}

这是一个典型的 Java bean,在 Book 类中定义了几个字段以及相应的 getter 和 setter 方法。Book类就会对应数据库中的 Book 表,而类中的每一个字段对应了表中的每一个列,这就是对象关系映射最直观的体验。
接下来还需要将 Book 类添加到映射模型列表当中,修改 litepal.xml:

<list>
        <mapping class="com.example.dean.test.Book" />
</list>

这里使用<mapping>标签来声明我们要配置的映射模型类,注意一定要用完整的类名。不管有多少模型类需要映射,都使用同样的方式配置。
使用上个项目中的布局,修改 MainActivity 中的代码创建数据库,如下:

Button createDatabase = (Button) findViewById(R.id.create_database);
        createDatabase.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                Connector.getDatabase();
            }
        });

在使用 SQLite 时,升级数据库的时候需要把之前的表 drop 掉,然后再创建。使用 LitePal 来升级数据库直接在版本号上面加1就行了。

3、使用 LitePal 添加数据

使用 LitePal 添加数据,只需要创建出模型类得实例,再将所有要存储得数据设置好,最后调用 save() 方法。如下

Button addData = (Button) findViewById(R.id.add_data);
        addData.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                Book book = new Book();
                book.setName("The Da Code");
                book.setAuthor("zhangsan");
                book.setPages(454);
                book.setPrice(16.96);
                book.setPress("Unknow");
                book.save();
            }
        });
4、使用 LitePal 更新数据

更新数据要比添加数据复杂,因为它的API接口比较多,这里介绍几种常用的更新方式。
首先最简单的更新方式就是对已存储的对象重新设值,然后重新调用 save() 方法即可。对于 LitePal 来说,对象是否已存储就是根据调用 model.isSaved() 方法的结果来判断的,返回 true 就表示已存储,返回 false 就表示未存储。
实际上只有在两种情况下 model.isSaved() 方法才会返回 true,一种情况是已经调用过 model.save() 方法添加数据了,此时 model 会被认为是已存储的对象。另一种情况是 model 对象是通过 LitePal 提供的查询API查出来的,由于是从数据库中查到的对象,因此也会被认为是已存储的对象。

Button updateData = (Button) findViewById(R.id.update_data);
        updateData.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                Book book = new Book();
                book.setName("The Lost Sumbol");
                book.setAuthor("Dan");
                book.setPages(500);
                book.setPrice(19.99);
                book.setPress("Unknow");
                book.save();
                book.setPrice(10.99);
                book.save();
            }
        });

上面例子中,先添加了一条Book数据,然后调用 setPrice() 方法进行修改,然后再次调用 save() 方法。此时 LitePal 会发现当前 Book 对象是已存储的,因此不会再向数据库中添加数据,而是直接更新当前的数据。
这种更新方式只能对已存储的对象进行操作,限制性很大,另外一种更新方式,如下:

Book book = new Book();
book.setPrice(19.33);
book.setPress("Anchor");
book.updateAll("name = ? and author = ?", "The Lost Sumbol", "Dan");

直接 new 一个 Book 实例,然后调用 setPrice() 和 setPress() 方法设置更新的数据,然后调用 updateAll() 方法。updateAll() 可以指定一个约束条件。
不过在使用 updateAll() 方法时,就是当你想把一个字段的值更新未默认值时,是不可以用上面的方式来 set 数据的(如int的默认值是0,boolean的默认值是false)。比如把数据库表中的pages列更新为0,直接调用 book.setPages(0) 是不可以的,LitePal 此时不会对这个列进行更新。对于想要将数据更新成默认值,LitePal 统一提供了 setToDefault() 方法。

Book book = new Book();
book.setToDefault("pages");
book.updateAll();

特别备注,以后来自 LitePal1.3.2 版本,最新可以查看官网,如
如使用 save() 方法来更新由 find() 找到的记录,如下:
Book book = LitePal.find(Book.class,1);
book.setPrice(10.99);
book.save();
从 LiteSupport 继承的每个模型都有update() 和updateAll() 方法。可以用于指定的ID更新单个记录:
Book book = new Book();
book.setPrice(20.99f); // raise the price
book.update(1);
或者可以用 where 条件更新多个记录:
Book book = new Book();
book.setPrice(20.99f); // raise the price
book.updateAll("name = ?", "album");

4、使用 LitePal 删除数据

使用 LitePal 删除数据的方式主要有两种,第一种就是直接调用已存储的对象的 delete() 方法就可以了(类似与上面)。还有就是调用 DataSupport.deleteAll() 方法来删除数据。

DataSupport.deleteAll(Book.class,"price < ?","15")

特别备注,以后来自 LitePal1.3.2 版本,最新可以查看官网
调用LitePal 的静态方法delete() 可删除指定id的单条记录:
LitePal.delete(Book.class, 1);
也可调用LitePal 的静态方法deleteAll() 删除多条记录:
LitePal.deleteAll(Book.class, "price > ?" , "15");

4、使用 LitePal 查询数据

Android数据库高手秘籍(七)——体验LitePal的查询艺术

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

推荐阅读更多精彩内容