3.5.3 ContentProvider使用全解

本節(jié)例程下載地址:
WillFlowContentProvider
WillFlowProviderTest

一纱新、內(nèi)容提供器簡(jiǎn)介

內(nèi)容提供程序管理一組共享的應(yīng)用數(shù)據(jù)掰担,用于在不同的應(yīng)用程序之間實(shí)現(xiàn)數(shù)據(jù)共享的功能。它提供了一套完整的機(jī)制怒炸,允許一個(gè)程序訪問另一個(gè)程序中的數(shù)據(jù),同時(shí)還能保證被訪數(shù)據(jù)的安全性毡代。我們可以將數(shù)據(jù)存儲(chǔ)在文件系統(tǒng)阅羹、SQLite 數(shù)據(jù)庫、網(wǎng)絡(luò)上或我們的應(yīng)用可以訪問的任何其他永久性存儲(chǔ)位置教寂。 其他應(yīng)用可以通過內(nèi)容提供程序查詢數(shù)據(jù)捏鱼,甚至修改數(shù)據(jù)(如果內(nèi)容提供程序允許)。 例如酪耕,Android 系統(tǒng)可提供管理用戶聯(lián)系人信息的內(nèi)容提供程序导梆。 因此,任何具有適當(dāng)權(quán)限的應(yīng)用都可以查詢內(nèi)容提供程序的某一部分(如 ContactsContract.Data),以讀取和寫入有關(guān)特定人員的信息看尼。

目前递鹉,使用內(nèi)容提供器是 Android 實(shí)現(xiàn)跨程序共享數(shù)據(jù)的標(biāo)準(zhǔn)方式。不同于文件存儲(chǔ)和 Shared Preferences 存儲(chǔ)中的兩種全局可讀寫操作模式藏斩,內(nèi)容提供器可以選擇只對(duì)哪一部分?jǐn)?shù)據(jù)進(jìn)行共享躏结,從而保證我們程序中的隱私數(shù)據(jù)不會(huì)有泄漏的風(fēng)險(xiǎn)。當(dāng)然內(nèi)容提供程序也適用于讀取和寫入我們的應(yīng)用不共享的私有數(shù)據(jù)狰域。 例如媳拴,記事本示例應(yīng)用使用內(nèi)容提供程序來保存筆記。

內(nèi)容提供器的用法一般有兩種兆览,一種是使用現(xiàn)有的內(nèi)容提供器來讀取和操作相應(yīng)程序中的數(shù)據(jù)屈溉,另一種是創(chuàng)建自己的內(nèi)容提供器給我們程序的數(shù)據(jù)提供外部訪問接口。這和我們之前學(xué)習(xí)的廣播有點(diǎn)類似是吧抬探?那么接下來我們就逐一開始學(xué)習(xí)子巾。

二、訪問其他程序中的數(shù)據(jù)

當(dāng)一個(gè)應(yīng)用程序通過內(nèi)容提供器對(duì)其數(shù)據(jù)提供了外部訪問接口驶睦,任何其他的應(yīng)用程序就都可以對(duì)這部分?jǐn)?shù)據(jù)進(jìn)行訪問砰左。 Android 系統(tǒng)中自帶的電話簿、短信场航、媒體庫等程序都提供了類似的訪問接口缠导,這就使得第三方應(yīng)用程序可以充分地利用這部分?jǐn)?shù)據(jù)來實(shí)現(xiàn)更好的功能。下面我們就來看一看溉痢,內(nèi)容提供器到底是如何使用的僻造。

1、ContentResolver 的基本用法

對(duì)于每一個(gè)應(yīng)用程序來說孩饼,如果想要訪問內(nèi)容提供器中共享的數(shù)據(jù)髓削,就一定要借助 ContentResolve 類,我們可以通過 Context 中的 getContentResolver() 方法獲取到該類的實(shí)例镀娶。ContentResolver 中提供了一系列的方法用于對(duì)數(shù)據(jù)進(jìn)行 CRUD 操作立膛,其中 insert()方法用于添加數(shù)據(jù), update()方法用于更新數(shù)據(jù)梯码, delete()方法用于刪除數(shù)據(jù)宝泵, query()方法用于查詢數(shù)據(jù)。等我我們學(xué)習(xí)到后面轩娶,你會(huì)發(fā)現(xiàn) SQLiteDatabase 中也是使用的這幾個(gè)方法來進(jìn)行 CRUD操作的儿奶,只不過它們?cè)诜椒▍?shù)上稍微有一些區(qū)別。

不同于 SQLiteDatabase鳄抒, ContentResolver 中的增刪改查方法都是不接收表名參數(shù)的闯捎,而是使用一個(gè) Uri 參數(shù)代替椰弊,這個(gè)參數(shù)被稱為內(nèi)容 URI。內(nèi)容 URI 給內(nèi)容提供器中的數(shù)據(jù)建立了唯一標(biāo)識(shí)符瓤鼻,它主要由兩部分組成秉版,權(quán)限(authority)和路徑(path)。

  • 權(quán)限是用于對(duì)不同的應(yīng)用程序做區(qū)分的娱仔,一般為了避免沖突沐飘,都會(huì)采用程序包名的方式來進(jìn)行命名。比如某個(gè)程序的包名是 com.example.app牲迫,那么該程序?qū)?yīng)的權(quán)限就可以命名為 com.example.app.provider耐朴。
  • 路徑則是用于對(duì)同一應(yīng)用程序中不同的表做區(qū)分的,通常都會(huì)添加到權(quán)限的后面盹憎。比如某個(gè)程序的數(shù)據(jù)庫里存在兩張表:table1 和 table2筛峭,這時(shí)就可以將路徑分別命名為/table1和/table2,然后把權(quán)限和路徑進(jìn)行組合陪每,內(nèi)容 URI 就變成了 com.example.app.provider/table1和 com.example.app.provider/table2影晓。

不過,目前還很難辨認(rèn)出這兩個(gè)字符串就是兩個(gè)內(nèi)容URI檩禾,我們還需要在字符串的頭部加上協(xié)議聲明挂签。因此,內(nèi)容 URI 最標(biāo)準(zhǔn)的格式寫法如下:

content://com.wgh.willflowcontentprovider.provider/table1/table1
content://com.wgh.willflowcontentprovider.provider/table1/table2

