Android —— 微信Sqlite數(shù)據(jù)庫(kù)框架WCDB學(xué)習(xí)

學(xué)習(xí)資料

數(shù)據(jù)庫(kù)软驰,重災(zāi)區(qū)之一鳍烁,除了Java基礎(chǔ)外也是以后的重點(diǎn)加強(qiáng)學(xué)習(xí)模塊蹲缠。4月入職以來(lái),終于空閑兩天症概,就學(xué)習(xí)了解下

項(xiàng)目中要求對(duì)Sqlite數(shù)據(jù)庫(kù)進(jìn)行加密,百度之后早芭,知道了SQLCipher這個(gè)東西彼城。過(guò)了沒(méi)幾天,看到微信團(tuán)隊(duì)開(kāi)源的WCDB退个,簡(jiǎn)單看了簡(jiǎn)介后募壕,了解到支持加密,就打算學(xué)習(xí)下怎么使用的语盈,以后再遇到需要對(duì)Sqlite加密的需求舱馅,就考慮使用

感覺(jué)WCDB可以看作是SQLCipher的一個(gè)加強(qiáng)升級(jí)版本,除了加密外刀荒,還有一個(gè)牛B的地方是支持?jǐn)?shù)據(jù)庫(kù)修復(fù)代嗤,其他的可以去wiki看看

本篇是記錄學(xué)習(xí)接入流程棘钞,以及簡(jiǎn)單地由不加密的數(shù)據(jù)庫(kù)遷移到加密的數(shù)據(jù)


1. 接入

接入使用很簡(jiǎn)單很方便

我用的版本是1.0.2

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è)了

關(guān)于.so文件兼容可以看看Android SO文件的兼容和適配

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

日常使用的手機(jī)沒(méi)root,為了看到.db文件宜猜,就使用了Android Studio自帶的模擬器,也就引入了x86


1.1 WCDB DBHelper

WCDB的類名方法名硝逢,基本和Android原生提供的一樣姨拥,可以按照以前的使用習(xí)慣來(lái)來(lái)使用

注意導(dǎo)入包時(shí),要導(dǎo)入WCDB的包


1.1.1 PlainDBHelper

直接繼承WCDB包下的SQLiteOpenHelper

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

import java.io.File;

/**
 * 簡(jiǎn)單的 SQLite Helper
 */
public class PlainDBHelper extends SQLiteOpenHelper {
    // 數(shù)據(jù)庫(kù) db 文件名稱
    private static final String DEFAULT_NAME = "plain.db";

    // 默認(rèn)版本號(hào)
    private static final int DEFAULT_VERSION = 1;

    private Context mContext;

    /**
     * 通過(guò)父類構(gòu)造方法創(chuàng)建 plain 數(shù)據(jù)庫(kù)
     */
    public PlainDBHelper(Context context) {
        super(context, DEFAULT_NAME, null, DEFAULT_VERSION, null);
        this.mContext = context;
    }

    /**
     * 表創(chuàng)建
     */
    @Override
    public void onCreate(SQLiteDatabase db) {
        final String SQL_CREATE = "CREATE TABLE IF NOT EXISTS person (_id INTEGER PRIMARY KEY AUTOINCREMENT , name VARCHAR(20) , address TEXT)";
        db.execSQL(SQL_CREATE);
    }

    /**
     * 版本升級(jí)
     */
    @Override
    public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) {
        // TODO
    }

    /**
     * 刪除數(shù)據(jù)庫(kù) db 文件
     */
    public boolean onDelete() {
        File file = mContext.getDatabasePath(DEFAULT_NAME);
        return SQLiteDatabase.deleteDatabase(file);
    }
}

重點(diǎn)在于構(gòu)造方法中渠鸽,super()方法叫乌。若一開(kāi)始就想創(chuàng)建一個(gè)加密的數(shù)據(jù),選擇對(duì)應(yīng)的super()方法徽缚,傳入一個(gè)密碼就可以

