Android数据库源码分析(2)-SQLiteDatabase的实现以及多线程行为

本系列主要关注安卓数据库的线程行为,分为四个部分:
(1)SQLiteOpenHelper的getReadableDatabase和getWritableDatabase
(2)SQLiteDatabase的实现以及多线程行为
(3)连接缓存池SQLiteConnectionPool
(4)SQLiteDatabase多线程实践

本篇主要关注SQLiteDatabase的线程同步实现与架构实现。

1 SQLiteClosable的acquireReference与releaseReference方法

SQLiteClosableSQLiteDatabase的父类,也同时是数据库下其他几个类的父类。其中实现了引用计数逻辑来控制资源释放的时机。

private int mReferenceCount = 1;

public void acquireReference() {
    synchronized(this) {
        if (mReferenceCount <= 0) {
            throw new IllegalStateException(
                    "attempt to re-open an already-closed object: " + this);
        }
        mReferenceCount++;
    }
}

public void releaseReference() {
    boolean refCountIsZero = false;
    synchronized(this) {
        refCountIsZero = --mReferenceCount == 0;
    }
    if (refCountIsZero) {
        onAllReferencesReleased();
    }
}

可以看到这里用mReferenceCount简单地实现了一个引用计数。而引用计数的初始值是1。SQLiteDatabase会在每次操作前调用一次acquireReference,而在结束后调用一次releaseReference。为了方便,下文中把这样的被acquireReferencereleaseReference包裹的过程称为一次“操作”。
那么如果这两个方法保持成对调用的话,是不是就不可能触发onAllReferenceReleased方法?事实上,SQLiteClosable还有一个方法close调用了releaseReference。由于锁的存在,只要不在其它“操作”中调用close,调用close之后mReferenceCount的值可以断定是0。
到这里为止,感觉上是可以用一个boolean值来标记引用状态的。因为由于锁的存在,只要各个“操作”是序列进行的(没有一个“操作”调用了另一个“操作”的情况),mReferenceCount只可能是0和1。推测引用计数就是为了应付“操作”之间存在调用这种情况。这就像同一个线程里的嵌套锁需要进行计数一样。

2 SQLiteDatabase的打开与关闭

2.1 关闭

上文中提到的onAllReferenceReleased是一个抽象方法。其在SQLiteDatabase中的实现为

@Override
protected void onAllReferencesReleased() {
    dispose(false);
}

在finalize中同样调用了dispose方法

protected void finalize() throws Throwable {
    try {
        dispose(true);
    } finally {
        super.finalize();
    }
}

dispose的实现为

private void dispose(boolean finalized) {
    final SQLiteConnectionPool pool;
    synchronized (mLock) {
        if (mCloseGuardLocked != null) {
            if (finalized) {
                //CloseGuard是一个监测是否及时调用close方法的类,一般来说除了输出日志并不会做别的什么
                //这里事实上就是在finalize的时候如果没有close过,就输出一条日志
                mCloseGuardLocked.warnIfOpen();
            }
            mCloseGuardLocked.close();
        }
        pool = mConnectionPoolLocked;//这个mConnectionPool是连接池。此方法里将其置空并关闭。后文详细讨论其作用。
        mConnectionPoolLocked = null;
    }
    if (!finalized) {
        //sActiveDatabases是一个静态的WeakHashMap,用key来放置所有活动数据库,而value并没有作用。dispose的时候自然要移除this。
        //跟踪代码分析下来,用这个map只是为了bug report
        synchronized (sActiveDatabases) {
            sActiveDatabases.remove(this);
        }
        if (pool != null) {
            pool.close();
        }
    }
}

2.2 打开

在本系列第一篇中我们曾看到过,最终的打开数据库的是一个静态方法,SQLiteDatabase.openDatabase(String path, CursorFactory factory, int flags, DatabaseErrorHandler errorHandler)