如果你足夠細(xì)心的話你可以發(fā)現(xiàn):內(nèi)容 URI 可以非常清楚地表達(dá)出我們想要訪問哪個(gè)程序中哪張表里的數(shù)據(jù)盼产。 也正是因此饵婆, ContentResolver 中的增刪改查方法才都接收 Uri 對(duì)象作為參數(shù),因?yàn)槭褂帽砻脑捪到y(tǒng)將無法得知我們期望訪問的是哪個(gè)應(yīng)用程序里的表戏售。

在得到了內(nèi)容 URI 字符串之后侨核,我們還需要將它解析成 Uri 對(duì)象才可以作為參數(shù)傳入。解析的方法也相當(dāng)簡(jiǎn)單灌灾,代碼如下所示:

Uri uri = Uri.parse("content://com.wgh.willflowcontentprovider.provider/table1/table1")

只需要調(diào)用 Uri.parse()方法搓译,就可以將內(nèi)容 URI 字符串解析成 Uri 對(duì)象了。

現(xiàn)在我們就可以使用這個(gè) Uri 對(duì)象來查詢 table1 表中的數(shù)據(jù)了锋喜,代碼如下所示:

    Cursor cursor = getContentResolver().query(
            uri,
            projection,
            selection,
            selectionArgs,
            sortOrder);

這些參數(shù)和 SQLiteDatabase 中 query() 方法里的參數(shù)很像些己,但總體來說要簡(jiǎn)單一些,畢竟這是在訪問其他程序中的數(shù)據(jù)嘿般,沒必要構(gòu)建過于復(fù)雜的查詢語句轴总。下表對(duì)使用到的這部分參數(shù)進(jìn)行了詳細(xì)的解釋。

query()方法參數(shù) 對(duì)應(yīng) SQL 部分 描述
uri from table_name 指定查詢某個(gè)應(yīng)用程序下的某一張表
projection select column1, column2 指定查詢的列名
selection where column = value 指定 where 的約束條件
selectionArgs --- 為 where 中的占位符提供具體的值
orderBy order by column1, column2 指定查詢結(jié)果的排序方式

查詢完成后返回的仍然是一個(gè) Cursor 對(duì)象博个,這時(shí)我們就可以將數(shù)據(jù)從 Cursor 對(duì)象中逐個(gè)讀取出來了。讀取的思路仍然是通過移動(dòng)游標(biāo)的位置來遍歷 Cursor 的所有行功偿,然后再取出每一行中相應(yīng)列的數(shù)據(jù)盆佣,代碼如下所示:

    if (cursor != null) {
        while (cursor.moveToNext()) {
            String column1 = cursor.getString(cursor.getColumnIndex("column1"));
            int column2 = cursor.getInt(cursor.getColumnIndex("column2"));
        } 
        cursor.close();
    }

掌握了最難的查詢操作往堡,剩下的增加、修改共耍、刪除操作就更不在話下了虑灰。我們先來看看如何向 table1 表中添加一條數(shù)據(jù),代碼如下所示:

    ContentValues values = new ContentValues();
    values.put("column1", "text");
    values.put("column2", 1);
    getContentResolver().insert(uri, values);

可以看到痹兜,仍然是將待添加的數(shù)據(jù)組裝到 ContentValues 中穆咐,然后調(diào)用 ContentResolver 的 insert() 方法,將 Uri 和 ContentValues 作為參數(shù)傳入即可字旭。

現(xiàn)在如果我們想要更新這條新添加的數(shù)據(jù),把 column1 的值清空,可以借助 ContentResolver 的 update() 方法實(shí)現(xiàn)巍耗,代碼如下所示:

    ContentValues values = new ContentValues();
    values.put("column1", "");
    getContentResolver().update(uri, values, "column1 = ? and column2 = ?", new String[] {"text", "1"});

注意上述代碼使用了 selection 和 selectionArgs 參數(shù)來對(duì)想要更新的數(shù)據(jù)進(jìn)行約束令哟,以防止所有的行都會(huì)受影響。

最后屈暗,可以調(diào)用 ContentResolver 的 delete()方法將這條數(shù)據(jù)刪除掉拆讯,代碼如下所示:

getContentResolver().delete(uri, "column2 = ?", new String[] { "1" });

到這里為止,我們就把 ContentResolver 中的增刪改查方法全部學(xué)完了养叛,接下來种呐,我們就利用目前所學(xué)的知識(shí),看一看如何讀取系統(tǒng)電話簿中的聯(lián)系人信息弃甥。

2爽室、讀取系統(tǒng)聯(lián)系人

由于我們之前一直使用的都是模擬器,電話簿里面并沒有聯(lián)系人存在潘飘,所以現(xiàn)在需要自己手動(dòng)添加幾個(gè)肮之,以便稍后進(jìn)行讀取。打開電話簿程序卜录,我們可以通過點(diǎn)擊 Create a new contact 按鈕來對(duì)聯(lián)系人進(jìn)行創(chuàng)建戈擒。這里就先創(chuàng)建兩個(gè)聯(lián)系人吧,分別填入他們的姓名和手機(jī)號(hào)艰毒,如圖所示:


這樣準(zhǔn)備工作就做好了筐高,首先還是來編寫一下布局文件,這里我們希望讀取出來的聯(lián)系人信息能夠在 ListView 中顯示丑瞧。

修改 activity_main.xml 中的代碼柑土,如下所示:
<?xml version="1.0" encoding="utf-8"?>
<android.support.constraint.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    tools:context="com.wgh.willflowcontentprovider.MainActivity">

    <ListView
        android:id="@+id/contacts_view"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        app:layout_constraintBottom_toBottomOf="parent"
        app:layout_constraintLeft_toLeftOf="parent"
        app:layout_constraintRight_toRightOf="parent"
        app:layout_constraintTop_toTopOf="parent" />

</android.support.constraint.ConstraintLayout>
接著修改 MainActivity 中的代碼,如下所示:
public class MainActivity extends AppCompatActivity {
    ListView mContactsView;
    ArrayAdapter<String> mAdapter;
    List<String> mContactsList = new ArrayList<String>();

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