之后在onCreate()中憨奸,執(zhí)行創(chuàng)建person表語(yǔ)句


1.1.2 PlainDBManager

簡(jiǎn)單的數(shù)據(jù)操作管理類,可以將增刪改查的一些操作統(tǒng)一放在這個(gè)類里

public class PlainDBManager {
    private PlainDBHelper mDBHelper;
    private SQLiteDatabase mDB;

    public PlainDBManager(Context context) {
        mDBHelper = new PlainDBHelper(context);
        mDB = mDBHelper.getWritableDatabase();
    }

    public void addPersonData(PlainPerson person) {
        try {
            // 開(kāi)啟事務(wù)
            mDB.beginTransaction();

            // 執(zhí)行插入語(yǔ)句
            final String sql = "INSERT INTO person VALUES(NULL,?,?)";
            Object[] objects = new Object[]{person.getName(), person.getAddress()};
            mDB.execSQL(sql, objects);

            // 設(shè)置事務(wù)完成成功
            mDB.setTransactionSuccessful();
        } finally {
            // 關(guān)閉事務(wù)
            mDB.endTransaction();
        }
    }

    public boolean addPersonList(List<PlainPerson> list) {
        try {
            // 開(kāi)啟事務(wù)
            mDB.beginTransaction();

            // 執(zhí)行插入語(yǔ)句
            for (PlainPerson person : list) {
                Object[] objects = new Object[]{person.getName(), person.getAddress()};
                final String sql = "INSERT INTO person VALUES(NULL,?,?)";
                mDB.execSQL(sql, objects);
            }

            // 設(shè)置事務(wù)完成成功
            mDB.setTransactionSuccessful();
        } catch (Exception e) {
            return false;
        } finally {
            // 關(guān)閉事務(wù)
            mDB.endTransaction();
        }
        return true;
    }

    /**
     * 拿到數(shù)據(jù)庫(kù)中所有的Person并放入集合中
     */
    public List<PlainPerson> getPersonListData() {
        List<PlainPerson> listData = new ArrayList<>();
        Cursor c = getAllPersonInfo();
        while (c.moveToNext()) {
            PlainPerson person = new PlainPerson();
            person.setName(c.getString(c.getColumnIndex("name")));
            person.setAddress(c.getString(c.getColumnIndex("address")));
            listData.add(person);
        }
        c.close();
        return listData;
    }

    private Cursor getAllPersonInfo() {
        return mDB.rawQuery("SELECT * FROM person", null);
    }

    /**
     * 關(guān)閉  database猎拨;
     */
    public void closeDB() {
        mDB.close();
    }

    /**
     * 刪除數(shù)據(jù)庫(kù)
     */
    public Boolean deleteDatabase() {
        return mDBHelper.onDelete();
    }
}

關(guān)于插入查詢的語(yǔ)句如何進(jìn)行優(yōu)化膀藐,希望知道的同學(xué)可以留言告訴一下


1.2 Activity中使用

創(chuàng)建表時(shí)屠阻,隨意創(chuàng)建了一個(gè)person表红省,字段就是name,address

Activity

Activity代碼

/**
 * 原始:未加密的數(shù)據(jù)庫(kù)
 */
public class PlainDBActivity extends AppCompatActivity {
    private final String TAG = PlainDBActivity.class.getSimpleName();
    // 數(shù)據(jù)庫(kù)操作管理類
    private PlainDBManager mDBManager;

    // 適配器
    private RecyclerAdapter mAdapter;

    // 顯示數(shù)據(jù)按鈕
    private Button mBtShow;

    // 插入按鈕
    private Button mBtInsert;

    // 刪除按鈕
    private Button mBtDelete;

    // 是否進(jìn)行了刪除操作
    private Boolean isHasDeleted = false;