public static SQLiteDatabase openDatabase(String path, CursorFactory factory, int flags,
        DatabaseErrorHandler errorHandler) {
    SQLiteDatabase db = new SQLiteDatabase(path, flags, factory, errorHandler);
    db.open();
    return db;
}

这里很简单,就是新建一个对象,然后调用open。构造器里只有一些初始化,略过。着重看open方法:

private void open() {
    try {
        try {
            openInner();//尝试一次
        } catch (SQLiteDatabaseCorruptException ex) {
            onCorruption();//失败了,再次尝试前调用另一个方法。
            openInner();
        }
    } catch (SQLiteException ex) {
        Log.e(TAG, "Failed to open database '" + getLabel() + "'.", ex);
        close();
        throw ex;
    }
}

private void openInner() {
    synchronized (mLock) {
        assert mConnectionPoolLocked == null;
        mConnectionPoolLocked = SQLiteConnectionPool.open(mConfigurationLocked);
        mCloseGuardLocked.open("close");
    }

    synchronized (sActiveDatabases) {//这是之前那个WeakHashMap
        sActiveDatabases.put(this, null);
    }
}

void onCorruption() {
    EventLog.writeEvent(EVENT_DB_CORRUPT, getLabel());
    mErrorHandler.onCorruption(this);
}

open中会尝试调用openInner。如果失败一次,则调用onCorruption,随后再尝试一次。mErrorHandler是构造器传入的,构造器参数由静态方法openDatabase传入,而这个参数又最终从SQLiteOpenHelper传入。

openInner中做的事情,从命名上看,是开启一个SQLiteConnectionPool即数据库连接池。简单地说,数据库连接池维持了对数据库的多个连接。数据库连接的类是SQLiteConnection

3 线程内单例的SQLiteSession

private final ThreadLocal<SQLiteSession> mThreadSession = new ThreadLocal<SQLiteSession>() {
    @Override
    protected SQLiteSession initialValue() {
        return createSession();
    }
};

SQLiteSession getThreadSession() {
    return mThreadSession.get(); // initialValue() throws if database closed
}

SQLiteSession createSession() {
    final SQLiteConnectionPool pool;
    synchronized (mLock) {
        throwIfNotOpenLocked();
        pool = mConnectionPoolLocked;
    }
    return new SQLiteSession(pool);
}

ThreadLocal会在每个线程内维护一个对象,而在线程结束时解除对对象的引用。initialValue方法会在线程中不存在已有对象时创建一个,不Override的话会给出一个null。除此之外也可以通过ThreadLocal.set来给本线程配置一个对象。
可以看到mThreadSession是一个ThreadLocal。调用getThreadSession会获取一个线程内单例的SQLiteSession对象。

SQLiteSession是提供数据库操作能力(增删改查以及事务)的一个单元。它会从SQLiteConnectionPool即连接池中获取连接,最终对数据库进行操作。

到这儿类已经有点多了。整理一下逻辑:
(1)SQLiteDatabase持有一个ThreadLocal,用于对每个线程生成一个SQLiteSession
(2)SQLiteSession持有SQLiteConnectionPool(虽然SQLiteDatabase也持有连接池对象,但它只用来传递给SQLiteSession),但是同一个SQLiteDatabase下的SQLiteSession是共用一个SQLiteConnectionPool的;
(3)SQLiteConnectionPool管理SQLiteConnection并适时向SQLiteSession提供之;
(4)SQLiteConnection直接对底层数据库进行操作(这个类里面才有大量的native方法)。

接下来分析一下SQLiteSession

获取与释放连接,还是一个引用计数实现:

private final SQLiteConnectionPool mConnectionPool;//构造器中初始化,值从SQLiteDatabase对象中传入
private SQLiteConnection mConnection;
private int mConnectionFlags;
private int mConnectionUseCount;//无处不在的引用计数