        mContactsView = (ListView) findViewById(R.id.contacts_view);
        mAdapter = new ArrayAdapter<String>(this, android.R.layout.simple_list_item_1, mContactsList);
        mContactsView.setAdapter(mAdapter);
        readContacts();
    }

    private void readContacts() {
        Cursor cursor = null;
        try {// 查詢聯(lián)系人數(shù)據(jù)
            cursor = getContentResolver().query(ContactsContract.CommonDataKinds.Phone.CONTENT_URI, null, null, null, null);
            while (cursor.moveToNext()) {// 獲取聯(lián)系人姓名
                String displayName = cursor.getString(cursor.getColumnIndex(ContactsContract.CommonDataKinds.Phone.DISPLAY_NAME));// 獲取聯(lián)系人手機(jī)號(hào)
                String number = cursor.getString(cursor.getColumnIndex(ContactsContract.CommonDataKinds.Phone.NUMBER));
                mContactsList.add(displayName + "\n" + number);
            }
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            if (cursor != null) {
                cursor.close();
            }
        }
    }
}

在onCreate()方法中绊汹,我們首先獲取了 ListView 控件的實(shí)例稽屏,并給它設(shè)置好了適配器,然后就去調(diào)用 readContacts()方法西乖。下面重點(diǎn)看下 readContacts()方法狐榔,可以看到坛增,這里使用了 ContentResolver 的 query()方法來查詢系統(tǒng)的聯(lián)系人數(shù)據(jù)。接著我們對(duì) Cursor 對(duì)象進(jìn)行遍歷薄腻,將聯(lián)系人姓名和手機(jī)號(hào)這些數(shù)據(jù)逐個(gè)取出收捣,聯(lián)系人姓名這一列對(duì)應(yīng)的常量是 ContactsContract.CommonDataKinds.Phone.DISPLAY_NAME,聯(lián)系人手機(jī)號(hào)這一列對(duì)應(yīng)的常量是 ContactsContract.CommonDataKinds.Phone.NUMBER庵楷。兩個(gè)數(shù)據(jù)都取出之后罢艾,將它們進(jìn)行拼接,并且中間加上換行符尽纽,然后將拼接后的數(shù)據(jù)添加到 ListView 里咐蚯。最后千萬不要忘記將 Cursor 對(duì)象關(guān)閉掉。

當(dāng)然了蜓斧,讀取系統(tǒng)聯(lián)系人也是需要聲明權(quán)限的仓蛆,因此修改 AndroidManifest.xml 中的代碼,如下所示:

<uses-permission android:name="android.permission.READ_CONTACTS" />
編譯運(yùn)行看效果:

三挎春、自定義內(nèi)容提供器

1看疙、創(chuàng)建內(nèi)容提供器的步驟

前面已經(jīng)提到過,如果想要實(shí)現(xiàn)跨程序共享數(shù)據(jù)的功能直奋,官方推薦的方式就是使用內(nèi)容提供器能庆,可以通過新建一個(gè)類去繼承 ContentProvider 的方式來創(chuàng)建一個(gè)自己的內(nèi)容提供器。ContentProvider 類中有六個(gè)抽象方法脚线,我們?cè)谑褂米宇惱^承它的時(shí)候搁胆,需要將這六個(gè)方法全部重寫。

新建 MyProvider 繼承自 ContentProvider邮绿,代碼如下所示:
/**
 * Created by   : WGH.
 */
public class MyProvider extends ContentProvider {
    @Override
    public boolean onCreate() {
        return false;
    }

    @Nullable
    @Override
    public Cursor query(@NonNull Uri uri, @Nullable String[] strings, @Nullable String s, @Nullable String[] strings1, @Nullable String s1) {
        return null;
    }

    @Nullable
    @Override
    public String getType(@NonNull Uri uri) {
        return null;
    }

    @Nullable
    @Override
    public Uri insert(@NonNull Uri uri, @Nullable ContentValues contentValues) {
        return null;
    }

    @Override
    public int delete(@NonNull Uri uri, @Nullable String s, @Nullable String[] strings) {
        return 0;
    }

    @Override
    public int update(@NonNull Uri uri, @Nullable ContentValues contentValues, @Nullable String s, @Nullable String[] strings) {
        return 0;
    }
}

在這六個(gè)方法中渠旁,相信大多數(shù)你都已經(jīng)非常熟悉了,我再來簡(jiǎn)單介紹一下吧船逮。

1. onCreate()

初始化內(nèi)容提供器的時(shí)候調(diào)用顾腊。通常會(huì)在這里完成對(duì)數(shù)據(jù)庫的創(chuàng)建和升級(jí)等操作,返回 true 表示內(nèi)容提供器初始化成功挖胃,返回 false 則表示失敗杂靶。注意,只有當(dāng)存在ContentResolver 嘗試訪問我們程序中的數(shù)據(jù)時(shí),內(nèi)容提供器才會(huì)被初始化。

2. query()

從內(nèi)容提供器中查詢數(shù)據(jù)蔚舀。使用 uri 參數(shù)來確定查詢哪張表饵沧, projection 參數(shù)用于確定查詢哪些列蚀之, selection 和 selectionArgs 參數(shù)用于約束查詢哪些行, sortOrder 參數(shù)用于對(duì)結(jié)果進(jìn)行排序捷泞, 查詢的結(jié)果存放在 Cursor 對(duì)象中返回。

3. insert()

向內(nèi)容提供器中添加一條數(shù)據(jù)寿谴。使用 uri 參數(shù)來確定要添加到的表锁右,待添加的數(shù)據(jù)保存在 values 參數(shù)中咏瑟。添加完成后,返回一個(gè)用于表示這條新記錄的 URI痪署。

4. update()

更新內(nèi)容提供器中已有的數(shù)據(jù)码泞。使用 uri 參數(shù)來確定更新哪一張表中的數(shù)據(jù),新數(shù)據(jù)保存在 values 參數(shù)中悯森, selection 和 selectionArgs 參數(shù)用于約束更新哪些行宋舷, 受影響的行數(shù)將作為返回值返回。

5. delete()

從內(nèi)容提供器中刪除數(shù)據(jù)瓢姻。使用 uri 參數(shù)來確定刪除哪一張表中的數(shù)據(jù)祝蝠, selection 和 electionArgs 參數(shù)用于約束刪除哪些行, 被刪除的行數(shù)將作為返回值返回幻碱。

6. getType()