    private ProgressDialog mDialog;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_plain_db);
        initDB();
        initView();
    }

    private void initDB() {
        mDBManager = new PlainDBManager(PlainDBActivity.this);
    }

    private void initView() {
        // RecyclerView
        RecyclerView rv = (RecyclerView) findViewById(R.id.activity_plain_rv);
        rv.addItemDecoration(new DividerItemDecoration(PlainDBActivity.this, DividerItemDecoration.VERTICAL));

        LinearLayoutManager manager = new LinearLayoutManager(PlainDBActivity.this);
        rv.setLayoutManager(manager);

        mAdapter = new RecyclerAdapter(rv, R.layout.item_layout);
        rv.setAdapter(mAdapter);

        // 插入按鈕
        mBtInsert = (Button) findViewById(R.id.activity_plain_bt_insert);

        // 顯示按鈕
        mBtShow = (Button) findViewById(R.id.activity_plain_bt_show);

        // 刪除數(shù)據(jù)庫(kù)
        mBtDelete = (Button) findViewById(R.id.activity_plain_bt_delete);
        
        setOnClick();
    }

    private void setOnClick() {
        // 插入按鈕:5秒內(nèi),防止重復(fù)點(diǎn)擊
        RxView
                .clicks(mBtInsert)
                .throttleFirst(5, TimeUnit.SECONDS)
                .subscribe(new Consumer<Object>() {
                    @Override
                    public void accept(@NonNull Object o) throws Exception {
                        if (isHasDeleted) {
                            initDB();
                        }
                        addDataIntoSql();
                    }
                });

        // 顯示按鈕:1秒內(nèi),防止重復(fù)點(diǎn)擊
        RxView
                .clicks(mBtShow)
                .throttleFirst(1, TimeUnit.SECONDS)
                .subscribe(new Consumer<Object>() {
                    @Override
                    public void accept(@NonNull Object o) throws Exception {
                        if (isHasDeleted) {
                            initDB();
                        }
                        selectDataFromSql();
                    }
                });


        // 刪除按鈕:3秒內(nèi)国觉,防止重復(fù)點(diǎn)擊
        RxView
                .clicks(mBtDelete)
                .throttleFirst(3, TimeUnit.SECONDS)
                .subscribe(new Consumer<Object>() {
                    @Override
                    public void accept(@NonNull Object o) throws Exception {
                        if (isHasDeleted) {
                            toast("數(shù)據(jù)庫(kù)不存在");
                            return;
                        }

                        if (mDBManager.deleteDatabase()) {
                            isHasDeleted = true;
                            mDBManager.closeDB();
                            toast("刪除成功");
                        }
                    }
                });
    }

    /**
     * 查詢數(shù)據(jù)吧恃,并顯示
     */
    private void selectDataFromSql() {
        Observable
                .just(0)
                .subscribeOn(Schedulers.io())
                .doOnSubscribe(new Consumer<Disposable>() {
                    @Override
                    public void accept(@NonNull Disposable disposable) throws Exception {
                        showProgressDialog("正在查詢數(shù)據(jù)...");
                    }
                })
                .subscribeOn(AndroidSchedulers.mainThread())
                .map(new Function<Integer, List<PlainPerson>>() {
                    @Override
                    public List<PlainPerson> apply(@NonNull Integer integer) throws Exception {
                        return mDBManager.getPersonListData();
                    }
                })
                .filter(new Predicate<List<PlainPerson>>() {
                    @Override
                    public boolean test(@NonNull List<PlainPerson> plainList) throws Exception {
                        return plainList.size() > 0;
                    }
                })
                .observeOn(AndroidSchedulers.mainThread())
                .doFinally(new Action() {
                    @Override
                    public void run() throws Exception {
                        closeProgressDialog();
                    }
                })
                .subscribe(new Consumer<List<PlainPerson>>() {
                    @Override
                    public void accept(@NonNull List<PlainPerson> plainList) throws Exception {
                        mAdapter.setData(plainList);
                    }
                });
    }


    /**
     * 存入數(shù)據(jù)
     */
    private void addDataIntoSql() {
        Observable
                .just(10000)
                .subscribeOn(Schedulers.io())
                .doOnSubscribe(new Consumer<Disposable>() {
                    @Override
                    public void accept(@NonNull Disposable disposable) throws Exception {
                        mBtShow.setEnabled(false);
                        showProgressDialog("正在插入數(shù)據(jù)...");
                    }
                })
                .subscribeOn(AndroidSchedulers.mainThread())
                .map(new Function<Integer, List<PlainPerson>>() {
                    @Override
                    public List<PlainPerson> apply(@NonNull Integer integer) throws Exception {
                        List<PlainPerson> list = new ArrayList<>();
                        for (int i = 0; i < integer; i++) {
                            PlainPerson person = new PlainPerson();
                            person.setName("隔壁老王" + i);
                            person.setAddress("天使大街 " + i + " 號(hào)");
                            list.add(person);
                        }
                        return list;
                    }
                })
                .map(new Function<List<PlainPerson>, Boolean>() {
                    @Override
                    public Boolean apply(@NonNull List<PlainPerson> plainList) throws Exception {
                        return mDBManager.addPersonList(plainList);
                    }
                })
                .observeOn(AndroidSchedulers.mainThread())
                .doFinally(new Action() {
                    @Override
                    public void run() throws Exception {
                        closeProgressDialog();
                    }
                })
                .subscribe(new Consumer<Boolean>() {
                    @Override
                    public void accept(@NonNull Boolean aBoolean) throws Exception {
                        if (aBoolean) {
                            mBtShow.setEnabled(true);
                            toast("插入成功");
                        }
                    }
                });
    }

    /**
     * 關(guān)閉 ProgressDialog
     */
    private void closeProgressDialog() {
        if (null != mDialog && mDialog.isShowing()) {
            mDialog.dismiss();
        }
    }

    /**
     * 顯示 ProgressDialog
     */
    private void showProgressDialog(String info) {
        final String TITLE = "提示";
        mDialog = ProgressDialog.show(PlainDBActivity.this, TITLE, info);
        mDialog.show();
    }


    private void toast(String info) {
        Toast.makeText(PlainDBActivity.this, info, Toast.LENGTH_SHORT).show();
    }

    @Override
    protected void onDestroy() {
        super.onDestroy();
        mDBManager.closeDB();
    }
}

