SQLiteOpenHelper在query得到Cursor返回值異常問題探究

最近開發的一個功能會用到SQLite,碰到一個問題,糾結了整整一個下午,終於找到原因,記錄一下。

      功能很簡單,創建了一個自定義的ListView,在每個ListView中都對應有一個Button,而該Button需要有個狀態記錄Button使用情況,比如Enable和Disable。順利地創建了數據庫和自定義ListView,在點擊Button時,將ListView對應的信息通過setTag(key, value)傳遞,並在Button的onClick()中通過getTag(key)獲得對應屬性。以上步驟暫時都未碰到任何問題,接下來問題就來了。

      在SQLiteOpenHelper的繼承類DataBaseProvider中,增加了一個查詢數據記錄的函數,代碼如下:

	public Cursor query(String name) {
		SQLiteDatabase db = this.getReadableDatabase();
		String querySql = "select " + PKG_NAME + " , " + CMP_STATE + " from " + TABLE_APPS_NAME + " where " + 
						CMP_NAME + " = '" + compName + "'";
		
		Cursor cursor = db.rawQuery(querySql, null);
		return cursor;
	}
在反覆跟蹤這段代碼時,發現得到的Cursor對象值總有問題,數據內容老是對應的database中table的Column名,未得到正確的database record。百思不得其解,對應地也查找了Android工程中其他數據庫查詢的操作,也大同小異,最後檢查下來才發現,在rawQuery結束後,需要將cursor定位到record的起始位置,即需要再調用一下cursor.moveToFirst()。最新代碼更新如下:

	public Cursor query(String name) {
		SQLiteDatabase db = this.getReadableDatabase();
		String querySql = "select " + PKG_NAME + " , " + CMP_STATE + " from " + TABLE_APPS_NAME + " where " + 
						CMP_NAME + " = '" + compName + "'";
		
		Cursor cursor = db.rawQuery(querySql, null);
		cursor.moveToFirst();
		return cursor;
	}
到這裏,數據庫功能上的問題已經解決了,但是作爲有專研精神的碼農怎麼能放過Discovery的好機會呢?那麼我們從SQLiteDatabase的rawQuery接口開始分析,碼農比較習慣以文件名來劃分步驟:

1、SQLiteDatabase.java

    public Cursor rawQuery(String sql, String[] selectionArgs,
            CancellationSignal cancellationSignal) {
        return rawQueryWithFactory(null, sql, selectionArgs, null, cancellationSignal);
    }

顯然,rawQuery(......)又調用了rawQueryWithFactory(......),函數如下:

    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實例,並由該實例driver來執行query(...)。

2、SQLiteDirectCursorDriver.java

    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;
    }
在這個函數中,我們終於看到了我們需要的Cursor對象了,無疑是個好消息啊!我們先看第一個參數factory,從rawQueryWithFactory開始看,傳了個null下去,會在driver.query中判斷並賦值:

cursorFactory != null ? cursorFactory : mCursorFactory
mCursorFactory從代碼流程看,是在SQLiteDatabase創建db時,即openDatabase或者openOrCreateDatabase中,創建SQLiteDatabase實例時賦值的,如下:

    private SQLiteDatabase(String path, int openFlags, CursorFactory cursorFactory,
            DatabaseErrorHandler errorHandler) {
        mCursorFactory = cursorFactory;
        mErrorHandler = errorHandler != null ? errorHandler : new DefaultDatabaseErrorHandler();
        mConfigurationLocked = new SQLiteDatabaseConfiguration(path, openFlags);
    }
實際上openOrCreateDatabase其實是調用ContextImpl.java中的openOrCreateDatabase,但是我們發現我們往往在使用時都是傳了null給cursorFactory, 因爲我們常常寫成這樣:

openOrCreateDatabase("test.db", Context.MODE_PRIVATE, null);

也就是說在上面的query函數中,我們會創建一個SQLiteCursor實例。代碼走到這裏,不免疑惑,貌似這個cursor沒和我們的查詢語句String sql關聯起來啊?而且再看cursor = new SQLiteCursor(this, mEditTable, query)以及SQLiteCursor的構造函數,只是做了賦值操作:

    public SQLiteCursor(SQLiteCursorDriver driver, String editTable, SQLiteQuery query) {
        if (query == null) {
            throw new IllegalArgumentException("query object cannot be null");
        }
        if (StrictMode.vmSqliteObjectLeaksEnabled()) {
            mStackTrace = new DatabaseObjectNotClosedException().fillInStackTrace();
        } else {
            mStackTrace = null;
        }
        mDriver = driver;
        mEditTable = editTable;
        mColumnNameMap = null;
        mQuery = query;

        mColumns = query.getColumnNames();
        mRowIdColumnIndex = DatabaseUtils.findRowIdColumnIndex(mColumns);
        }
    }