根據(jù)傳入的內(nèi)容 URI 來返回相應(yīng)的 MIME 類型绎狭。

可以看到,幾乎每一個(gè)方法都會(huì)帶有 Uri 這個(gè)參數(shù)褥傍,這個(gè)參數(shù)也正是調(diào)用 ContentResolver 的增刪改查方法時(shí)傳遞過來的儡嘶。而現(xiàn)在,我們需要對(duì)傳入的 Uri 參數(shù)進(jìn)行解析摔桦,從中分析出調(diào)用方期望訪問的表和數(shù)據(jù)社付。

回顧一下,一個(gè)標(biāo)準(zhǔn)的內(nèi)容 URI 寫法是這樣的:

content://com.wgh.willflowcontentprovider.provider/table1

這就表示調(diào)用方期望訪問的是 com.com.wgh.willflowcontentprovider 這個(gè)應(yīng)用的 table1 表中的數(shù)據(jù)邻耕。除此之外鸥咖,我們還可以在這個(gè)內(nèi)容 URI 的后面加上一個(gè) id,如下所示:

content://com.wgh.willflowcontentprovider.provider/table1/1

這就表示調(diào)用方期望訪問的是 com.wgh.willflowcontentprovider 這個(gè)應(yīng)用的 table1 表中 id 為 1 的數(shù)據(jù)兄世。

內(nèi)容 URI 的格式主要就只有以上兩種啼辣,以路徑結(jié)尾就表示期望訪問該表中所有的數(shù)據(jù),以 id 結(jié)尾就表示期望訪問該表中擁有相應(yīng) id 的數(shù)據(jù)御滩。我們可以使用通配符的方式來分別匹配這兩種格式的內(nèi)容 URI鸥拧,規(guī)則如下:

  • “*”:表示匹配任意長度的任意字符
  • “#”:表示匹配任意長度的數(shù)字

所以党远,一個(gè)能夠匹配任意表的內(nèi)容 URI 格式就可以寫成:

content://com.wgh.willflowcontentprovider.provider/*

而一個(gè)能夠匹配 table1 表中任意一行數(shù)據(jù)的內(nèi)容 URI 格式就可以寫成:

content://com.wgh.willflowcontentprovider.provider/#

接著,我們?cè)俳柚?UriMatcher 這個(gè)類就可以輕松地實(shí)現(xiàn)匹配內(nèi)容 URI 的功能富弦。UriMatcher 中提供了一個(gè) addURI() 方法沟娱,這個(gè)方法接收三個(gè)參數(shù),可以分別把權(quán)限腕柜、路徑和一個(gè)自定義代碼傳進(jìn)去济似。這樣,當(dāng)調(diào)用 UriMatcher 的 match() 方法時(shí)盏缤,就可以將一個(gè) Uri 對(duì)象傳入,返回值是某個(gè)能夠匹配這個(gè) Uri 對(duì)象所對(duì)應(yīng)的自定義代碼唉铜,利用這個(gè)代碼,我們就可以判斷出調(diào)用方期望訪問的是哪張表中的數(shù)據(jù)了潭流。

修改 MyProvider 中的代碼碰声,如下所示:

    public static final int TABLE1_DIR = 0;
    public static final int TABLE1_ITEM = 1;
    public static final int TABLE2_DIR = 2;
    public static final int TABLE2_ITEM = 3;

    private static UriMatcher uriMatcher;

    static {
        uriMatcher = new UriMatcher(UriMatcher.NO_MATCH);
        uriMatcher.addURI("com.wgh.willflowcontentprovider.provider", "table1", TABLE1_DIR);
        uriMatcher.addURI("com.wgh.willflowcontentprovider.provider ", "table1/#", TABLE1_ITEM);
        uriMatcher.addURI("com.wgh.willflowcontentprovider.provider ", "table2", TABLE2_DIR);
        uriMatcher.addURI("com.wgh.willflowcontentprovider.provider ", "table2/#", TABLE2_ITEM);
    }

    @Override
    public boolean onCreate() {
        return false;
    }

    @Nullable
    @Override
    public Cursor query(@NonNull Uri uri, @Nullable String[] strings, @Nullable String s, @Nullable String[] strings1, @Nullable String s1) {
        switch (uriMatcher.match(uri)) {
            case TABLE1_DIR:
                // 查詢table1表中的所有數(shù)據(jù)
                break;
            case TABLE1_ITEM:
                // 查詢table1表中的單條數(shù)據(jù)
                break;
            case TABLE2_DIR:
                // 查詢table2表中的所有數(shù)據(jù)
                break;
            case TABLE2_ITEM:
                // 查詢table2表中的單條數(shù)據(jù)
                break;
            default:
                break;
        }
        return null;
    }

可以看到, MyProvider 中新增了四個(gè)整型常量,其中 TABLE1_DIR 表示訪問 table1 表中的所有數(shù)據(jù)郑象,TABLE1_ITEM 表示訪問 table1 表中的單條數(shù)據(jù)贡这, TABLE2_DIR 表示訪問 table2 表中的所有數(shù)據(jù), TABLE2_ITEM 表示訪問 table2 表中的單條數(shù)據(jù)厂榛。接著在靜態(tài)代碼塊里我們創(chuàng)建了 UriMatcher 的實(shí)例盖矫,并調(diào)用 addURI() 方法,將期望匹配的內(nèi)容 URI 格式傳遞進(jìn)去击奶,注意這里傳入的路徑參數(shù)是可以使用通配符的辈双。然后當(dāng) query() 方法被調(diào)用的時(shí)候,就會(huì)通過 UriMatcher 的 match() 方法對(duì)傳入的 Uri 對(duì)象進(jìn)行匹配柜砾,如果發(fā)現(xiàn) UriMatcher 中某個(gè)內(nèi)容 URI 格式成功匹配了該 Uri 對(duì)象湃望,則會(huì)返回相應(yīng)的自定義代碼,然后我們就可以判斷出調(diào)用方期望訪問的到底是什么數(shù)據(jù)了。

上述代碼只是以 query() 方法為例做了個(gè)示范证芭,其實(shí) insert()瞳浦、 update()、 delete() 這幾個(gè)方法的實(shí)現(xiàn)也是差不多的废士,它們都會(huì)攜帶 Uri 這個(gè)參數(shù)叫潦,然后同樣利用 UriMatcher 的 match() 方法判斷出調(diào)用方期望訪問的是哪張表,再對(duì)該表中的數(shù)據(jù)進(jìn)行相應(yīng)的操作就可以了官硝。

