最近開發的一個功能會用到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;
}
未完,待續...