WCDB for Android

WCDB for Android

前言

最近自己項(xiàng)目記錄數(shù)據(jù)庫(kù)有用戶反饋數(shù)據(jù)會(huì)丟失秘通,我們一直都沒(méi)找到初步原因股淡,因此也是懷疑部分用戶數(shù)據(jù)庫(kù)損壞導(dǎo)致声邦,查看了下sqlite官網(wǎng)的說(shuō)法有導(dǎo)致?lián)p壞db文件的如下幾點(diǎn)原因:

  • 文件錯(cuò)寫
  • 文件鎖 bug
  • 文件 sync 失敗
  • 設(shè)備損壞
  • 內(nèi)存覆蓋
  • 操作系統(tǒng) bug
  • SQLite bug

具體的大家可以看下這篇文章:微信客戶端SQLite數(shù)據(jù)庫(kù)損壞修復(fù)實(shí)踐
因此我們才會(huì)調(diào)研考慮要不要使用微信自己出的這個(gè)WCDB數(shù)據(jù)庫(kù)编丘,下面先具體的講解下WCDB

具體的功能

  • 基于SQLCipher的數(shù)據(jù)庫(kù)加密
  • 使用連接池實(shí)現(xiàn)并發(fā)讀寫
  • Reparir Kit工具類用于修復(fù)損壞數(shù)據(jù)庫(kù)
  • 針對(duì)占用空間大小優(yōu)化的數(shù)據(jù)庫(kù)備份和恢復(fù)功能
  • 日志輸出重定向和性能跟蹤接口
  • 內(nèi)建用于全文搜索的mmicu FTS3/4的分詞器

接入

在build.gradle下面配置

dependencies {
    ...
    compile 'com.tencent.wcdb:wcdb-android:1.0.2'
}

選擇接入的CPU架構(gòu)挽牢,WCDB包含 armeabi, armeabi-v7a, arm64-v8a, x86四種架構(gòu)的動(dòng)態(tài)庫(kù)谱煤,具體的就想用哪個(gè)用哪個(gè)了具體配置在build.gradle:

android {
    defaultConfig {

        ...

        ndk {
            // 接入 armeabi ,armeabi-v7a ,x86
            abiFilters 'armeabi', 'armeabi-v7a','x86'
        }
    }
}

加密:WCDB在android上語(yǔ)法和官方再帶的sqlite是一樣的,記得導(dǎo)包的時(shí)候引用tencent的禽拔,下面開(kāi)始看一個(gè)具體的列子:

import android.content.Context;

import com.tencent.wcdb.DatabaseErrorHandler;
import com.tencent.wcdb.database.SQLiteCipherSpec;
import com.tencent.wcdb.database.SQLiteDatabase;
import com.tencent.wcdb.database.SQLiteOpenHelper;


public class DBHelper extends SQLiteOpenHelper {

    static final String DATABASE_NAME = "test-repair.db";
    static final int DATABASE_VERSION = 1;

    static final byte[] PASSPHRASE = "testkey".getBytes();

    // The test database is taken from SQLCipher test-suit.
    //
    // To be compatible with databases created by the official SQLCipher
    // library, a SQLiteCipherSpec must be specified with page size of
    // 1024 bytes.
    static final SQLiteCipherSpec CIPHER_SPEC = new SQLiteCipherSpec()
            .setPageSize(1024);


    // We don't want corrupted databases get deleted or renamed on this sample,
    // so use an empty DatabaseErrorHandler.
    static final DatabaseErrorHandler ERROR_HANDLER = new DatabaseErrorHandler() {
        @Override
        public void onCorruption(SQLiteDatabase dbObj) {
            // Do nothing
        }
    };

    public DBHelper(Context context) {

        super(context, DATABASE_NAME, null, CIPHER_SPEC, null,
                DATABASE_VERSION, ERROR_HANDLER);
//        super(context,DATABASE_NAME,null,DATABASE_VERSION);
    }

    @Override
    public void onCreate(SQLiteDatabase db) {
        db.execSQL("CREATE TABLE t1(a,b);");

        // OPTIONAL: backup master info for corruption recovery.
        // However, we want to test recovery feature, so omit backup here.

        //RepairKit.MasterInfo.save(db, db.getPath() + "-mbak", PASSPHRASE);
    }