按鈕的點(diǎn)擊,試著用了下RxBinding麻诀,插入數(shù)據(jù)時(shí)痕寓,插入了10000條數(shù)據(jù)

錯(cuò)誤:下面這段話說(shuō)明的問(wèn)題之前搞錯(cuò)了

嘗試了下插入100w條也可以,速度還可以接受蝇闭。但當(dāng)試著插入1億條字符串時(shí)呻率,就報(bào)了OOM,不知道如何解決呻引。PlainDBHelper中插入數(shù)據(jù)的方法需要進(jìn)行優(yōu)化


1.3 補(bǔ)充:之前的錯(cuò)誤說(shuō)明

2017年7月6號(hào) 20:17

今天看到一篇博客 一個(gè)Java對(duì)象到底占用多大內(nèi)存礼仗?,突然想到逻悠,上面說(shuō)的OOM問(wèn)題元践,可能就不是數(shù)據(jù)庫(kù)插入操作導(dǎo)致的,問(wèn)題出在:

.map(new Function<Integer, List<PlainPerson>>() {
    @Override
    public List<PlainPerson> apply(@NonNull Integer integer) throws Exception {
         List<PlainPerson> list = new ArrayList<>();
             for (int i = 0; i < integer; i++) {
                    PlainPerson person = new PlainPerson();
                    person.setName("隔壁老王" + i);
                    person.setAddress("天使大街 " + i + " 號(hào)");
                    list.add(person);
              }
              return list;
          }
})

這里童谒,創(chuàng)建了大量對(duì)象单旁,大量的對(duì)象占用過(guò)多的內(nèi)存,導(dǎo)致的OOM饥伊,代碼根本就沒(méi)有走到數(shù)據(jù)庫(kù)插入操作就已經(jīng)發(fā)生了OOM