除此之外诅挑,還有一個(gè)方法你會(huì)比較陌生,即 getType() 方法泛源。它是所有的內(nèi)容提供器都必須提供的一個(gè)方法,用于獲取 Uri 對(duì)象所對(duì)應(yīng)的 MIME 類型忿危。 一個(gè)內(nèi)容 URI 所對(duì)應(yīng)的 MIME 字符串主要由三部分組分达箍, Android 對(duì)這三個(gè)部分做了如下格式規(guī)定:

  • 必須以 vnd 開頭。
  • 如果內(nèi)容 URI 以路徑結(jié)尾铺厨,則后接 android.cursor.dir/缎玫,如果內(nèi)容 URI 以 id 結(jié)尾,則后接 android.cursor.item/解滓。
  • 最后接上 vnd.<authority>.<path>赃磨。

所以,對(duì)于 content://com.wgh.willflowcontentprovider.provider/table1 這個(gè)內(nèi)容 URI洼裤,它所對(duì)應(yīng)的 MIME 類型就可以寫成:

vnd.android.cursor.dir/vnd.com.wgh.willflowcontentprovider.provider.table1

對(duì)于 content://com.wgh.willflowcontentprovider.provider/table1/1 這個(gè)內(nèi)容 URI邻辉,它所對(duì)應(yīng)的 MIME 類型就可以寫成:

vnd.android.cursor.item/vnd.com.wgh.willflowcontentprovider.provider.table1

現(xiàn)在我們可以繼續(xù)完善 MyProvider 中的內(nèi)容了,這次來實(shí)現(xiàn) getType()方法中的邏輯腮鞍,代碼如下所示:

    @Nullable
    @Override
    public String getType(@NonNull Uri uri) {
        switch (uriMatcher.match(uri)) {
            case TABLE1_DIR:
                return "vnd.android.cursor.dir/vnd.com.wgh.willflowcontentprovider.provider.table1";
            case TABLE1_ITEM:
                return "vnd.android.cursor.item/vnd.com.wgh.willflowcontentprovider.provider.table1";
            case TABLE2_DIR:
                return "vnd.android.cursor.dir/vnd.com.wgh.willflowcontentprovider.provider.table2";
            case TABLE2_ITEM:
                return "vnd.android.cursor.item/vnd.com.wgh.willflowcontentprovider.provider.table2";
            default:
                break;
        }
        return null;
    }

到這里值骇,一個(gè)完整的內(nèi)容提供器就創(chuàng)建完成了,現(xiàn)在任何一個(gè)應(yīng)用程序都可以使用 ContentResolver 來訪問我們程序中的數(shù)據(jù)移国。那么前面所提到的吱瘩,如何才能保證隱私數(shù)據(jù)不會(huì)泄漏出去呢?其實(shí)多虧了內(nèi)容提供器的良好機(jī)制迹缀,這個(gè)問題在不知不覺中已經(jīng)被解決了使碾。因?yàn)樗械?CRUD 操作都一定要匹配到相應(yīng)的內(nèi)容 URI 格式才能進(jìn)行的,而我們當(dāng)然不可能向 UriMatcher 中添加隱私數(shù)據(jù)的 URI祝懂,所以這部分?jǐn)?shù)據(jù)根本無法被外部程序訪問到票摇,安全問題也就不存在了。那么下面我們就來實(shí)戰(zhàn)一下嫂易,真正體驗(yàn)一回跨程序數(shù)據(jù)共享的功能兄朋。

2、實(shí)現(xiàn)跨程序數(shù)據(jù)共享

簡(jiǎn)單起見,我們還是在面創(chuàng)建的代碼基礎(chǔ)上繼續(xù)開發(fā)颅和,通過內(nèi)容提供器來給它加入外部訪問接口傅事。首先將 MyDatabaseHelper 中使用 Toast 彈出創(chuàng)建數(shù)據(jù)庫成功的提示去除掉,因?yàn)榭绯绦蛟L問時(shí)我們不能直接使用 Toast峡扩。然后添加一個(gè) DatabaseProvider 類蹭越,代碼如下所示:

/**
 * Created by   : WGH.
 */
public class DatabaseProvider  extends ContentProvider {
    public static final int BOOK_DIR = 0;
    public static final int BOOK_ITEM = 1;
    public static final int CATEGORY_DIR = 2;
    public static final int CATEGORY_ITEM = 3;
    public static final String AUTHORITY = "com.wgh.willflowcontentprovider.provider";

    private static UriMatcher uriMatcher;
    private MyDatabaseHelper dbHelper;

    static {
        uriMatcher = new UriMatcher(UriMatcher.NO_MATCH);
        uriMatcher.addURI(AUTHORITY, "book", BOOK_DIR);
        uriMatcher.addURI(AUTHORITY, "book/#", BOOK_ITEM);
        uriMatcher.addURI(AUTHORITY, "category", CATEGORY_DIR);
        uriMatcher.addURI(AUTHORITY, "category/#", CATEGORY_ITEM);
    }

    @Override
    public boolean onCreate() {
        dbHelper = new MyDatabaseHelper(getContext(), "BookStore.db", null, 2);
        return true;
    }

    @Nullable
    @Override
    public Cursor query(@NonNull Uri uri, @Nullable String[] projection, @Nullable String selection, @Nullable String[] selectionArgs, @Nullable String sortOrder) {
        // 查詢數(shù)據(jù)
        SQLiteDatabase db = dbHelper.getReadableDatabase();
        Cursor cursor = null;
        switch (uriMatcher.match(uri)) {
            case BOOK_DIR:
                cursor = db.query("Book", projection, selection, selectionArgs, null, null, sortOrder);
                break;
            case BOOK_ITEM:
                String bookId = uri.getPathSegments().get(1);
                cursor = db.query("Book", projection, "id = ?", new String[]{ bookId }, null, null, sortOrder);
                break;
            case CATEGORY_DIR:
                cursor = db.query("Category", projection, selection, selectionArgs, null, null, sortOrder);
                break;
            case CATEGORY_ITEM:
                String categoryId = uri.getPathSegments().get(1);
                cursor = db.query("Category", projection, "id = ?", new String[]{ categoryId }, null, null, sortOrder);
                break;
            default:
                break;
        }
        return cursor;
    }