    @Override
    public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) {
        // Do nothing.
    }
}
  • 也是繼承SQLiteOpenHelper去做事情刘离。WCDB 使用了 SQLCipher 的 C 層庫(kù)室叉,但沒(méi)有直接使用 SQLCipher Android 的封裝層。SQLCipher Android 封裝層中很多設(shè)置需要手寫 PRAGMA 語(yǔ)句實(shí)現(xiàn)硫惕,比如設(shè)置 KDF 迭代次數(shù)(兼容老版本 SQLCipher DB)太惠、設(shè)置 Page Size 等操作。
  • 構(gòu)造方法中直接傳入一個(gè)byte[]作為密碼加密操作疲憋,很簡(jiǎn)單凿渊,WCDB 將 String 類型的密碼改為 byte[] 類型,可以支持非打印字符作為密碼(比如 hash(user id) 方式)缚柳,原來(lái)字符類型密碼只要轉(zhuǎn)換為 UTF-8 的 byte 數(shù)組即可埃脏,和 SQLCipher Android 兼容。

數(shù)據(jù)遷移

SQLCipher 提供了 sqlcipher_export SQL 函數(shù)用于導(dǎo)出數(shù)據(jù)到掛載的另一個(gè) DB秋忙,可以用于數(shù)據(jù)遷移彩掐。 但這個(gè)函數(shù)用于 Android 的 SQLiteOpenHelper 并不方便。

SQLiteOpenHelper 主要幫助開(kāi)發(fā)者做 Schema 版本管理灰追,通過(guò)它打開(kāi) SQLite 數(shù)據(jù)庫(kù)堵幽,會(huì)讀取 user_version 字段來(lái)判斷是否需要升級(jí),并調(diào)用子類實(shí)現(xiàn)的 onCreate弹澎、onUpgrade 等接口來(lái)完成創(chuàng)建或升級(jí)操作朴下。 sqlcipher_export 由于是導(dǎo)出而非導(dǎo)入,就跟 onCreate 等接口不搭了苦蒿,因?yàn)橐P(guān)閉原來(lái)的 DB殴胧, 打開(kāi)老的 DB,執(zhí)行 export 到新 DB佩迟,再重打開(kāi)团滥。

為了方便使用,WCDB 就做了擴(kuò)展报强,將 sqlcipher_export 擴(kuò)展為可以接受第二個(gè)參數(shù)表示從哪里導(dǎo)出灸姊, 從而實(shí)現(xiàn)了導(dǎo)入,列子看下:

@Override
    public void onCreate(SQLiteDatabase db) {
        // Check whether old plain-text database exists, if so, export it
        // to the new, encrypted one.
        File oldDbFile = mContext.getDatabasePath(OLD_DATABASE_NAME);
        if (oldDbFile.exists()) {

            Log.i(TAG, "Migrating plain-text database to encrypted one.");

            // SQLiteOpenHelper begins a transaction before calling onCreate().
            // We have to end the transaction before we can attach a new database.
            db.endTransaction();

            // Attach old database to the newly created, encrypted database.
            String sql = String.format("ATTACH DATABASE %s AS old KEY '';",
                    DatabaseUtils.sqlEscapeString(oldDbFile.getPath()));
            db.execSQL(sql);

            // Export old database.
            db.beginTransaction();
            //從old舊的數(shù)據(jù)庫(kù)倒出數(shù)據(jù)庫(kù)到main
            DatabaseUtils.stringForQuery(db, "SELECT sqlcipher_export('main', 'old');", null);
            db.setTransactionSuccessful();
            db.endTransaction();

            // Get old database version for later upgrading.
            int oldVersion = (int) DatabaseUtils.longForQuery(db, "PRAGMA old.user_version;", null);

            // Detach old database and enter a new transaction.
            db.execSQL("DETACH DATABASE old;");

            // Old database can be deleted now.
            oldDbFile.delete();

            // Before further actions, restore the transaction.
            db.beginTransaction();

            // Check if we need to upgrade the schema.
            if (oldVersion > DATABASE_VERSION) {
                onDowngrade(db, oldVersion, DATABASE_VERSION);
            } else if (oldVersion < DATABASE_VERSION) {
                onUpgrade(db, oldVersion, DATABASE_VERSION);
            }
        } else {
            Log.i(TAG, "Creating new encrypted database.");

            // Do the real initialization if the old database is absent.
            db.execSQL("CREATE TABLE message (content TEXT, "
                    + "sender TEXT);");
        }

        // OPTIONAL: backup master info for corruption recovery.
        RepairKit.MasterInfo.save(db, db.getPath() + "-mbak", /*mPassphrase.getBytes()*/null);
    }

