zoukankan      html  css  js  c++  java
  • 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里耗时的!

  • 相关阅读:
    毕业设计
    毕业设计
    毕业设计
    毕业设计
    layui table
    毕业设计
    Echart图标统计
    Pxe自动化安装
    Linux运维常用脚本整理
    Zabbix 一键部署
  • 原文地址:https://www.cnblogs.com/punkisnotdead/p/4519377.html
Copyright © 2011-2022 走看看