    @Nullable
    @Override
    public String getType(@NonNull Uri uri) {
        switch (uriMatcher.match(uri)) {
            case BOOK_DIR:
                return "vnd.android.cursor.dir/vnd.com.wgh.willflowcontentprovider.provider.book";
            case BOOK_ITEM:
                return "vnd.android.cursor.item/vnd.com.wgh.willflowcontentprovider.provider.book";
            case CATEGORY_DIR:
                return "vnd.android.cursor.dir/vnd.com.wgh.willflowcontentprovider.provider.category";
            case CATEGORY_ITEM:
                return "vnd.android.cursor.item/vnd.com.wgh.willflowcontentprovider.provider.category";
        }
        return null;
    }

    @Nullable
    @Override
    public Uri insert(@NonNull Uri uri, @Nullable ContentValues contentValues) {
        // 添加數(shù)據(jù)
        SQLiteDatabase db = dbHelper.getWritableDatabase();
        Uri uriReturn = null;
        switch (uriMatcher.match(uri)) {
            case BOOK_DIR:
            case BOOK_ITEM:
                long newBookId = db.insert("Book", null, contentValues);
                uriReturn = Uri.parse("content://" + AUTHORITY + "/book/" + newBookId);
                break;
            case CATEGORY_DIR:
            case CATEGORY_ITEM:
                long newCategoryId = db.insert("Category", null, contentValues);
                uriReturn = Uri.parse("content://" + AUTHORITY + "/category/" + newCategoryId);
                break;
            default:
                break;
        }
        return uriReturn;
    }

    @Override
    public int delete(@NonNull Uri uri, @Nullable String selection, @Nullable String[] selectionArgs) {
        // 刪除數(shù)據(jù)
        SQLiteDatabase db = dbHelper.getWritableDatabase();
        int deletedRows = 0;
        switch (uriMatcher.match(uri)) {
            case BOOK_DIR:
                deletedRows = db.delete("Book", selection, selectionArgs);
                break;
            case BOOK_ITEM:
                String bookId = uri.getPathSegments().get(1);
                deletedRows = db.delete("Book", "id = ?", new String[] { bookId });
                break;
            case CATEGORY_DIR:
                deletedRows = db.delete("Category", selection, selectionArgs);
                break;
            case CATEGORY_ITEM:
                String categoryId = uri.getPathSegments().get(1);
                deletedRows = db.delete("Category", "id = ?", new String[]
                        { categoryId });
                break;
            default:
                break;
        }
        return deletedRows;
    }

    @Override
    public int update(@NonNull Uri uri, @Nullable ContentValues contentValues, @Nullable String selection, @Nullable String[] selectionArgs) {
        // 更新數(shù)據(jù)
        SQLiteDatabase db = dbHelper.getWritableDatabase();
        int updatedRows = 0;
        switch (uriMatcher.match(uri)) {
            case BOOK_DIR:
                updatedRows = db.update("Book", contentValues, selection, selectionArgs);
                break;
            case BOOK_ITEM:
                String bookId = uri.getPathSegments().get(1);
                updatedRows = db.update("Book", contentValues, "id = ?", new String[]{ bookId });
                break;
            case CATEGORY_DIR:
                updatedRows = db.update("Category", contentValues, selection, selectionArgs);
                break;
            case CATEGORY_ITEM:
                String categoryId = uri.getPathSegments().get(1);
                updatedRows = db.update("Category", contentValues, "id = ?", new String[]{ categoryId });
                break;
            default:
                break;
        }
        return updatedRows;
    }
}

首先在類的一開始,同樣是定義了四個(gè)常量教届,分別用于表示訪問 Book 表中的所有數(shù)據(jù)响鹃、訪問 Book 表中的單條數(shù)據(jù)、訪問 Category 表中的所有數(shù)據(jù)和訪問 Category 表中的單條數(shù)據(jù)案训。然后在靜態(tài)代碼塊里對(duì) UriMatcher 進(jìn)行了初始化操作买置,將期望匹配的幾種 URI 格式添加了進(jìn)去。接下來就是每個(gè)抽象方法的具體實(shí)現(xiàn)了强霎,我們分別來看一下忿项。

(1)先來看下 onCreate() 方法

這個(gè)方法的代碼很短,就是創(chuàng)建了一個(gè) MyDatabaseHelper 的實(shí)例城舞,然后返回 true 表示內(nèi)容提供器初始化成功轩触,這時(shí)數(shù)據(jù)庫就已經(jīng)完成了創(chuàng)建或升級(jí)操作。

(2)接著看一下 query() 方法

在這個(gè)方法中先獲取到了 SQLiteDatabase 的實(shí)例家夺,然后根據(jù)傳入的 Uri 參數(shù)判斷出用戶想要訪問哪張表脱柱,再調(diào)用 SQLiteDatabase 的 query() 進(jìn)行查詢,并將 Cursor 對(duì)象返回就好了拉馋。注意當(dāng)訪問單條數(shù)據(jù)的時(shí)候有一個(gè)細(xì)節(jié)榨为,這里調(diào)用了 Uri 對(duì)象的 getPathSegments() 方法,它會(huì)將內(nèi)容 URI 權(quán)限之后的部分以“ /” 符號(hào)進(jìn)行分割煌茴,并把分割后的結(jié)果放入到一個(gè)字符串列表中柠逞,那這個(gè)列表的第 0 個(gè)位置存放的就是路徑,第 1 個(gè)位置存放的就是 id 了景馁。得到了 id 之后板壮,再通過 selection 和 selectionArgs 參數(shù)進(jìn)行約束,就實(shí)現(xiàn)了查詢單條數(shù)據(jù)的功能合住。

(3)再往后就是 insert() 方法

同樣它也是先獲取到了 SQLiteDatabase 的實(shí)例绰精,然后根據(jù)傳入的 Uri 參數(shù)判斷出用戶想要往哪張表里添加數(shù)據(jù),再調(diào)用 SQLiteDatabase 的 insert() 方法進(jìn)行添加就可以了透葛。注意 insert() 方法要求返回一個(gè)能夠表示這條新增數(shù)據(jù)的 URI笨使,所以我們還需要調(diào)用 Uri.parse() 方法來將一個(gè)內(nèi)容 URI 解析成 Uri 對(duì)象,當(dāng)然這個(gè)內(nèi)容 URI 是以新增數(shù)據(jù)的 id 結(jié)尾的僚害。