看來關鍵還在參數query上,query就是在函數開頭創建的:

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

其中構造函數參數mSql就是在創建SQLiteDirectCursorDriver實例時傳入的String sql,mDatabase就是傳入的database,cancellationSignal爲null。

3、SQLiteQuery.java

先看SQLiteQuery的構造函數:

public final class SQLiteQuery extends SQLiteProgram {
    ......
    SQLiteQuery(SQLiteDatabase db, String query, CancellationSignal cancellationSignal) {
        super(db, query, null, cancellationSignal);

        mCancellationSignal = cancellationSignal;
    }
    ......
}
看起來在SQLiteQuery.java中並沒做什麼,我們再去看SQLiteProgram.java。

4、SQLiteProgram.java

還是看構造函數:

public abstract class SQLiteProgram extends SQLiteClosable {
    ......
    SQLiteProgram(SQLiteDatabase db, String sql, Object[] bindArgs,
            CancellationSignal cancellationSignalForPrepare) {
        mDatabase = db;
        mSql = sql.trim();

        int n = DatabaseUtils.getSqlStatementType(mSql);
        switch (n) {
            case DatabaseUtils.STATEMENT_BEGIN:
            case DatabaseUtils.STATEMENT_COMMIT:
            case DatabaseUtils.STATEMENT_ABORT:
                mReadOnly = false;
                mColumnNames = EMPTY_STRING_ARRAY;
                mNumParameters = 0;
                break;

            default:
                boolean assumeReadOnly = (n == DatabaseUtils.STATEMENT_SELECT);
                SQLiteStatementInfo info = new SQLiteStatementInfo();
                db.getThreadSession().prepare(mSql,
                        db.getThreadDefaultConnectionFlags(assumeReadOnly),
                        cancellationSignalForPrepare, info);
                mReadOnly = info.readOnly;
                mColumnNames = info.columnNames;
                mNumParameters = info.numParameters;
                break;
        }

        if (bindArgs != null && bindArgs.length > mNumParameters) {
            throw new IllegalArgumentException("Too many bind arguments.  "
                    + bindArgs.length + " arguments were provided but the statement needs "
                    + mNumParameters + " arguments.");
        }

        if (mNumParameters != 0) {
            mBindArgs = new Object[mNumParameters];
            if (bindArgs != null) {
                System.arraycopy(bindArgs, 0, mBindArgs, 0, bindArgs.length);
            }
        } else {
            mBindArgs = null;
        }
    }
    ......
}
從剛開始,我們知道String sql是類似於“select * from table where ...”這樣的查詢語句。因此,下面的代碼返回得到的n就是STATEMENT_SELECT。

5、DataBaseUtils.java

    public static int getSqlStatementType(String sql) {
        sql = sql.trim();
        if (sql.length() < 3) {
            return STATEMENT_OTHER;
        }
        String prefixSql = sql.substring(0, 3).toUpperCase(Locale.US);
        if (prefixSql.equals("SEL")) {
            return STATEMENT_SELECT;
        } else if (prefixSql.equals("INS") ||
                prefixSql.equals("UPD") ||
                prefixSql.equals("REP") ||
                prefixSql.equals("DEL")) {
            return STATEMENT_UPDATE;
        } else if (prefixSql.equals("ATT")) {
            return STATEMENT_ATTACH;
        } else if (prefixSql.equals("COM")) {
            return STATEMENT_COMMIT;
        } else if (prefixSql.equals("END")) {
            return STATEMENT_COMMIT;
        } else if (prefixSql.equals("ROL")) {
            return STATEMENT_ABORT;
        } else if (prefixSql.equals("BEG")) {
            return STATEMENT_BEGIN;
        } else if (prefixSql.equals("PRA")) {
            return STATEMENT_PRAGMA;
        } else if (prefixSql.equals("CRE") || prefixSql.equals("DRO") ||
                prefixSql.equals("ALT")) {
            return STATEMENT_DDL;
        } else if (prefixSql.equals("ANA") || prefixSql.equals("DET")) {
            return STATEMENT_UNPREPARED;
        }
        return STATEMENT_OTHER;
    }
代碼比較簡單,不多說,我們重新返回到第4步,繼續分析switch語句中的default流程:

        switch (n) {
            case DatabaseUtils.STATEMENT_BEGIN:
            case DatabaseUtils.STATEMENT_COMMIT:
            case DatabaseUtils.STATEMENT_ABORT:
            ......
            default:
                boolean assumeReadOnly = (n == DatabaseUtils.STATEMENT_SELECT);
                SQLiteStatementInfo info = new SQLiteStatementInfo();
                db.getThreadSession().prepare(mSql,
                        db.getThreadDefaultConnectionFlags(assumeReadOnly),
                        cancellationSignalForPrepare, info);
                mReadOnly = info.readOnly;
                mColumnNames = info.columnNames;
                mNumParameters = info.numParameters;
                break;
        }

