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. 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里耗时的!

 

posted @ 2015-05-21 12:19  希尔瓦娜斯女神  阅读(1727)  评论(0编辑  收藏  举报