(4)接下來就是 update()方法了

這個(gè)方法也是先獲取 SQLiteDatabase 的實(shí)例硫椰,然后根據(jù)傳入的 Uri 參數(shù)判斷出用戶想要更新哪張表里的數(shù)據(jù),再調(diào)用 SQLiteDatabase 的 update() 方法進(jìn)行更新就好了,受影響的行數(shù)將作為返回值返回靶草。

(5)下面是 delete()方法

這里仍然是先獲取到 SQLiteDatabase 的實(shí)例蹄胰,然后根據(jù)傳入的 Uri 參數(shù)判斷出用戶想要?jiǎng)h除哪張表里的數(shù)據(jù),再調(diào)用 SQLiteDatabase 的 delete() 方法進(jìn)行刪除就好了奕翔,被刪除的行數(shù)將作為返回值返回裕寨。

這樣我們就將內(nèi)容提供器中的代碼全部編寫完了,不過離實(shí)現(xiàn)跨程序數(shù)據(jù)共享的功能還差了一小步派继,因?yàn)檫€需要將內(nèi)容提供器在 AndroidManifest.xml 文件中注冊(cè)才可以宾袜,如下所示:

        <provider
            android:name=".DatabaseProvider"
            android:authorities="com.wgh.willflowcontentprovider.provider"
            android:exported="true">
        </provider>

這里我們使用了 <provider> 標(biāo)簽來對(duì) DatabaseProvider 這個(gè)內(nèi)容提供器進(jìn)行注冊(cè),在 android:name 屬性中指定了該類的全名驾窟,又在 android:authorities 屬性中指定了該內(nèi)容提供器的權(quán)限庆猫。

現(xiàn)在這個(gè)項(xiàng)目就已經(jīng)擁有了跨程序共享數(shù)據(jù)的功能了。首先運(yùn)行一下項(xiàng)目绅络,然后接著關(guān)閉掉這個(gè)項(xiàng)目阅悍,并創(chuàng)建一個(gè)新項(xiàng)目WillFlowProviderTest, 我們就將通過WillFlowProviderTest中的代碼程序去訪問剛剛啟動(dòng)又關(guān)閉掉的這個(gè)項(xiàng)目中的數(shù)據(jù)昨稼。

先來編寫一下布局文件 activity_main.xml :
<android.support.constraint.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    tools:context="com.wgh.willflowprovidertest.MainActivity">

    <Button
        android:id="@+id/button1"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="添加"
        android:textColor="#05d602"
        android:textSize="25dp" />

    <Button
        android:id="@+id/button2"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="查詢"
        android:textColor="#07b7f2"
        android:textSize="25dp" />

    <Button
        android:id="@+id/button3"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="修改"
        android:textColor="#f28007"
        android:textSize="25dp" />

    <Button
        android:id="@+id/button4"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="刪除"
        android:textColor="#f20755"
        android:textSize="25dp" />

</android.support.constraint.ConstraintLayout>

布局文件放置了四個(gè)按鈕,分別用于添加拳锚、查詢假栓、修改和刪除數(shù)據(jù)的。

然后修改 MainActivity 中的代碼霍掺,如下所示:
    @Override
    public void onClick(View view) {
        switch (view.getId()) {
            case R.id.button1:
                // 添加數(shù)據(jù)
                Uri uriAdd = Uri.parse("content://com.wgh.willflowcontentprovider.provider/book");
                ContentValues values = new ContentValues();
                values.put("name", "WillFlow");
                values.put("author", "WGH");
                values.put("pages", 525);
                values.put("price", 16.18);
                Uri newUri = getContentResolver().insert(uriAdd, values);
                newId = newUri.getPathSegments().get(1);
                break;
            case R.id.button2:
                // 查詢數(shù)據(jù)
                Uri uriQuery = Uri.parse("content://com.wgh.willflowcontentprovider.provider/book");
                Cursor cursor = getContentResolver().query(uriQuery, null, null, null, null);
                if (cursor != null) {
                    while (cursor.moveToNext()) {
                        String name = cursor.getString(cursor.getColumnIndex("name"));
                        String author = cursor.getString(cursor.getColumnIndex("author"));
                        int pages = cursor.getInt(cursor.getColumnIndex("pages"));
                        double price = cursor.getDouble(cursor.getColumnIndex("price"));
                        Log.d(TAG, "book name : " + name + ", author : " + author + ", pages : " + pages + ", price : " + price);
                        Toast.makeText(MainActivity.this, "book name : " + name + ", author : " + author + ", pages : " + pages + ", price : " + price, Toast.LENGTH_SHORT).show();
                    }
                    cursor.close();
                }
                break;
            case R.id.button3:
                // 更新數(shù)據(jù)
                Uri uriUpdate = Uri.parse("content://com.wgh.willflowcontentprovider.provider/book/" + newId);
                ContentValues contentValues = new ContentValues();
                contentValues.put("name", "A Flow of Wills");
                contentValues.put("pages", 1314);
                contentValues.put("price", 29.26);
                getContentResolver().update(uriUpdate, contentValues, null, null);
                break;
            case R.id.button4:
                // 刪除數(shù)據(jù)
                Uri uriDelete = Uri.parse("content://com.wgh.willflowcontentprovider.provider/book/" + newId);
                getContentResolver().delete(uriDelete, null, null);
                break;
        }
    }

我們分別在這四個(gè)按鈕的點(diǎn)擊事件里面處理了增刪改查的邏輯匾荆。

添加數(shù)據(jù)的時(shí)候,首先調(diào)用了 Uri.parse() 方法將一個(gè)內(nèi)容 URI 解析成 Uri 對(duì)象杆烁,然后把要添加的數(shù)據(jù)都存放到 ContentValues 對(duì)象中牙丽,接著調(diào)用 ContentResolver 的 insert() 方法執(zhí)行添加操作就可以了。注意 insert() 方法會(huì)返回一個(gè) Uri 對(duì)象兔魂,這個(gè)對(duì)象中包含了新增數(shù)據(jù)的 id烤芦,我們通過 getPathSegments() 方法將這個(gè) id 取出,稍后會(huì)用到它析校。