如此就可以不關(guān)閉原來(lái)的數(shù)據(jù)庫(kù)實(shí)現(xiàn)數(shù)據(jù)導(dǎo)入,可以兼容 SQLiteOpenHelper 的接口了秉溉。

數(shù)據(jù)庫(kù)修復(fù)

Android 接口支持三種修復(fù)方法力惯,如下:

修復(fù)方法 簡(jiǎn)介 相關(guān)接口
Repair Kit 解析 B-tree 修復(fù) RepairKit類
備份恢復(fù) 壓縮備份完整數(shù)據(jù),使用備份數(shù)據(jù)恢復(fù) BackupKit 和 RecoverKit
Dump .dump 命令坚嗜,已廢棄 DBDumpUtil

一夯膀,Repair Kit

使用 Repair Kit 可以直接從損壞的數(shù)據(jù)庫(kù)里盡量讀出未損壞的數(shù)據(jù)诗充,不需要事先準(zhǔn)備苍蔬, 但是先備份 Master 信息可以大大增加恢復(fù)成功率。 如果有意使用 Repair Kit 恢復(fù)數(shù)據(jù)庫(kù)蝴蜓, 建議備份 Master 信息碟绑。Master 信息保存了數(shù)據(jù)庫(kù)的 Schema俺猿,建議每次執(zhí)行完數(shù)據(jù)庫(kù)創(chuàng)建或升級(jí)時(shí)執(zhí)行備份,可以保證備份 是最新的格仲。不修改 Schema 的話 Master 信息不會(huì)改變押袍。如果你使用 SQLiteOpenHelper,最佳 實(shí)踐是在 SQLiteOpenHelper.onCreate(...) 和 SQLiteOpenHelper.onUpgrade(...) 的 最后進(jìn)行備份凯肋。備份 Master 信息只需要調(diào)用 RepairKit.MasterInfo.save(...) 即可谊惭。備份 Master 信息 典型消耗為幾kB ~ 幾十kB,幾毫秒 ~ 幾十毫秒侮东,但如果你有非常非常多的表和索引(萬(wàn)數(shù)量級(jí))圈盔, 這個(gè)過(guò)程可能會(huì)有點(diǎn)慢,建議放在子線程完成.如下:

public class DBHelper extends SQLiteOpenHelper {
    
    public DBHelper(Context context) {
        super(context, DATABASE_NAME, PASSPHRASE, CIPHER_SPEC, null,
                DATABASE_VERSION, ERROR_HANDLER);
    }

    @Override
    public void onCreate(SQLiteDatabase db) {
        // 執(zhí)行 CREATE TABLE 創(chuàng)建 Schema
        db.execSQL("CREATE TABLE t1(a,b);");
        db.execSQL("CREATE TABLE t2(c,d);");
        // ......

        // 備份 Master 信息
        RepairKit.MasterInfo.save(db, db.getPath() + "-mbak", BACKUP_PASSPHRASE);
    }

    @Override
    public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) {
        // 執(zhí)行升級(jí)
        db.execSQL("ALTER TABLE t1 ADD COLUMN x TEXT;");

        // 備份 Master 信息
        RepairKit.MasterInfo.save(db, db.getPath() + "-mbak", BACKUP_PASSPHRASE);
    }
}

二悄雅,恢復(fù)損壞數(shù)據(jù)庫(kù)

恢復(fù)損壞數(shù)據(jù)庫(kù)驱敲,首先加載之前備份的 Master 信息(如果有)。

RepairKit.MasterInfo master = RepairKit.MasterInfo.load('/path/to/database.db-mbak', 
        BACKUP_PASSPHRASE, null);
if (master == null) {
    // 加載不成功宽闲,可能是不存在或者損壞
}

使用 RepairKit 打開(kāi)損壞的數(shù)據(jù)庫(kù)众眨,使用 SQLiteDatabase 打開(kāi)新的數(shù)據(jù)庫(kù),調(diào)用 output(...) 即可將損壞數(shù)據(jù)庫(kù)的內(nèi)容轉(zhuǎn)移到新數(shù)據(jù)庫(kù)容诬。

RepairKit repair = new RepairKit(
        "/path/to/corrupted.db" // 損壞的數(shù)據(jù)庫(kù)文件
        PASSPHRASE,             // 數(shù)據(jù)庫(kù)密鑰(不是備份文件密鑰)
        CIPHER_SPEC,            // 加密描述娩梨,與打開(kāi)DB時(shí)一樣
        master                  // 之前加載的 Master 信息
);