為了驗(yàn)證象浑,我將代碼做了修改蔫饰,just(10000000),創(chuàng)建1000w個(gè)對(duì)象愉豺,并把數(shù)據(jù)庫(kù)插入操作注釋掉死嗦,沒(méi)有進(jìn)行數(shù)據(jù)庫(kù)任何操作,依然OOM,也驗(yàn)證了我的想法粒氧,是創(chuàng)建對(duì)象過(guò)多導(dǎo)致的OOM而無(wú)關(guān)數(shù)據(jù)庫(kù)操作

至于數(shù)據(jù)庫(kù) 大量操作會(huì)不會(huì)造成OOM越除,暫時(shí)不知道如何驗(yàn)證


2. 遷移

由非加密的數(shù)據(jù)庫(kù)遷移到加密的數(shù)據(jù)庫(kù),實(shí)際開(kāi)發(fā)時(shí)外盯,一定要先做好備份

2.1 EncryptDBHelper

/**
 * 將不加密的 plain.db 遷移到加密的 encrypt.db
 */
public class EncryptDBHelper extends SQLiteOpenHelper {
    private final String TAG = EncryptDBHelper.class.getSimpleName();
    private final static String ENCRYPT_NAME = "encrypt.db";
    private final static String PLAIN_NAME = "plain.db";
    private final static int VERSION = 2;
    private Context mContext;

    public EncryptDBHelper(Context context, String password) {
        super(context, ENCRYPT_NAME, password.getBytes(), null, VERSION, null);
        mContext = context;
    }

    @Override
    public void onCreate(SQLiteDatabase db) {
        File plainFile = mContext.getDatabasePath(PLAIN_NAME);

        // 判斷舊的數(shù)據(jù)庫(kù)文件是否存在
        if (plainFile.exists()) {
            move(plainFile, db);
        } else {
            final String SQL_CREATE = "CREATE TABLE IF NOT EXISTS person (_id INTEGER PRIMARY KEY AUTOINCREMENT , name VARCHAR(20) , address TEXT)";
            db.execSQL(SQL_CREATE);
        }
    }

    /**
     * 遷移數(shù)據(jù)庫(kù)
     */
    private void move(File file, SQLiteDatabase db) {
        db.endTransaction();

        String sql = String.format("ATTACH DATABASE %s AS old KEY '';",
                DatabaseUtils.sqlEscapeString(file.getPath()));
        db.execSQL(sql);

        db.beginTransaction();
        DatabaseUtils.stringForQuery(db, "SELECT sqlcipher_export('main', 'old');", null);
        db.setTransactionSuccessful();
        db.endTransaction();

        int oldVersion = (int) DatabaseUtils.longForQuery(db, "PRAGMA old.user_version;", null);
        db.execSQL("DETACH DATABASE old;");

        if (file.delete()) {
            Log.e(TAG, "舊數(shù)據(jù)庫(kù)文件刪除成功");
        }

        db.beginTransaction();

        // 是否要更新 schema
        if (oldVersion > VERSION) {
            onDowngrade(db, oldVersion, VERSION);
        } else if (oldVersion < VERSION) {
            onUpgrade(db, oldVersion, VERSION);
        }
    }

    @Override
    public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) {
        Log.e(TAG, "----" + oldVersion + "===> " + newVersion);
    }

    /**
     * 刪除數(shù)據(jù)庫(kù) db 文件
     */
    public boolean onDelete() {
        File file = mContext.getDatabasePath(ENCRYPT_NAME);
        return SQLiteDatabase.deleteDatabase(file);
    }
}

構(gòu)造方法中摘盆,傳入了一個(gè)字符串密碼,加密就是這里簡(jiǎn)單饱苟。孩擂。。

實(shí)際開(kāi)發(fā)時(shí)箱熬,傳入的密碼字符串需要使用一些算法來(lái)生成类垦,而不是直接在代碼中寫(xiě)一個(gè)看一眼就知道的密碼,不然加密也沒(méi)啥意義