查詢數(shù)據(jù)的時(shí)候构罗,同樣是調(diào)用了 Uri.parse() 方法將一個(gè)內(nèi)容 URI 解析成 Uri 對(duì)象,然后調(diào)用 ContentResolver 的 query() 方法去查詢數(shù)據(jù)智玻,查詢的結(jié)果當(dāng)然還是存放在 Cursor 對(duì)象中的遂唧。之后對(duì) Cursor 進(jìn)行遍歷,從中取出查詢結(jié)果吊奢,并一一打印出來盖彭。

更新數(shù)據(jù)的時(shí)候,也是先將內(nèi)容 URI 解析成 Uri 對(duì)象,然后把想要更新的數(shù)據(jù)存放到 ContentValues 對(duì)象中召边,再調(diào)用 ContentResolver 的 update() 方法執(zhí)行更新操作就可以了铺呵。注意這里我們?yōu)榱瞬幌胱?Book 表中其他的行受到影響,在調(diào)用 Uri.parse() 方法時(shí)掌实,給內(nèi)容 URI 的尾部增加了一個(gè) id陪蜻,而這個(gè) id 正是添加數(shù)據(jù)時(shí)所返回的。這就表示我們只希望更新剛剛添加的那條數(shù)據(jù)贱鼻, Book 表中的其他行都不會(huì)受影響宴卖。

刪除數(shù)據(jù)的時(shí)候,也是使用同樣的方法解析了一個(gè)以 id 結(jié)尾的內(nèi)容 URI邻悬,然后調(diào)用 ContentResolver 的 delete() 方法執(zhí)行刪除操作就可以了症昏。由于我們?cè)趦?nèi)容 URI 里指定了一個(gè)id,因此只會(huì)刪掉擁有相應(yīng) id 的那行數(shù)據(jù)父丰, Book 表中的其他數(shù)據(jù)都不會(huì)受影響肝谭。

編譯運(yùn)行看效果:

由此可以看出,我們的跨程序共享數(shù)據(jù)功能已經(jīng)成功實(shí)現(xiàn)了蛾扇!現(xiàn)在不僅是 WillFlowProviderTest 程序攘烛,任何一個(gè)程序都可以輕松訪問 WillFlowContentProvider 中的數(shù)據(jù),而且我們還絲毫不用擔(dān)心隱私數(shù)據(jù)泄漏的問題镀首。

點(diǎn)此進(jìn)入:GitHub開源項(xiàng)目“愛閱”坟漱。

感謝優(yōu)秀的你跋山涉水看到了這里,歡迎關(guān)注下讓我們永遠(yuǎn)在一起更哄!

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末芋齿,一起剝皮案震驚了整個(gè)濱河市,隨后出現(xiàn)的幾起案子成翩,更是在濱河造成了極大的恐慌觅捆,老刑警劉巖,帶你破解...
    沈念sama閱讀 211,376評(píng)論 6 491
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件麻敌,死亡現(xiàn)場(chǎng)離奇詭異栅炒,居然都是意外死亡,警方通過查閱死者的電腦和手機(jī)术羔,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 90,126評(píng)論 2 385
  • 文/潘曉璐 我一進(jìn)店門职辅,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人聂示,你說我怎么就攤上這事域携。” “怎么了鱼喉?”我有些...
    開封第一講書人閱讀 156,966評(píng)論 0 347
  • 文/不壞的土叔 我叫張陵秀鞭,是天一觀的道長趋观。 經(jīng)常有香客問我,道長锋边,這世上最難降的妖魔是什么皱坛? 我笑而不...
    開封第一講書人閱讀 56,432評(píng)論 1 283
  • 正文 為了忘掉前任,我火速辦了婚禮豆巨,結(jié)果婚禮上剩辟,老公的妹妹穿的比我還像新娘。我一直安慰自己往扔,他們只是感情好贩猎,可當(dāng)我...
    茶點(diǎn)故事閱讀 65,519評(píng)論 6 385
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著萍膛,像睡著了一般吭服。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上蝗罗,一...
    開封第一講書人閱讀 49,792評(píng)論 1 290
  • 那天艇棕,我揣著相機(jī)與錄音,去河邊找鬼串塑。 笑死沼琉,一個(gè)胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的桩匪。 我是一名探鬼主播打瘪,決...
    沈念sama閱讀 38,933評(píng)論 3 406
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場(chǎng)噩夢(mèng)啊……” “哼吸祟!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起桃移,我...
    開封第一講書人閱讀 37,701評(píng)論 0 266
  • 序言:老撾萬榮一對(duì)情侶失蹤屋匕,失蹤者是張志新(化名)和其女友劉穎,沒想到半個(gè)月后借杰,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體过吻,經(jīng)...
    沈念sama閱讀 44,143評(píng)論 1 303
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 36,488評(píng)論 2 327
  • 正文 我和宋清朗相戀三年蔗衡,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了纤虽。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 38,626評(píng)論 1 340
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡绞惦,死狀恐怖逼纸,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情济蝉,我是刑警寧澤杰刽,帶...
    沈念sama閱讀 34,292評(píng)論 4 329
  • 正文 年R本政府宣布菠发,位于F島的核電站,受9級(jí)特大地震影響贺嫂,放射性物質(zhì)發(fā)生泄漏滓鸠。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 39,896評(píng)論 3 313
  • 文/蒙蒙 一第喳、第九天 我趴在偏房一處隱蔽的房頂上張望糜俗。 院中可真熱鬧,春花似錦曲饱、人聲如沸悠抹。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,742評(píng)論 0 21
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽锌钮。三九已至,卻和暖如春引矩,著一層夾襖步出監(jiān)牢的瞬間梁丘,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 31,977評(píng)論 1 265
  • 我被黑心中介騙來泰國打工旺韭, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留氛谜,地道東北人。 一個(gè)月前我還...
    沈念sama閱讀 46,324評(píng)論 2 360
  • 正文 我出身青樓区端,卻偏偏與公主長得像值漫,于是被迫代替她去往敵國和親。 傳聞我的和親對(duì)象是個(gè)殘疾皇子织盼,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 43,494評(píng)論 2 348

推薦閱讀更多精彩內(nèi)容