SQLiteDatabase newDb = SQLiteDatabase.openOrCreateDatabase(...);
// 打開(kāi)新DB用于承載恢復(fù)數(shù)據(jù),是否加密沒(méi)所謂

boolean result = repair.output(newDb, 0);
// 輸出恢復(fù)數(shù)據(jù)到新DB

if (!result) {
    // 恢復(fù)失敗
}

repair.release();
// 最后要 release 釋放資源

恢復(fù)的過(guò)程需時(shí)較長(zhǎng)览徒,請(qǐng)務(wù)必在子線程完成姚建,如數(shù)據(jù)庫(kù)較大請(qǐng)考慮持有 Wake Lock。

三吱殉,選擇性恢復(fù)

Repair Kit 可以只恢復(fù)一部分表掸冤,只需要在 MasterInfo.load(...) 或者 MasterInfo.make(...) 里指定白名單即可。

// 白名單友雳,只有白名單里列到的表才會(huì)恢復(fù)稿湿,表對(duì)應(yīng)的索引也會(huì)相應(yīng)恢復(fù)
String[] tables = new String[] {
    "t1", "t2"      // 只恢復(fù) t1 和 t2 兩個(gè)表
};
RepairKit.MasterInfo master = RepairKit.MasterInfo.load('/path/to/database.db-mbak', 
        BACKUP_PASSPHRASE, tables);

日志重定向與性能監(jiān)控

SQLite 和 WCDB 框架在運(yùn)行中會(huì)產(chǎn)生日志,這些日志默認(rèn)會(huì)打印到系統(tǒng)日志(logcat)押赊,但這可能不是 所有開(kāi)發(fā)者都希望的行為饺藤。比如擔(dān)心日志里帶有敏感信息,直接輸出到系統(tǒng)不妥流礁,或者希望將日志寫到文件 用于上報(bào)和分析涕俗,WCDB 提供接口來(lái)完成日志重定向。使用情況:

//不打印任何日志
Log.setLogger(Log.LOGGER_NONE);
//或者自定義日志
Log.setLogger(new Log.LogCallback() {
    @Override
    public void println(int priority, String tag, String msg) {
      //處理日志

    }
});

WCDB 還提供了性能監(jiān)控接口 SQLiteTrace神帅,實(shí)現(xiàn)接口并綁定到 SQLiteDatabase 可以在每次 執(zhí)行 SQL 語(yǔ)句或連接池?fù)矶碌臅r(shí)候得到回調(diào)

SQLiteTrace trace=new SQLiteTrace() {
            @Override
            public void onSQLExecuted(SQLiteDatabase db, String sql, int type, long time) {
                //每次之行完一條sql的語(yǔ)句執(zhí)行的回調(diào)
            }

            @Override
            public void onConnectionObtained(SQLiteDatabase db, String sql, long waitTime, boolean isPrimary) {
                //從連接池獲得了鏈接成功
            }

            @Override
            public void onConnectionPoolBusy(SQLiteDatabase db, String sql, List<String> requests, String message) {
                //等待連接池超過(guò)3秒的回調(diào)再姑,因?yàn)榇嬖趧e的操作占用著連接池
            }

            @Override
            public void onDatabaseCorrupted(SQLiteDatabase db) {
                //數(shù)據(jù)庫(kù)損壞時(shí)回調(diào)
            }
        };
        mDB.setTraceCallback(trace);

SQLiteDatabase 也開(kāi)放了 dump 方法,可以打印出數(shù)據(jù)庫(kù)的當(dāng)前狀態(tài)找御,包括連接池內(nèi)所有連接 被持有的狀態(tài)以及最近執(zhí)行的 SQL 語(yǔ)句和耗時(shí)元镀,對(duì)排查性能和死鎖問(wèn)題也有很大幫助绍填。

優(yōu)化 Cursor 實(shí)現(xiàn)

Android 框架查詢數(shù)據(jù)庫(kù)使用的是 Cursor 接口,調(diào)用 SQLiteDatabase.query(...) 會(huì)返回一個(gè)Cursor 對(duì)象栖疑,之后就可以使用 Cursor 遍歷結(jié)果集了讨永。Android SDK SQLite Cursor 的實(shí)現(xiàn)是分配一個(gè)固定 2MB 大小的緩沖區(qū),稱作 Cursor Window遇革,用于存放查詢結(jié)果集卿闹。