move()方法中城须,是固定的套路蚤认,但具體的語(yǔ)句,查了下糕伐,具體啥意思就先放棄了


2.2 EncryptDBManager

這里主要就是對(duì) EncryptDBHelper 的初始化

/**
 * 遷移數(shù)據(jù)庫(kù)管理
 */
public class EncryptDBManager {
    private Context mContext;
    private String mPassword;
    private SQLiteDatabase mDB;
    private EncryptDBHelper mDBHelper;

    public EncryptDBManager(Context mContext, String mPassword) {
        this.mContext = mContext;
        this.mPassword = mPassword;
    }

    /**
     * 初始化 EncryptDBHelper
     * 內(nèi)部實(shí)現(xiàn)了數(shù)據(jù)庫(kù)的遷移
     */
    public boolean init() {
        try {
            mDBHelper = new EncryptDBHelper(mContext, mPassword);
            mDB = mDBHelper.getWritableDatabase();
            return true;
        } catch (Exception e) {
            return false;
        }
    }

    /**
     * 拿到數(shù)據(jù)庫(kù)中所有的Person并放入集合中
     */
    public List<PlainPerson> getPersonListData() {
        List<PlainPerson> listData = new ArrayList<>();
        Cursor c = getAllPersonInfo();
        while (c.moveToNext()) {
            PlainPerson person = new PlainPerson();
            person.setName(c.getString(c.getColumnIndex("name")));
            person.setAddress(c.getString(c.getColumnIndex("address")));
            listData.add(person);
        }
        c.close();
        return listData;
    }

    private Cursor getAllPersonInfo() {
        return mDB.rawQuery("SELECT * FROM person", null);
    }

    /**
     * 關(guān)閉  database砰琢;
     */
    public void closeDB() {
        mDB.close();
    }

    /**
     * 刪除數(shù)據(jù)庫(kù)
     */
    public Boolean deleteDatabase() {
        return mDBHelper.onDelete();
    }
}

Activity中直接調(diào)用方法就成

使用db工具查看未加密和加密后的數(shù)據(jù)庫(kù)文件

未加密
加密


3. 最后

WCDB中還有很多東西不了解,日后慢慢接觸

關(guān)于遺留的那個(gè)插入操作優(yōu)化良瞧,有知道的同學(xué)陪汽,請(qǐng)留言說(shuō)一下 </p>補(bǔ)充:這里搞錯(cuò)了,已改正褥蚯,原因說(shuō)明在上面

有錯(cuò)誤挚冤,請(qǐng)指出