private void acquireConnection(String sql, int connectionFlags,
        CancellationSignal cancellationSignal) {
    if (mConnection == null) {
        assert mConnectionUseCount == 0;
        mConnection = mConnectionPool.acquireConnection(sql, connectionFlags,
                cancellationSignal); // might throw
        mConnectionFlags = connectionFlags;
    }
    mConnectionUseCount += 1;
}

private void releaseConnection() {
    assert mConnection != null;
    assert mConnectionUseCount > 0;
    if (--mConnectionUseCount == 0) {
        try {
            mConnectionPool.releaseConnection(mConnection); // might throw
        } finally {
            mConnection = null;
        }
    }
}

具体的数据库操作有很多executeXXX形式的方法,逻辑大同小异。挑一个看看:

public int executeForChangedRowCount(String sql, Object[] bindArgs, int connectionFlags,
        CancellationSignal cancellationSignal) {
    if (sql == null) {
        throw new IllegalArgumentException("sql must not be null.");
    }

    if (executeSpecial(sql, bindArgs, connectionFlags, cancellationSignal)) {//排除特殊操作
        return 0;
    }

    //获取连接
    acquireConnection(sql, connectionFlags, cancellationSignal); // might throw
    try {
        //底层数据库操作。本文不关心。
        return mConnection.executeForChangedRowCount(sql, bindArgs,
                cancellationSignal); // might throw
    } finally {
        //释放连接
        releaseConnection(); // might throw
    }
}

//用来支持'BEGIN','COMMIT','ROLLBACK'的操作。就是与Transaction相关的操作。
private boolean executeSpecial(String sql, Object[] bindArgs, int connectionFlags,
        CancellationSignal cancellationSignal) {
    if (cancellationSignal != null) {
        cancellationSignal.throwIfCanceled();
    }

    final int type = DatabaseUtils.getSqlStatementType(sql);
    switch (type) {
        case DatabaseUtils.STATEMENT_BEGIN:
            beginTransaction(TRANSACTION_MODE_EXCLUSIVE, null, connectionFlags,
                    cancellationSignal);
            return true;

        case DatabaseUtils.STATEMENT_COMMIT:
            setTransactionSuccessful();
            endTransaction(cancellationSignal);
            return true;

        case DatabaseUtils.STATEMENT_ABORT:
            endTransaction(cancellationSignal);
            return true;
    }
    return false;
}

4 单次完整的SQLite操作

4.1 SQLiteStatement

以最简单的delete方法为例。其它方法的流程均大同小异。

public int delete(String table, String whereClause, String[] whereArgs) {
    acquireReference();
    try {
        SQLiteStatement statement =  new SQLiteStatement(this, "DELETE FROM " + table +
                (!TextUtils.isEmpty(whereClause) ? " WHERE " + whereClause : ""), whereArgs);
        try {
            return statement.executeUpdateDelete();
        } finally {
            statement.close();
        }
    } finally {
        releaseReference();
    }
}

先用SQLiteStatement做一些sql转义和拼接,然后调用statement.executeUpdateDelete()

具体看一下executeUpdateDelete

//以下来自SQLiteStatement
public int executeUpdateDelete() {
    acquireReference();//注意这里是SQLiteStatement内的引用计数,不是SQLiteDatabase了。
    try {
        return getSession().executeForChangedRowCount(
                getSql(), getBindArgs(), getConnectionFlags(), null);//上一节分析过了,执行SQL。
    } catch (SQLiteDatabaseCorruptException ex) {
        onCorruption();
        throw ex;
    } finally {
        releaseReference();
    }
}

//这个方法在父类SQLiteProgram中。又回到了上一小节的getThreadSession。获取线程内的单例。
protected final SQLiteSession getSession() {
    return mDatabase.getThreadSession();
}

4.2 SQLiteDirectCursorDriver与SQLiteQuery

与4.1不同的是,在进行query操作时,最终没有使用SQLiteStatement类,而是通过SQLiteDirectCursorDriver间接使用了SQLiteQuery。而SQLiteQuerySQLiteStatement同为SQLiteProgram的子类,完成类似的功能。