查詢時(shí),先分配Cursor Window萝快,然后執(zhí)行 SQL 獲取結(jié)果集填充之比原,直到 Cursor Window 放滿或者遍歷完結(jié)果集,之后將 Cursor 返回給調(diào)用者杠巡。

假如 Cursor 遍歷到緩沖區(qū)以外的行量窘,Cursor 會(huì)丟棄之前緩沖區(qū)的所有內(nèi)容,重新查詢氢拥,跳過(guò)前面的行蚌铜,重新選定一個(gè)開(kāi)始位置填充 Cursor Window 直到緩沖區(qū)再次填滿或遍歷完結(jié)果集。
這樣的實(shí)現(xiàn)能保證大部分情況正常工作嫩海,在很多情況下卻不是最優(yōu)實(shí)現(xiàn)冬殃。微信對(duì) DB 操作最多的場(chǎng)景是獲取 Cursor 直接遍歷獲取數(shù)據(jù)后關(guān)閉,獲取到的數(shù)據(jù)叁怪,一般是生成對(duì)應(yīng)的實(shí)體對(duì)象(通過(guò) ORM 或者自行從 Cursor 轉(zhuǎn)換)后放到 List 或 Map 等容器里返回审葬,或用于顯示,或用于其他邏輯奕谭。

在這種場(chǎng)景下涣觉,先將數(shù)據(jù)保存到 Cursor Window 后再取出,中間要經(jīng)歷兩次內(nèi)存拷貝和轉(zhuǎn)換(SQLite → CursorWindow → Java)血柳,這是完全沒(méi)有必要的官册。另外,由于 Cursor Window 是定長(zhǎng)的难捌,對(duì)于較小的結(jié)果集膝宁,需要無(wú)故分配 2MB 內(nèi)存,對(duì)于大結(jié)果集根吁,如果 2MB 不足以放下员淫,遍歷到途中還會(huì)引發(fā) Cursor 重查詢,這個(gè)消耗就相當(dāng)大了击敌。

Cursor Window介返,其實(shí)也是在 JNI 層通過(guò) SQLite 庫(kù)的 Statement 填充的,Statement 這里可以理解為一個(gè)輕量但只能往前遍歷愚争,沒(méi)有緩存的 Cursor映皆。這個(gè)不就跟我們的場(chǎng)景一致嗎挤聘?何不直接使用底層的 Statement 呢轰枝?我們對(duì) Statement 做了簡(jiǎn)單的封裝捅彻,暴露了 Cursor 接口, SQLiteDirectCursor 就誕生了鞍陨,它直接操作底層 SQLite 獲取數(shù)據(jù)步淹,只能執(zhí)行往前迭代的操作,但這完全滿足需要诚撵。

 com.tencent.wcdb.Cursor cursor=mDB.rawQueryWithFactory(SQLiteDirectCursor.FACTORY,sql,null);
        try {
            while (cursor.moveToNext()) {
                //處理數(shù)據(jù)
            }
        }catch (Exception e){
            e.printStackTrace();
        }

在大部分不需要將 Cursor 傳遞出去的場(chǎng)景缭裆,能很好的解決 Cursor 的額外消耗,特別是結(jié)果集大于 2MB 的場(chǎng)合寿烟。