共勉 : )

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個(gè)濱河市赞庶,隨后出現(xiàn)的幾起案子训挡,更是在濱河造成了極大的恐慌,老刑警劉巖尘执,帶你破解...
    沈念sama閱讀 219,110評(píng)論 6 508
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件舍哄,死亡現(xiàn)場(chǎng)離奇詭異,居然都是意外死亡誊锭,警方通過(guò)查閱死者的電腦和手機(jī)表悬,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 93,443評(píng)論 3 395
  • 文/潘曉璐 我一進(jìn)店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái)丧靡,“玉大人蟆沫,你說(shuō)我怎么就攤上這事籽暇。” “怎么了饭庞?”我有些...
    開(kāi)封第一講書(shū)人閱讀 165,474評(píng)論 0 356
  • 文/不壞的土叔 我叫張陵戒悠,是天一觀的道長(zhǎng)。 經(jīng)常有香客問(wèn)我舟山,道長(zhǎng)绸狐,這世上最難降的妖魔是什么? 我笑而不...
    開(kāi)封第一講書(shū)人閱讀 58,881評(píng)論 1 295
  • 正文 為了忘掉前任累盗,我火速辦了婚禮寒矿,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘若债。我一直安慰自己符相,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,902評(píng)論 6 392
  • 文/花漫 我一把揭開(kāi)白布蠢琳。 她就那樣靜靜地躺著啊终,像睡著了一般。 火紅的嫁衣襯著肌膚如雪傲须。 梳的紋絲不亂的頭發(fā)上蓝牲,一...
    開(kāi)封第一講書(shū)人閱讀 51,698評(píng)論 1 305
  • 那天,我揣著相機(jī)與錄音躏碳,去河邊找鬼搞旭。 笑死散怖,一個(gè)胖子當(dāng)著我的面吹牛菇绵,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播镇眷,決...
    沈念sama閱讀 40,418評(píng)論 3 419
  • 文/蒼蘭香墨 我猛地睜開(kāi)眼咬最,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼!你這毒婦竟也來(lái)了欠动?” 一聲冷哼從身側(cè)響起永乌,我...
    開(kāi)封第一講書(shū)人閱讀 39,332評(píng)論 0 276
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤,失蹤者是張志新(化名)和其女友劉穎具伍,沒(méi)想到半個(gè)月后翅雏,有當(dāng)?shù)厝嗽跇?shù)林里發(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 45,796評(píng)論 1 316
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡望几,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,968評(píng)論 3 337
  • 正文 我和宋清朗相戀三年,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了萤厅。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片橄抹。...
    茶點(diǎn)故事閱讀 40,110評(píng)論 1 351
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡靴迫,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出楼誓,到底是詐尸還是另有隱情玉锌,我是刑警寧澤,帶...
    沈念sama閱讀 35,792評(píng)論 5 346
  • 正文 年R本政府宣布疟羹,位于F島的核電站主守,受9級(jí)特大地震影響,放射性物質(zhì)發(fā)生泄漏榄融。R本人自食惡果不足惜丸逸,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,455評(píng)論 3 331
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望剃袍。 院中可真熱鬧黄刚,春花似錦、人聲如沸民效。這莊子的主人今日做“春日...
    開(kāi)封第一講書(shū)人閱讀 32,003評(píng)論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)畏邢。三九已至业扒,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間舒萎,已是汗流浹背程储。 一陣腳步聲響...
    開(kāi)封第一講書(shū)人閱讀 33,130評(píng)論 1 272
  • 我被黑心中介騙來(lái)泰國(guó)打工, 沒(méi)想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留臂寝,地道東北人章鲤。 一個(gè)月前我還...
    沈念sama閱讀 48,348評(píng)論 3 373
  • 正文 我出身青樓,卻偏偏與公主長(zhǎng)得像咆贬,于是被迫代替她去往敵國(guó)和親败徊。 傳聞我的和親對(duì)象是個(gè)殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 45,047評(píng)論 2 355

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

  • Android 自定義View的各種姿勢(shì)1 Activity的顯示之ViewRootImpl詳解 Activity...
    passiontim閱讀 172,173評(píng)論 25 707
  • Spring Cloud為開(kāi)發(fā)人員提供了快速構(gòu)建分布式系統(tǒng)中一些常見(jiàn)模式的工具(例如配置管理掏缎,服務(wù)發(fā)現(xiàn)皱蹦,斷路器,智...
    卡卡羅2017閱讀 134,663評(píng)論 18 139
  • Objective-C 代碼容易被 hook眷蜈,暴露信息太赤裸裸沪哺,為了安全,改用 C 來(lái)寫(xiě)吧酌儒! 當(dāng)然不是全部代碼都要...
    無(wú)灃閱讀 223評(píng)論 0 0
  • 參考文章: https://zhuanlan.zhihu.com/p/27238630
    rogerwu1228閱讀 459評(píng)論 0 0
  • 在掌聲與笑聲中開(kāi)始 在掌聲與笑聲中結(jié)束 斑斕的水彩涂畫(huà)丑惡的面容 愚笨的動(dòng)作盡顯思想的呆滯 舞臺(tái)萎縮成一個(gè)圓 僅為...
    楚謖姑娘閱讀 275評(píng)論 0 2