那麼顯然assumeReadOnly值爲true,從下文看我們應該重點分析db.getThreadSession().prepare以及info變量,因爲執行完該語句後,即獲得了mColumnNames, mNumParameters。

先看db.getThreadSession(),db即我們一直往下傳遞的SQLiteDatabase參數,我們找到getThreadSession()。

    SQLiteSession getThreadSession() {
        return mThreadSession.get(); // initialValue() throws if database closed
    }
那這個mThreadSession從哪來?從上下文不難看到:

    private final ThreadLocal<SQLiteSession> mThreadSession = new ThreadLocal<SQLiteSession>() {
        @Override
        protected SQLiteSession initialValue() {
            return createSession();
        }
    };
mThreadSession是每個應用對應database session的本地線程。那mThreadSession.get()回來的就是一個SQLiteSession的引用了,繼續第6步。

6、SQLiteSession.java

    public void prepare(String sql, int connectionFlags, CancellationSignal cancellationSignal,
            SQLiteStatementInfo outStatementInfo) {
        if (sql == null) {
            throw new IllegalArgumentException("sql must not be null.");
        }

        if (cancellationSignal != null) {
            cancellationSignal.throwIfCanceled();
        }

        acquireConnection(sql, connectionFlags, cancellationSignal); // might throw
        try {
            mConnection.prepare(sql, outStatementInfo); // might throw
        } finally {
            releaseConnection(); // might throw
        }
    }
暫時先不分析如何從SQLiteConnectionPool中獲取一個mConnection,接着看SQLiteConnection.java中的prepare。

7、SQLiteConnection.java

    public void prepare(String sql, SQLiteStatementInfo outStatementInfo) {
        if (sql == null) {
            throw new IllegalArgumentException("sql must not be null.");
        }

        final int cookie = mRecentOperations.beginOperation("prepare", sql, null);
        try {
            final PreparedStatement statement = acquirePreparedStatement(sql);
            try {
                if (outStatementInfo != null) {
                    outStatementInfo.numParameters = statement.mNumParameters;
                    outStatementInfo.readOnly = statement.mReadOnly;

                    final int columnCount = nativeGetColumnCount(
                            mConnectionPtr, statement.mStatementPtr);
                    if (columnCount == 0) {
                        outStatementInfo.columnNames = EMPTY_STRING_ARRAY;
                    } else {
                        outStatementInfo.columnNames = new String[columnCount];
                        for (int i = 0; i < columnCount; i++) {
                            outStatementInfo.columnNames[i] = nativeGetColumnName(
                                    mConnectionPtr, statement.mStatementPtr, i);
                        }
                    }
                }
            } finally {
                releasePreparedStatement(statement);
            }
        } catch (RuntimeException ex) {
            mRecentOperations.failOperation(cookie, ex);
            throw ex;
        } finally {
            mRecentOperations.endOperation(cookie);
        }
    }
從上面代碼看,outStatementInfo即前面第4步中提到的info,而info的columnCount是通過調用JNI層的接口nativeGetColumnCount得到,info的columnNames根據得到的columnCount創建String數組,並通過JNI接口nativeGetColumnName得到。由JNI相關的知識我們知道,對應接口在android_database_SQLiteConnection.cpp中。

8、android_database_SQLiteConnection.cpp

static jint nativeGetColumnCount(JNIEnv* env, jclass clazz, jint connectionPtr,
        jint statementPtr) {
    SQLiteConnection* connection = reinterpret_cast<SQLiteConnection*>(connectionPtr);
    sqlite3_stmt* statement = reinterpret_cast<sqlite3_stmt*>(statementPtr);

    return sqlite3_column_count(statement);
}
static jstring nativeGetColumnName(JNIEnv* env, jclass clazz, jint connectionPtr,
        jint statementPtr, jint index) {
    SQLiteConnection* connection = reinterpret_cast<SQLiteConnection*>(connectionPtr);
    sqlite3_stmt* statement = reinterpret_cast<sqlite3_stmt*>(statementPtr);

    const jchar* name = static_cast<const jchar*>(sqlite3_column_name16(statement, index));
    if (name) {
        size_t length = 0;
        while (name[length]) {
            length += 1;
        }
        return env->NewString(name, length);
    }
    return NULL;
}
未完,待續...

















發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章