Android数据库一些源码分析
对于批量数据插入这种最常见的情况来说,我们来看两种实现方式(两种都用了事务)。
下面这种应该是最多人使用的插入数据的方法:
1 public long addByExec(List<Person> persons) { 2 3 long start = System.currentTimeMillis(); 4 db.beginTransaction(); 5 6 for (Person person : persons) { 7 db.execSQL(" INSERT INTO person(name,age,info) VALUES(?, ?, ?) ", 8 new Object[] { person.name, person.age, person.info }); 9 } 10 11 db.setTransactionSuccessful(); 12 long end = System.currentTimeMillis(); 13 db.endTransaction(); 14 return end - start; 15 16 }
再看一种比较少用的插入方法
1 public long addByStatement(List<Person> persons) { 2 long start = System.currentTimeMillis(); 3 db.beginTransaction(); 4 SQLiteStatement sqLiteStatement = db.compileStatement(sql); 5 6 for (Person person : persons) { 7 sqLiteStatement.bindString(1, person.name); 8 sqLiteStatement.bindString(2, person.age); 9 sqLiteStatement.bindString(3, person.info); 10 sqLiteStatement.executeInsert(); 11 } 12 db.setTransactionSuccessful(); 13 long end = System.currentTimeMillis(); 14 db.endTransaction(); 15 return end - start; 16 }
然后我们分别用这两个方法 来向数据库里面插入一万条数据 看看耗时多少。为了演示效果更加突出一点,我录制了一个GIF,同时,
这2个方法我也没有用子线程来操作他,直接在ui线程上操作 所以看起来效果会比较突出一些(但是自己写代码的时候千万别这么写小心ANR)。
可以看出来后者耗时几乎只有前者的 一半(所以以后大家在做大批量数据插入的时候可以考虑后者的实现方式)。我们来看看源代码为啥会这样。
首先看前者的实现方法源码
1 public void execSQL(String sql, Object[] bindArgs) throws SQLException { 2 if (bindArgs == null) { 3 throw new IllegalArgumentException("Empty bindArgs"); 4 } 5 executeSql(sql, bindArgs); 6 } 7 8 private int executeSql(String sql, Object[] bindArgs) throws SQLException { 9 if (DatabaseUtils.getSqlStatementType(sql) == DatabaseUtils.STATEMENT_ATTACH) { 10 disableWriteAheadLogging(); 11 mHasAttachedDbs = true; 12 } 13 SQLiteStatement statement = new SQLiteStatement(this, sql, bindArgs); 14 try { 15 return statement.executeUpdateDelete(); 16 } catch (SQLiteDatabaseCorruptException e) { 17 onCorruption(); 18 throw e; 19 } finally { 20 statement.close(); 21 } 22 }
我们发现 前者的实现 实际上最后也是通过SQLiteStatement 这个类是操作的。
而后者不过是
1 public SQLiteStatement compileStatement(String sql) throws SQLException { 2 verifyDbIsOpen(); 3 return new SQLiteStatement(this, sql, null); 4 }
所以实际上前者之所以比后者耗时 应该是下面这段代码的原因:
1 if (DatabaseUtils.getSqlStatementType(sql) == DatabaseUtils.STATEMENT_ATTACH) { 2 disableWriteAheadLogging(); 3 mHasAttachedDbs = true; 4 }
1 public static int getSqlStatementType(String sql) { 2 sql = sql.trim(); 3 if (sql.length() < 3) { 4 return STATEMENT_OTHER; 5 } 6 String prefixSql = sql.substring(0, 3).toUpperCase(); 7 if (prefixSql.equals("SEL")) { 8 return STATEMENT_SELECT; 9 } else if (prefixSql.equals("INS") || 10 prefixSql.equals("UPD") || 11 prefixSql.equals("REP") || 12 prefixSql.equals("DEL")) { 13 return STATEMENT_UPDATE; 14 } else if (prefixSql.equals("ATT")) { 15 return STATEMENT_ATTACH; 16 } else if (prefixSql.equals("COM")) { 17 return STATEMENT_COMMIT; 18 } else if (prefixSql.equals("END")) { 19 return STATEMENT_COMMIT; 20 } else if (prefixSql.equals("ROL")) { 21 return STATEMENT_ABORT; 22 } else if (prefixSql.equals("BEG")) { 23 return STATEMENT_BEGIN; 24 } else if (prefixSql.equals("PRA")) { 25 return STATEMENT_PRAGMA; 26 } else if (prefixSql.equals("CRE") || prefixSql.equals("DRO") || 27 prefixSql.equals("ALT")) { 28 return STATEMENT_DDL; 29 } else if (prefixSql.equals("ANA") || prefixSql.equals("DET")) { 30 return STATEMENT_UNPREPARED; 31 } 32 return STATEMENT_OTHER; 33 }
实际上就是多了一个字符串处理的函数。这就是为什么前者耗时要比后者多。因为实际上直接调用executeSql的时候
他里面是先做字符串处理然后再调用SQLiteStatement来执行,这个过程当然是比我们直接调用SQLiteStatement
来执行速度慢的。
我们首先来看一下下面这个函数
1 public Cursor queryTest1() { 2 long start1 = System.currentTimeMillis(); 3 Cursor c = db.rawQuery("select * from t1,t3 where t1.num>t3.num", null); 4 long end1 = System.currentTimeMillis(); 5 Log.v("DBManager", "time1 need " + (end1 - start1)); 6 long start2 = System.currentTimeMillis(); 7 c.moveToNext(); 8 long end2 = System.currentTimeMillis(); 9 Log.v("DBManager", "time2 need" + (end2 - start2)); 10 long start3 = System.currentTimeMillis(); 11 c.moveToNext(); 12 long end3 = System.currentTimeMillis(); 13 Log.v("DBManager", "time3 need" + (end3 - start3)); 14 return c; 15 }
一个很常见的,多表查询的函数,有些人可能会奇怪为啥在这个地方我要加那么多日志。实际上如果你t1和t3的数据都很多的话,这个查询是可以预料到的会非常耗时。
很多人都会以为这个耗时是在下面这条语句做的:
1 Cursor c = db.rawQuery("select * from t1,t3 where t1.num>t3.num", null);
但是实际上这个查询耗时是在你第一调用
1 c.moveToNext();
来做的,有兴趣的同学可以自己试一下,我们这里就不帮大家来演示这个效果了,但是可以帮助大家分析一下源代码为什么会是这样奇怪的结果?
我们首先来分析一下rawQuery 这个函数
1 public Cursor rawQuery(String sql, String[] selectionArgs) { 2 return rawQueryWithFactory(null, sql, selectionArgs, null); 3 } 4 5 /** 6 * Runs the provided SQL and returns a cursor over the result set. 7 * 8 * @param cursorFactory the cursor factory to use, or null for the default factory 9 * @param sql the SQL query. The SQL string must not be ; terminated 10 * @param selectionArgs You may include ?s in where clause in the query, 11 * which will be replaced by the values from selectionArgs. The 12 * values will be bound as Strings. 13 * @param editTable the name of the first table, which is editable 14 * @return A {@link Cursor} object, which is positioned before the first entry. Note that 15 * {@link Cursor}s are not synchronized, see the documentation for more details. 16 */ 17 public Cursor rawQueryWithFactory( 18 CursorFactory cursorFactory, String sql, String[] selectionArgs, 19 String editTable) { 20 verifyDbIsOpen(); 21 BlockGuard.getThreadPolicy().onReadFromDisk(); 22 23 SQLiteDatabase db = getDbConnection(sql); 24 SQLiteCursorDriver driver = new SQLiteDirectCursorDriver(db, sql, editTable); 25 26 Cursor cursor = null; 27 try { 28 cursor = driver.query( 29 cursorFactory != null ? cursorFactory : mFactory, 30 selectionArgs); 31 } finally { 32 releaseDbConnection(db); 33 } 34 return cursor; 35 }
看一下24行,发现是构造了一个driver对象 然后调用这个driver对象的query方法
我们继续跟踪源代码
1 public SQLiteDirectCursorDriver(SQLiteDatabase db, String sql, String editTable, 2 CancellationSignal cancellationSignal) { 3 mDatabase = db; 4 mEditTable = editTable; 5 mSql = sql; 6 mCancellationSignal = cancellationSignal; 7 } 8 9 public Cursor query(CursorFactory factory, String[] selectionArgs) { 10 final SQLiteQuery query = new SQLiteQuery(mDatabase, mSql, mCancellationSignal); 11 final Cursor cursor; 12 try { 13 query.bindAllArgsAsStrings(selectionArgs); 14 15 if (factory == null) { 16 cursor = new SQLiteCursor(this, mEditTable, query); 17 } else { 18 cursor = factory.newCursor(mDatabase, this, mEditTable, query); 19 } 20 } catch (RuntimeException ex) { 21 query.close(); 22 throw ex; 23 } 24 25 mQuery = query; 26 return cursor; 27 }
发现这个返回的cursor实际上就是直接new出来的一个对象
1 public SQLiteCursor(SQLiteCursorDriver driver, String editTable, SQLiteQuery query) { 2 if (query == null) { 3 throw new IllegalArgumentException("query object cannot be null"); 4 } 5 if (query.mDatabase == null) { 6 throw new IllegalArgumentException("query.mDatabase cannot be null"); 7 } 8 mStackTrace = new DatabaseObjectNotClosedException().fillInStackTrace(); 9 mDriver = driver; 10 mEditTable = editTable; 11 mColumnNameMap = null; 12 mQuery = query; 13 14 query.mDatabase.lock(query.mSql); 15 try { 16 // Setup the list of columns 17 int columnCount = mQuery.columnCountLocked(); 18 mColumns = new String[columnCount]; 19 20 // Read in all column names 21 for (int i = 0; i < columnCount; i++) { 22 String columnName = mQuery.columnNameLocked(i); 23 mColumns[i] = columnName; 24 if (false) { 25 Log.v("DatabaseWindow", "mColumns[" + i + "] is " 26 + mColumns[i]); 27 } 28 29 // Make note of the row ID column index for quick access to it 30 if ("_id".equals(columnName)) { 31 mRowIdColumnIndex = i; 32 } 33 } 34 } finally { 35 query.mDatabase.unlock(); 36 } 37 }
所以看到这里我们就能确定的是rawquery这个方法 返回的cursor实际上就是一个对象,并没有任何真正调用sql的地方。
然后我们来看看我们怀疑的moveToNext这个方法因为从日志上看耗时的地方在第一次调用他的时候,所以我们怀疑真正调用查询sql的地方
在这个函数里面被触发。
1 public final boolean moveToNext() { 2 return moveToPosition(mPos + 1); 3 }
1 public final boolean moveToPosition(int position) { 2 // Make sure position isn't past the end of the cursor 3 final int count = getCount(); 4 if (position >= count) { 5 mPos = count; 6 return false; 7 } 8 9 // Make sure position isn't before the beginning of the cursor 10 if (position < 0) { 11 mPos = -1; 12 return false; 13 } 14 15 // Check for no-op moves, and skip the rest of the work for them 16 if (position == mPos) { 17 return true; 18 } 19 20 boolean result = onMove(mPos, position); 21 if (result == false) { 22 mPos = -1; 23 } else { 24 mPos = position; 25 if (mRowIdColumnIndex != -1) { 26 mCurrentRowID = Long.valueOf(getLong(mRowIdColumnIndex)); 27 } 28 } 29 30 return result; 31 }
看一下那个getcount方法
1 @Override 2 public int getCount() { 3 if (mCount == NO_COUNT) { 4 fillWindow(0); 5 } 6 return mCount; 7 } 8 9 private void fillWindow(int startPos) { 10 clearOrCreateLocalWindow(getDatabase().getPath()); 11 mWindow.setStartPosition(startPos); 12 int count = getQuery().fillWindow(mWindow); 13 if (startPos == 0) { // fillWindow returns count(*) only for startPos = 0 14 if (Log.isLoggable(TAG, Log.DEBUG)) { 15 Log.d(TAG, "received count(*) from native_fill_window: " + count); 16 } 17 mCount = count; 18 } else if (mCount <= 0) { 19 throw new IllegalStateException("Row count should never be zero or negative " 20 + "when the start position is non-zero"); 21 } 22 }
发现如果满足某个条件的话 就调用fillwindow这个方法,我们来看看是什么条件
1 /** The number of rows in the cursor */ 2 private volatile int mCount = NO_COUNT; 3 static final int NO_COUNT = -1;
看到这就明白了,如果你默认的mCount为-1的话就代表你这个cursor里面还没有查过吗,所以必须要调用fillwindow方法
-
1 private synchronized SQLiteQuery getQuery() { 2 return mQuery; 3 }
我们来看看这个query是什么
1 /** The query object for the cursor */ 2 private SQLiteQuery mQuery;
看看他的fillwindow方法
1 /** 2 * Reads rows into a buffer. This method acquires the database lock. 3 * 4 * @param window The window to fill into 5 * @return number of total rows in the query 6 */ 7 /* package */ int fillWindow(CursorWindow window) { 8 mDatabase.lock(mSql); 9 long timeStart = SystemClock.uptimeMillis(); 10 try { 11 acquireReference(); 12 try { 13 window.acquireReference(); 14 int startPos = window.getStartPosition(); 15 int numRows = nativeFillWindow(nHandle, nStatement, window.mWindowPtr, 16 startPos, mOffsetIndex); 17 if (SQLiteDebug.DEBUG_LOG_SLOW_QUERIES) { 18 long elapsed = SystemClock.uptimeMillis() - timeStart; 19 if (SQLiteDebug.shouldLogSlowQuery(elapsed)) { 20 Log.d(TAG, "fillWindow took " + elapsed 21 + " ms: window=\"" + window 22 + "\", startPos=" + startPos 23 + ", offset=" + mOffsetIndex 24 + ", filledRows=" + window.getNumRows() 25 + ", countedRows=" + numRows 26 + ", query=\"" + mSql + "\"" 27 + ", args=[" + (mBindArgs != null ? 28 TextUtils.join(", ", mBindArgs.values()) : "") 29 + "]"); 30 } 31 } 32 mDatabase.logTimeStat(mSql, timeStart); 33 return numRows; 34 } catch (IllegalStateException e){ 35 // simply ignore it 36 return 0; 37 } catch (SQLiteDatabaseCorruptException e) { 38 mDatabase.onCorruption(); 39 throw e; 40 } catch (SQLiteException e) { 41 Log.e(TAG, "exception: " + e.getMessage() + "; query: " + mSql); 42 throw e; 43 } finally { 44 window.releaseReference(); 45 } 46 } finally { 47 releaseReference(); 48 mDatabase.unlock(); 49 } 50 }
一目了然,其实就是rawquery返回的是一个没有意义的cursor对象里面什么都没有,当你调用movetonext之类的方法的时候,
会判断是否里面没有数据 如果有数据就返回你要的数据,如果没有的话,实际上最终调用的就是SQLiteQuery这个类的fillwindow方法
来最终执行你写的sql语句~~耗时也就是在这里耗时!!!!!切记!不是在rawquery里耗时的!