所有的query操作最终均调用这样一个方法:

public Cursor rawQueryWithFactory(
        CursorFactory cursorFactory, String sql, String[] selectionArgs,
        String editTable, CancellationSignal cancellationSignal) {
    acquireReference();
    try {
        SQLiteCursorDriver driver = new SQLiteDirectCursorDriver(this, sql, editTable,
                cancellationSignal);
        return driver.query(cursorFactory != null ? cursorFactory : mCursorFactory,
                selectionArgs);
    } finally {
        releaseReference();
    }
}

SQLiteDirectCursorDriver的query方法如下:

public Cursor query(CursorFactory factory, String[] selectionArgs) {
    final SQLiteQuery query = new SQLiteQuery(mDatabase, mSql, mCancellationSignal);
    final Cursor cursor;
    try {
        query.bindAllArgsAsStrings(selectionArgs);

        if (factory == null) {
            cursor = new SQLiteCursor(this, mEditTable, query);
        } else {
            cursor = factory.newCursor(mDatabase, this, mEditTable, query);
        }
    } catch (RuntimeException ex) {
        query.close();
        throw ex;
    }

    mQuery = query;
    return cursor;
}

其中新建了一个SQLiteQuery,并绑定参数。随后新建一个Cursor,这就是最终返回的Cursor对象。接下来考察无CursorFactory情况下默认返回的SQLiteCursor

AbstractCursor中各种move方法均会调用moveToPosition,而moveToPosition会调用onMoveSQliteCursoronMove的实现为:

@Override
public boolean onMove(int oldPosition, int newPosition) {
    // Make sure the row at newPosition is present in the window
    if (mWindow == null || newPosition < mWindow.getStartPosition() ||
            newPosition >= (mWindow.getStartPosition() + mWindow.getNumRows())) {
        fillWindow(newPosition);
    }

    return true;
}

private void fillWindow(int requiredPos) {
    clearOrCreateWindow(getDatabase().getPath());

    try {
        if (mCount == NO_COUNT) {
            int startPos = DatabaseUtils.cursorPickFillWindowStartPosition(requiredPos, 0);
            mCount = mQuery.fillWindow(mWindow, startPos, requiredPos, true);
            mCursorWindowCapacity = mWindow.getNumRows();
            if (Log.isLoggable(TAG, Log.DEBUG)) {
                Log.d(TAG, "received count(*) from native_fill_window: " + mCount);
            }
        } else {
            int startPos = DatabaseUtils.cursorPickFillWindowStartPosition(requiredPos,
                    mCursorWindowCapacity);
            mQuery.fillWindow(mWindow, startPos, requiredPos, false);
        }
    } catch (RuntimeException ex) {
        // Close the cursor window if the query failed and therefore will
        // not produce any results.  This helps to avoid accidentally leaking
        // the cursor window if the client does not correctly handle exceptions
        // and fails to close the cursor.
        closeWindow();
        throw ex;
    }
}

核心逻辑在mQuery.fillWindow(mWindow, startPos, requiredPos, false);这里。mQuery就是之前传入的SQLiteQuery对象。查看其fillWindow方法:

int fillWindow(CursorWindow window, int startPos, int requiredPos, boolean countAllRows) {
    acquireReference();
    try {
        window.acquireReference();
        try {
            int numRows = getSession().executeForCursorWindow(getSql(), getBindArgs(),
                    window, startPos, requiredPos, countAllRows, getConnectionFlags(),
                    mCancellationSignal);
            return numRows;
        } catch (SQLiteDatabaseCorruptException ex) {
            onCorruption();
            throw ex;
        } catch (SQLiteException ex) {
            Log.e(TAG, "exception: " + ex.getMessage() + "; query: " + getSql());
            throw ex;
        } finally {
            window.releaseReference();
        }
    } finally {
        releaseReference();
    }
}