以上就是我自己對(duì)WCDB的理解總結(jié)澈驼,很多知識(shí)點(diǎn)都是看微信WCDB官網(wǎng)上的知識(shí),大家可以自己去看下。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末筛武,一起剝皮案震驚了整個(gè)濱河市缝其,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌徘六,老刑警劉巖内边,帶你破解...
    沈念sama閱讀 211,265評(píng)論 6 490
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場(chǎng)離奇詭異待锈,居然都是意外死亡漠其,警方通過(guò)查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 90,078評(píng)論 2 385
  • 文/潘曉璐 我一進(jìn)店門竿音,熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái)和屎,“玉大人,你說(shuō)我怎么就攤上這事春瞬】袅” “怎么了?”我有些...
    開(kāi)封第一講書人閱讀 156,852評(píng)論 0 347
  • 文/不壞的土叔 我叫張陵快鱼,是天一觀的道長(zhǎng)颠印。 經(jīng)常有香客問(wèn)我,道長(zhǎng)抹竹,這世上最難降的妖魔是什么线罕? 我笑而不...
    開(kāi)封第一講書人閱讀 56,408評(píng)論 1 283
  • 正文 為了忘掉前任,我火速辦了婚禮窃判,結(jié)果婚禮上钞楼,老公的妹妹穿的比我還像新娘。我一直安慰自己袄琳,他們只是感情好询件,可當(dāng)我...
    茶點(diǎn)故事閱讀 65,445評(píng)論 5 384
  • 文/花漫 我一把揭開(kāi)白布燃乍。 她就那樣靜靜地躺著,像睡著了一般宛琅。 火紅的嫁衣襯著肌膚如雪刻蟹。 梳的紋絲不亂的頭發(fā)上,一...
    開(kāi)封第一講書人閱讀 49,772評(píng)論 1 290
  • 那天嘿辟,我揣著相機(jī)與錄音舆瘪,去河邊找鬼。 笑死红伦,一個(gè)胖子當(dāng)著我的面吹牛英古,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播昙读,決...
    沈念sama閱讀 38,921評(píng)論 3 406
  • 文/蒼蘭香墨 我猛地睜開(kāi)眼召调,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼!你這毒婦竟也來(lái)了蛮浑?” 一聲冷哼從身側(cè)響起唠叛,我...
    開(kāi)封第一講書人閱讀 37,688評(píng)論 0 266
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤,失蹤者是張志新(化名)和其女友劉穎陵吸,沒(méi)想到半個(gè)月后玻墅,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 44,130評(píng)論 1 303
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡壮虫,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 36,467評(píng)論 2 325
  • 正文 我和宋清朗相戀三年澳厢,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片囚似。...
    茶點(diǎn)故事閱讀 38,617評(píng)論 1 340
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡剩拢,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出饶唤,到底是詐尸還是另有隱情徐伐,我是刑警寧澤,帶...
    沈念sama閱讀 34,276評(píng)論 4 329
  • 正文 年R本政府宣布募狂,位于F島的核電站办素,受9級(jí)特大地震影響,放射性物質(zhì)發(fā)生泄漏祸穷。R本人自食惡果不足惜性穿,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 39,882評(píng)論 3 312
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望雷滚。 院中可真熱鬧需曾,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開(kāi)封第一講書人閱讀 30,740評(píng)論 0 21
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)。三九已至谋减,卻和暖如春牡彻,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背逃顶。 一陣腳步聲響...
    開(kāi)封第一講書人閱讀 31,967評(píng)論 1 265
  • 我被黑心中介騙來(lái)泰國(guó)打工讨便, 沒(méi)想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留充甚,地道東北人以政。 一個(gè)月前我還...
    沈念sama閱讀 46,315評(píng)論 2 360
  • 正文 我出身青樓,卻偏偏與公主長(zhǎng)得像伴找,于是被迫代替她去往敵國(guó)和親盈蛮。 傳聞我的和親對(duì)象是個(gè)殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 43,486評(píng)論 2 348

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

  • Android 自定義View的各種姿勢(shì)1 Activity的顯示之ViewRootImpl詳解 Activity...
    passiontim閱讀 171,749評(píng)論 25 707
  • 我的辦公室旁邊,是醫(yī)院的康復(fù)科衰倦。來(lái)這里康復(fù)的大多是中風(fēng)后遺癥的中老年人袒炉。輪椅、拐杖和一張憂郁的臉樊零,是這里病人的標(biāo)配...
    白立平閱讀 1,334評(píng)論 0 13
  • 本人原創(chuàng)我磁,不得轉(zhuǎn)載。 前語(yǔ):撤訴驻襟、調(diào)解案子幾分鐘內(nèi)完成裁判文書及各類需當(dāng)事人簽署文件的制作與打印夺艰,當(dāng)場(chǎng)送達(dá)結(jié)案事了...
    panjiandong閱讀 1,727評(píng)論 0 2
  • 長(zhǎng)長(zhǎng)的隧道 幽森 看不到盡頭 無(wú)聲角落默默端坐 這樣的我 永遠(yuǎn)走不到你的心里嗎 給你我愛(ài)的黑膠唱片 沙啞破舊依然用...
    雪小凝閱讀 194評(píng)論 0 0
  • “吃胡蘿卜吧,對(duì)視力好沉衣!”“我不喜歡吃郁副,我也不想看很遠(yuǎn)⊥阆埃” “吃胡蘿卜吧存谎,眼睛有力氣,看電視才不累肥隆!”“眼睛太有力...
    寶寶邦社區(qū)閱讀 463評(píng)論 0 3