可以看到,最终回到了SQLiteSession.executeXXX方法逻辑之下。其余即与上一节类似。

而从Cursor中取出数据的过程,则最终是由CursorWindow下的一系列native方法来完成,我认为属于Cursor的代码体系了,这里不重点展开。

5 Transaction

5.1 beginTransaction

//一群差不多的beginTransaction方法最终调用到了这里
private void beginTransaction(SQLiteTransactionListener transactionListener,
        boolean exclusive) {
    acquireReference();//怎么老是你
    try {
        getThreadSession().beginTransaction(
                exclusive ? SQLiteSession.TRANSACTION_MODE_EXCLUSIVE :
                        SQLiteSession.TRANSACTION_MODE_IMMEDIATE,
                transactionListener,
                getThreadDefaultConnectionFlags(false /*readOnly*/), null);
    } finally {
        releaseReference();
    }
}

//上面的方法调用了这个方法。这套flags做了两件小事:1.确定只读还是可写  2.如果是主线程,就要提高连接的优先级
int getThreadDefaultConnectionFlags(boolean readOnly) {
    int flags = readOnly ? SQLiteConnectionPool.CONNECTION_FLAG_READ_ONLY :
            SQLiteConnectionPool.CONNECTION_FLAG_PRIMARY_CONNECTION_AFFINITY;
    if (isMainThread()) {
        flags |= SQLiteConnectionPool.CONNECTION_FLAG_INTERACTIVE;
    }
    return flags;
}

还是要看SQLiteSession内部:

public void beginTransaction(int transactionMode,
        SQLiteTransactionListener transactionListener, int connectionFlags,
        CancellationSignal cancellationSignal) {
    throwIfTransactionMarkedSuccessful();//一点合法性检查,不贴了
    beginTransactionUnchecked(transactionMode, transactionListener, connectionFlags,
            cancellationSignal);
}

private void beginTransactionUnchecked(int transactionMode,
        SQLiteTransactionListener transactionListener, int connectionFlags,
        CancellationSignal cancellationSignal) {
    if (cancellationSignal != null) {
    //cancellationSignal从beginTransaction以及SQLiteStatement诸方法传入的均为null,调查发现仅query时可以传入此参数。
        cancellationSignal.throwIfCanceled();
    }

    if (mTransactionStack == null) {//Transaction栈为空时才获取连接。
        acquireConnection(null, connectionFlags, cancellationSignal); // might throw
    }
    try {
        // Set up the transaction such that we can back out safely
        // in case we fail part way.
        if (mTransactionStack == null) {//如果没有进行中的Transaction,创建一个并BEGIN
            // Execute SQL might throw a runtime exception.
            switch (transactionMode) {
                case TRANSACTION_MODE_IMMEDIATE:
                    mConnection.execute("BEGIN IMMEDIATE;", null,
                            cancellationSignal); // might throw
                    break;
                case TRANSACTION_MODE_EXCLUSIVE:
                    mConnection.execute("BEGIN EXCLUSIVE;", null,
                            cancellationSignal); // might throw
                    break;
                default:
                    mConnection.execute("BEGIN;", null, cancellationSignal); // might throw
                    break;
            }
        }

        // Listener might throw a runtime exception.
        if (transactionListener != null) {
            try {
                transactionListener.onBegin(); // might throw
            } catch (RuntimeException ex) {
                if (mTransactionStack == null) {
                    mConnection.execute("ROLLBACK;", null, cancellationSignal); // might throw
                }
                throw ex;
            }
        }

        // Bookkeeping can't throw, except an OOM, which is just too bad...
        Transaction transaction = obtainTransaction(transactionMode, transactionListener);//创建事务
        transaction.mParent = mTransactionStack;
        mTransactionStack = transaction;//入栈
    } finally {
        if (mTransactionStack == null) {//这里要栈为空时才释放连接。不为空时永远持有一个连接。
            releaseConnection(); // might throw
        }
    }
}

private static final class Transaction {
    public Transaction mParent;//这个是个链表,或者说在这里充当了一个栈
    public int mMode;
    public SQLiteTransactionListener mListener;
    public boolean mMarkedSuccessful;
    public boolean mChildFailed;
}

5.2 setTransactionSuccessful与endTransaction

直接看SQLiteSession吧:

public void setTransactionSuccessful() {
    throwIfNoTransaction();
    throwIfTransactionMarkedSuccessful();

    mTransactionStack.mMarkedSuccessful = true;//仅仅是个标记
}

public void endTransaction(CancellationSignal cancellationSignal) {
    throwIfNoTransaction();
    assert mConnection != null;

    endTransactionUnchecked(cancellationSignal, false);
}

private void endTransactionUnchecked(CancellationSignal cancellationSignal, boolean yielding) {
    if (cancellationSignal != null) {
        cancellationSignal.throwIfCanceled();
    }

    final Transaction top = mTransactionStack;
    boolean successful = (top.mMarkedSuccessful || yielding) && !top.mChildFailed;//如果有子Transaction失败,也是失败的

    RuntimeException listenerException = null;
    final SQLiteTransactionListener listener = top.mListener;
    if (listener != null) {
        try {
            if (successful) {
                listener.onCommit(); // might throw
            } else {
                listener.onRollback(); // might throw
            }
        } catch (RuntimeException ex) {
            listenerException = ex;
            successful = false;
        }
    }

    mTransactionStack = top.mParent;//退栈
    recycleTransaction(top);//回收

    if (mTransactionStack != null) {//还没到最外层事务,只做个标记
        if (!successful) {
            mTransactionStack.mChildFailed = true;
        }
    } else {//到了最外层事务了,提交或回滚
        try {
            if (successful) {
                mConnection.execute("COMMIT;", null, cancellationSignal); // might throw
            } else {
                mConnection.execute("ROLLBACK;", null, cancellationSignal); // might throw
            }
        } finally {
            releaseConnection(); // might throw
        }
    }

    if (listenerException != null) {
        throw listenerException;
    }
}

6 总结

(1)总的来说,SQLiteDatabase是线程安全且高效的。它并没有简单地对每次操作加锁,而是使用引用计数和ThreadLocal来保证连接复用的线程安全性,数据一致性则交由SQLite自身去保证,以达到最优性能。
而很多时候我们在业务层封装时反而处处加锁,其实是没有必要的。
(2)SQLiteDatabase的内部实现会让每个线程单独持有一个数据库连接(不一定是创建,因为有连接池优化),而不是每个SQLiteDatabase对象对应一个连接。
(3)数据库会给主线程持有的连接提高优先级。如果执行的是读操作或者小量数据的写入操作的话,可能可以满足主线程低延迟的需要。但是还没有具体的数据来支撑这一结论,希望有大牛补充。
(4)多线程下的事务行为本文中未作分析,下一篇会就此问题单独进行讨论。

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

推荐阅读更多精彩内容

  • Android 自定义View的各种姿势1 Activity的显示之ViewRootImpl详解 Activity...
    passiontim阅读 171,442评论 25 707
  • 从三月份找实习到现在,面了一些公司,挂了不少,但最终还是拿到小米、百度、阿里、京东、新浪、CVTE、乐视家的研发岗...
    时芥蓝阅读 42,178评论 11 349
  • 早上醒来盯着眼前的躯干不知道里面的人是谁镜子中的面目一片模糊 远古的神话已无人传诵童年的襁褓再也找不回 那些荒诞的...
    猫儿脑良少阅读 184评论 0 0
  • 林俊杰 ❤
    WYY_阅读 180评论 0 0
  • 广州始建于公元前214年,商代时广州地区称为“南越”,春秋战国时期,当时居住在这里的民族称为南越(又称南粤)……...
    羽悦开心阅读 514评论 1 1