An overview of the

Android provides SQLite interfaces for developers in the application framework layer, which is under the Android.database.sqlite package. SQLiteProgram, SQLiteDatabase, SQLiteSession, SQLiteConnectionPool, SQLiteConnection, etc. Compared to SQLite’s lightweight interface, the SQLite abstraction layer encapsulated by the application framework layer for developers is more complex, but it also shields developers from more details and makes SQLite easier to use.

By design, an SQLiteDatabase holds one SQLiteConnectionPool, which contains n SQliteconnectionpools with different connection pool capacities depending on the logging mode. Each thread’s operations on SQLiteDatabase are managed by the SQLiteSession created by ThreadLocal. SQLiteSession requires an SQLiteConnection to operate. If all connections in the database connection pool are in use at this point, it blocks until a connection is available.

SQLiteDatabase

SQLiteDatabase provides a set of methods for managing databases, including adding, deleting, modifying, and executing SQLite command statements. It highly encapsulates the execution details of SQLiteSession, SQLiteConnectionPool and SQLiteConnection. Developers only need to care about the use of the upper API.

The SQLiteDatabase open

SQLiteDatabase provides a series of open* functions that open database connections, depending on the parameters passed in.

public static SQLiteDatabase openDatabase(@NonNull String path, @NonNull OpenParams openParams);

public static SQLiteDatabase openDatabase(@NonNull String path, @Nullable CursorFactory factory, @DatabaseOpenFlags int flags);

public static SQLiteDatabase openDatabase(@NonNull String path, @Nullable CursorFactory factory, @DatabaseOpenFlags int flags, @Nullable DatabaseErrorHandler errorHandler);

public static SQLiteDatabase openOrCreateDatabase(@NonNull String path, @Nullable CursorFactory factory);

public static SQLiteDatabase openOrCreateDatabase(@NonNull String path, @Nullable CursorFactory factory, @Nullable DatabaseErrorHandler errorHandler);
Copy the code

Management activities such as database creation and version upgrade are encapsulated in SQLiteOpenHelper. SQLiteOpenHelper uses lazy initialization to create or open a database. The developer calls getReadableDatabase or getWritableDatabase to get an instance of SQLiteDatabase. And you end up with the getDatabaseLocked method.

private SQLiteDatabase getDatabaseLocked(boolean writable) {
    if(mDatabase ! =null) {
        // The database is shut down. At this point, empty the SQLiteDatabase object and reopen the database
        if(! mDatabase.isOpen()) {// Darn! The user closed the database by calling mDatabase.close().
            mDatabase = null;    
        } else if(! writable || ! mDatabase.isReadOnly()) {// If writable is false, the database is read-only
            // The database is not read-only and can be read or written to meet the requirements
            returnmDatabase; }}// Prevent repeated initialization
    if (mIsInitializing) {
        throw new IllegalStateException("getDatabase called recursively");
    }

    SQLiteDatabase db = mDatabase;
    try {
        mIsInitializing = true;

        if(db ! =null) {
            // The database needs to be writable, but the database is opened in read-only mode and needs to be opened in read-write mode again
            if(writable && db.isReadOnly()) { db.reopenReadWrite(); }}else if (mName == null) {
            // If the database name is empty, the in-memory database is to be created
            db = SQLiteDatabase.createInMemory(mOpenParamsBuilder.build());
        } else {
            final File filePath = mContext.getDatabasePath(mName);
            // Set creator mode to open parameters
            SQLiteDatabase.OpenParams params = mOpenParamsBuilder.build();
            try {
                db = SQLiteDatabase.openDatabase(filePath, params);
                // Keep pre-O-MR1 behavior by resetting file permissions to 660
                setFilePermissionsForDb(filePath.getPath());
            / / SQLiteDatabase OpenParams no configuration openFlags, default to open the database for both reading and writing
            } catch (SQLException ex) {
                // Open the database and throw an exception, if writable is required, otherwise try to open the database in read-only mode
                if (writable) {
                    throw ex;
                }
                Log.e(TAG, "Couldn't open " + mName
                        + " for writing (will try read-only):", ex); params = params.toBuilder().addOpenFlags(SQLiteDatabase.OPEN_READONLY).build(); db = SQLiteDatabase.openDatabase(filePath, params); }}// onConfigure callback to configure database related Settings
        onConfigure(db);

        // Get the current database version
        final int version = db.getVersion();
        // mNewVersion indicates the self-set version. If the two versions are different, it indicates that the database is created for the first time, or the database is upgraded or degraded (generally, the database is upgraded by changing the table structure).
        if(version ! = mNewVersion) {if (db.isReadOnly()) {
                throw new SQLiteException("Can't upgrade read-only database from version " +
                        db.getVersion() + " to " + mNewVersion + ":" + mName);
            }

            // If the database version is smaller than the minimum supported version, delete the database and rebuild it
            if (version > 0 && version < mMinimumSupportedVersion) {
                File databaseFile = new File(db.getPath());
                onBeforeDelete(db);
                db.close();
                if (SQLiteDatabase.deleteDatabase(databaseFile)) {
                    mIsInitializing = false;
                    return getDatabaseLocked(writable);
                } else {
                    throw new IllegalStateException("Unable to delete obsolete database "
                            + mName + " with version "+ version); }}else {
                // Start transaction, create DB or upgrade or downgrade
                db.beginTransaction();
                try {
                    // Create the database for the first time. The default version is 0
                    if (version == 0) {
                        onCreate(db);
                    } else {
                        if (version > mNewVersion) {
                            onDowngrade(db, version, mNewVersion);
                        } else {
                            onUpgrade(db, version, mNewVersion);
                        }
                    }
                    db.setVersion(mNewVersion);
                    db.setTransactionSuccessful();
                } finally{ db.endTransaction(); }}}// the onOpen function is called back
        onOpen(db);

        if (db.isReadOnly()) {
            Log.w(TAG, "Opened " + mName + " in read-only mode");
        }

        mDatabase = db;
        return db;
    } finally {
        mIsInitializing = false;
        if(db ! =null&& db ! = mDatabase) { db.close(); }}}Copy the code

Database opening related configuration parameter Settings:

private OpenParams(int openFlags, CursorFactory cursorFactory,
        DatabaseErrorHandler errorHandler, int lookasideSlotSize, int lookasideSlotCount,
        long idleConnectionTimeout, String journalMode, String syncMode) {
    // Open mode
    mOpenFlags = openFlags;
    mCursorFactory = cursorFactory;
    / / error handler, when selling SQLiteDatabaseCorruptionException is used
    mErrorHandler = errorHandler;
    / / the default 120 KB
    mLookasideSlotSize = lookasideSlotSize;
    / / the default 100
    mLookasideSlotCount = lookasideSlotCount;
    // Database idle connection timeout
    mIdleConnectionTimeout = idleConnectionTimeout;
    // Log mode
    mJournalMode = journalMode;
    // Synchronous mode
    mSyncMode = syncMode;
}
Copy the code

The SQLiteDatabase#Open* function called by SQLiteOpenHelper eventually opens a database master connection, which can be used for reading and writing, as discussed in the SQLiteConnectionPool section.

private void open(a) {
    try {
        try {
            openInner();
        } catch (RuntimeException ex) {
            if (SQLiteDatabaseCorruptException.isCorruptException(ex)) {
                Log.e(TAG, "Database corruption detected in open()", ex);
                onCorruption();
                openInner();
            } else {
                throwex; }}}catch (SQLiteException ex) {
        Log.e(TAG, "Failed to open database '" + getLabel() + "'.", ex);
        close();
        throwex; }}private void openInner(a) {
    synchronized (mLock) {
        assert mConnectionPoolLocked == null;
        // Open a connection pool. Inside the pool, a connection is opened. This connection is defined as the database master connection
        mConnectionPoolLocked = SQLiteConnectionPool.open(mConfigurationLocked);
        mCloseGuardLocked.open("close");
    }

    synchronized (sActiveDatabases) {
        sActiveDatabases.put(this.null); }}Copy the code

SQLiteClosable

SQLiteClosable is a class in the Android Application Framework layer designed specifically for database resource release, similar to the Closeable standard interface provided by Java for I/O. The classes at the bottom of the Android.database.sqLite package have a large number of applications to ensure that resources are released in a timely manner. Analyzing the source code, it mainly provides two interfaces: acquireReference(get reference count) and releaseReference(releaseReference count) (note: these two calls always come in pairs). When the reference count is 0, all related resource connections are released, and when the reference count is already 0, an IllegalStateException is thrown if the reference count is retrieved again.

protected abstract void onAllReferencesReleased(a);

public void acquireReference(a) {
    synchronized(this) {
        if (mReferenceCount <= 0) {
            throw new IllegalStateException(
                    "attempt to re-open an already-closed object: " + this); } mReferenceCount++; }}public void releaseReference(a) {
    boolean refCountIsZero = false;
    synchronized(this) {
        refCountIsZero = --mReferenceCount == 0;
    }
    if(refCountIsZero) { onAllReferencesReleased(); }}Copy the code

// TODO analyzes the cause of IllegalStateException

SQLiteSession

Each operation of SQLiteDatabase needs to be done by SQLiteSession. Each thread holds at most one SQLiteSession for a database, which is technically guaranteed by ThreadLocal. This restriction ensures that a thread cannot use more than one database connection at a time for a given database, ensuring that database use within a single process does not generate deadlocks. Regarding transaction management, SQLiteSession provides support for SQLite’s three transaction modes and supports transaction nesting. To analyze the role SQLiteSession plays, we need to understand the locking and transaction mechanisms in SQLite beforehand.

SQLite locking mechanism

SQLite uses extensive locks. When a connection writes to the database, all other connections are locked until the write connection terminates its transaction. SQLite has a lock table to help different write databases lock again at the last minute to ensure maximum concurrency. SQLite uses a lock escalation mechanism, where connections need to acquire exclusive locks step by step in order to write to the database. SQLite has five different lock states, and each database connection can only be in one of them at a time.

The lock state Lock description
UNLOCKED – UNLOCKED The database is not read or written at this time, and other threads or processes can read or write to the database as their lock state allows.
Sharing – a SHARED lock The database can be read but not written. Multiple threads or processes can acquire a SHARED lock at the same time, so multiple readers can be available at the same time. When there is one or more shared locks, no other threads or processes are allowed to write to the database file.
Keep lock – RESERVED The RESERVED lock means that the process is ready to write to the database file, but it is currently only reading from the file. Only one RESERVED lock can be created at a time. Multiple SHARED locks can coexist with one RESERVED lock. RESERVED differs from PENDING in that when a RESERVED lock is present, a new SHARED lock can be acquired.
Lock – PENDING PENDING locks mean that the process holding the lock wants to write to the database as soon as possible and is simply waiting for all current SHARED locks to be released so that it can acquire the EXCLUSIVE lock. If a PENDING lock is active, new SHARED locks are not allowed to be acquired, but existing SHARED locks are allowed to continue to be used.
EXCLUSIVE lock – EXCLUSIVE Only one EXCLUSIVE lock can be written to a database at a time, and no other locks are allowed to coexist with it. To maximize concurrency, SQLite should minimize the time spent holding exclusive locks.

The transaction type of SQLite

SQLite allows multiple database connections to initiate read transactions at the same time, but only one write transaction can be initiated at a time. Read transactions are only used for reading, while write transactions allow reading and writing. Read transactions are started by the SELECT statement, and write transactions are started by statements such as CREATE, DELETE, DROP, INSERT, or UPDATE (collectively referred to as “write statements”). SQLite provides three different transaction types that start transactions in different lock states: DEFERRED, IMMEDIATE, and EXCLUSIVE. The default transaction type is DEFERRED. Transactions are specified in the BEGIN type:

BEGIN [ DEFERRED | IMMEDIATE | EXCLUSIVE ] TRANSACTION;
Copy the code

BEGIN DEFERRED enabled transactions do not acquire any locks until it needs them. The BEGIN statement does nothing by itself; it begins in UNLOCK state. By default, if a transaction is only started with BEGIN, the transaction is DEFERRED and it does not acquire any locks. When the database is read for the first time, it acquires the SHARED lock. Also, it gets the RESERVED lock when it does the first write.

BEGIN IMMEDIATE The enabled transaction attempts to obtain the RESERVED lock. If successful, BEGIN IMMEDIATE ensures that no other connection can write to the database. However, other connections can read the database. However, the RESERVED lock prevents the BEGIN IMMEDIATE or BEGIN EXCLUSIVE commands for other connections. SQLITE_BUSY error is returned when the above command is executed for other connections. If the SQLITE_BUSY error is returned when the COMMIT operation is performed, it means that there are other read transactions that have not completed and must wait for them to complete before committing the transaction.

BEGIN EXCLUSIVE Started transactions attempt to acquire the EXCLUSIVE lock on the database. This is similar to BEGIN IMMEDIATE, but once successful, an EXCLUSIVE transaction guarantees that there are no other connections, so you can read and write to the database. EXCLUSIVE and IMMEDIATE are the same in WAL mode, but in other logging modes, EXCLUSIVE prevents other database connections from reading the database while a transaction is in progress.

Start and end of a transaction

The previous two sections introduced locking mechanisms and transaction-related concepts in SQLite, but now look at how the Android Application Framework layer encapsulates them. SQLiteDatabase provides several methods to start a transaction. The main difference between these methods is that the parameters passed in are different. The final actual call public void beginTransaction (SQLiteTransactionListener transactionListener, Boolean exclusive) method. Of particular importance in this method is the EXCLUSIVE argument, which determines the type of transaction to open. BEGIN EXCLUSIVE is called when true; Otherwise, run BEGIN IMMEDIATE.

public void beginTransaction(a); // 实际调用 beginTransaction(null, true)

public void beginTransactionNonExclusive(a); // 实际调用 beginTransaction(null, false)

public void beginTransactionWithListener(SQLiteTransactionListener transactionListener);    // Actually call beginTransaction(transactionListener, true)

public void beginTransactionWithListenerNonExclusive(SQLiteTransactionListener transactionListener));   // Actually call beginTransaction(transactionListener, false)

public void beginTransaction(SQLiteTransactionListener transactionListener, boolean exclusive);
Copy the code

A series of open transaction methods of SQLiteDatabase eventually go SQLiteSession# beginTransactionUnchecked. The stack data structure preserves the relationship between nested transactions, which are executed within the same database connection. If any nested transaction fails to execute, the entire transaction, including all its nested transactions, is rolled back when the outermost transaction ends.

private void beginTransactionUnchecked(int transactionMode,
        SQLiteTransactionListener transactionListener, int connectionFlags,
        CancellationSignal cancellationSignal) {
    if(cancellationSignal ! =null) {
        cancellationSignal.throwIfCanceled();
    }

    When a nested transaction occurs, the topmost transaction obtains a connection to the database. The nested transactions use this connection to operate on the database
    if (mTransactionStack == null) {
        acquireConnection(null, connectionFlags, cancellationSignal); // might throw
    }
    try {
        / / transactionMode in upper call SQLiteDatabase# beginTransaction (SQLiteTransactionListener transactionListener, Boolean EXCLUSIVE),
        // transactionMode = exclusive ? SQLiteSession.TRANSACTION_MODE_EXCLUSIVE :SQLiteSession.TRANSACTION_MODE_IMMEDIATE
        if (mTransactionStack == null) {
            // Execute SQL might throw a runtime exception.
            switch (transactionMode) {
                case TRANSACTION_MODE_IMMEDIATE:
                    mConnection.execute("BEGIN IMMEDIATE;".null,
                            cancellationSignal); // might throw
                    break;
                case TRANSACTION_MODE_EXCLUSIVE:
                    mConnection.execute("BEGIN EXCLUSIVE;".null,
                            cancellationSignal); // might throw
                    break;
                default:
                    mConnection.execute("BEGIN;".null, cancellationSignal); // might throw
                    break; }}// Listener might throw a runtime exception.
        if(transactionListener ! =null) {
            try {
                transactionListener.onBegin(); // might throw
            } catch (RuntimeException ex) {
                if (mTransactionStack == null) {
                    mConnection.execute("ROLLBACK;".null, cancellationSignal); // might throw
                }
                throwex; }}// Reuse objects from the object pool, which is a linked list that fetches useless objects from the end of the queue each time
        Transaction transaction = obtainTransaction(transactionMode, transactionListener);
        Support for nested transactions by storing transactions on a stack, with the innermost transaction at the top
        transaction.mParent = mTransactionStack;
        mTransactionStack = transaction;
    } finally {
        if (mTransactionStack == null) {
            releaseConnection(); // might throw}}}private void endTransactionUnchecked(CancellationSignal cancellationSignal, boolean yielding) {
    if(cancellationSignal ! =null) {
        cancellationSignal.throwIfCanceled();
    }

    // Get the transaction at the top of the stack
    final Transaction top = mTransactionStack;

    // A case of yielding true for a transaction that is marked as successful or yielding temporarily and its subordinate transaction did not fail is a call to yieldTransaction
    booleansuccessful = (top.mMarkedSuccessful || yielding) && ! top.mChildFailed; RuntimeException listenerException =null;
    final SQLiteTransactionListener listener = top.mListener;
    if(listener ! =null) {
        try {
            if (successful) {
                listener.onCommit(); // might throw
            } else {
                listener.onRollback(); // might throw}}catch (RuntimeException ex) {
            listenerException = ex;
            successful = false; }}// The transaction at the top of the current stack
    mTransactionStack = top.mParent;
    // Cache useless transaction objects into the object pool
    recycleTransaction(top);

    // Check whether all transactions in the stack are completed
    if(mTransactionStack ! =null) {
        if(! successful) {// Marks the failure of the subordinate transaction
            mTransactionStack.mChildFailed = true; }}else {
        // The transaction is already at the bottom of the stack. If there is only one failure in the whole transaction, the whole transaction will fail
        try {
            // Commit the changes for this transaction
            if (successful) {
                mConnection.execute("COMMIT;".null, cancellationSignal); // might throw
            } else {
                // Roll back the changes made in this transaction
                mConnection.execute("ROLLBACK;".null, cancellationSignal); // might throw}}finally {
            releaseConnection(); // might throw}}if(listenerException ! =null) {
        throwlistenerException; }}Copy the code

Now let’s look at beginTransactionUnchecked connectionFlags determination method. By SQLiteDatabase# beginTransaction (SQLiteTransactionListener transactionListener, Boolean exclusive) will call into the following code, ConnectionFlags will be flagged as requiring the primary connection. Therefore, calling the beginTransaction method explicitly in your code, even if you are executing a read statement, will fetch the primary connection, and only one primary connection will be fetched at the same time, thus causing performance degradation. So there is no need to explicitly call the transaction open method if the read statement is not necessary.

@UnsupportedAppUsage
private void beginTransaction(SQLiteTransactionListener transactionListener,
        boolean exclusive) {
    acquireReference();
    try {
        getThreadSession().beginTransaction(
                exclusive ? SQLiteSession.TRANSACTION_MODE_EXCLUSIVE :
                        SQLiteSession.TRANSACTION_MODE_IMMEDIATE,
                transactionListener,
                getThreadDefaultConnectionFlags(false /*readOnly*/), null);
    } finally{ releaseReference(); }}int getThreadDefaultConnectionFlags(boolean readOnly) {
    int flags = readOnly ? SQLiteConnectionPool.CONNECTION_FLAG_READ_ONLY :
            SQLiteConnectionPool.CONNECTION_FLAG_PRIMARY_CONNECTION_AFFINITY;
    if (isMainThread()) {
        flags |= SQLiteConnectionPool.CONNECTION_FLAG_INTERACTIVE;
    }
    return flags;
}
Copy the code

SQLiteConnectionPool

SQLiteConnectionPool is primarily used to cache database connections and contains one primary connection (with read/write capability) and several non-primary connections (with read-only capability).

Creation of a connection pool

// Determine the size of the connection pool in the constructor
private SQLiteConnectionPool(SQLiteDatabaseConfiguration configuration) {
    mConfiguration = new SQLiteDatabaseConfiguration(configuration);
    setMaxConnectionPoolSizeLocked();
    // If timeout is set, setup idle connection handler
    // In case of MAX_VALUE - idle connections are never closed
    if (mConfiguration.idleConnectionTimeoutMs != Long.MAX_VALUE) {
        setupIdleConnectionHandler(Looper.getMainLooper(),
                mConfiguration.idleConnectionTimeoutMs);
    }
}

// for non-memory databases with WAL enabled, the poolSize is determined by the built-in configuration. Generally, the poolSize is 4, depending on the internal configuration of different models, including 1 primary connection and poolsize-1 non-primary connection.
// The minimum connection pool size is 2
private void setMaxConnectionPoolSizeLocked(a) {
    if(! mConfiguration.isInMemoryDb() && (mConfiguration.openFlags & SQLiteDatabase.ENABLE_WRITE_AHEAD_LOGGING) ! =0) {
        mMaxConnectionPoolSize = SQLiteGlobal.getWALConnectionPoolSize();
    } else {
        // We don't actually need to always restrict the connection pool size to 1
        // for non-WAL databases. There might be reasons to use connection pooling
        // with other journal modes. However, we should always keep pool size of 1 for in-memory
        // databases since every :memory: db is separate from another.
        // For now, enabling connection pooling and using WAL are the same thing in the API.
        mMaxConnectionPoolSize = 1; }}public static int getWALConnectionPoolSize(a) {
    int value = SystemProperties.getInt("debug.sqlite.wal.poolsize",
            Resources.getSystem().getInteger(
            com.android.internal.R.integer.db_connection_pool_size));
    return Math.max(2, value);
}

// A database master connection is opened in the connection pool
private void open(a) {
    // Open the primary connection.
    // This might throw if the database is corrupt.
    mAvailablePrimaryConnection = openConnectionLocked(mConfiguration,
            true /*primaryConnection*/); // might throw
    // Mark it released so it can be closed after idle timeout
    synchronized (mLock) {
        if(mIdleConnectionHandler ! =null) { mIdleConnectionHandler.connectionReleased(mAvailablePrimaryConnection); }}// Mark the pool as being open for business.
    mIsOpen = true;
    mCloseGuard.open("close");
}
Copy the code

Get connected

SQLiteSession calls SQLiteConnectionPool#acquireConnection to obtain a database connection before operating on the database.

// SQLiteDatabase#getThreadDefaultConnectionFlags
int getThreadDefaultConnectionFlags(boolean readOnly) {
    int flags = readOnly ? SQLiteConnectionPool.CONNECTION_FLAG_READ_ONLY :
            SQLiteConnectionPool.CONNECTION_FLAG_PRIMARY_CONNECTION_AFFINITY;
    if (isMainThread()) {
        flags |= SQLiteConnectionPool.CONNECTION_FLAG_INTERACTIVE;
    }
    return flags;
}

/ / connectionFlags said connection type, its through SQLiteDatabase# getThreadDefaultConnectionFlags determined
public SQLiteConnection acquireConnection(String sql, int connectionFlags,
        CancellationSignal cancellationSignal) {
    SQLiteConnection con = waitForConnection(sql, connectionFlags, cancellationSignal);
    synchronized (mLock) {
        if(mIdleConnectionHandler ! =null) { mIdleConnectionHandler.connectionAcquired(con); }}return con;
}

// Wait for an available connection
private SQLiteConnection waitForConnection(String sql, int connectionFlags,
        CancellationSignal cancellationSignal) {
    // Use connectionFlags to determine if it is the primary connection. WantPrimaryConnection is true if write capability is required
    final booleanwantPrimaryConnection = (connectionFlags & CONNECTION_FLAG_PRIMARY_CONNECTION_AFFINITY) ! =0;

    final ConnectionWaiter waiter;
    final int nonce;
    synchronized (mLock) {
        throwIfClosedLocked();

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

        // Try to acquire a connection.
        SQLiteConnection connection = null;
        // If a non-primary connection is required, try to obtain a connection from the available connection pool. If the pool limit is not reached, create a database connection
        if(! wantPrimaryConnection) { connection = tryAcquireNonPrimaryConnectionLocked( sql, connectionFlags);// might throw
        }
        // No non-primary connection is available or a primary connection is required
        if (connection == null) {
            connection = tryAcquirePrimaryConnectionLocked(connectionFlags); // might throw
        }
        if(connection ! =null) {
            return connection;
        }

        // There is no main connection available, insert queue according to connection priority, if the connection is obtained on the main thread, it is high priority
        final int priority = getPriority(connectionFlags);
        final long startTime = SystemClock.uptimeMillis();
        waiter = obtainConnectionWaiterLocked(Thread.currentThread(), startTime,
                priority, wantPrimaryConnection, sql, connectionFlags);
        ConnectionWaiter predecessor = null;
        ConnectionWaiter successor = mConnectionWaiterQueue;
        // Maintain a waiting list, starting at the head of the list to find the insertion location
        while(successor ! =null) {
            if (priority > successor.mPriority) {
                waiter.mNext = successor;
                break;
            }
            predecessor = successor;
            successor = successor.mNext;
        }
        if(predecessor ! =null) {
            predecessor.mNext = waiter;
        } else {
            mConnectionWaiterQueue = waiter;
        }

        nonce = waiter.mNonce;
    }

    // Set up the cancellation listener.
    if(cancellationSignal ! =null) {
        cancellationSignal.setOnCancelListener(new CancellationSignal.OnCancelListener() {
            @Override
            public void onCancel(a) {
                synchronized (mLock) {
                    if(waiter.mNonce == nonce) { cancelConnectionWaiterLocked(waiter); }}}}); }try {
        / / the default 30 s
        long busyTimeoutMillis = CONNECTION_POOL_BUSY_MILLIS;
        long nextBusyTimeoutTime = waiter.mStartTime + busyTimeoutMillis;
        for (;;) {
            // Detect a leaking connection and use it
            if (mConnectionLeaked.compareAndSet(true.false)) {
                synchronized (mLock) {
                    // Wake up waitwakeConnectionWaitersLocked(); }}// Sleep the current thread for up to 30s
            LockSupport.parkNanos(this, busyTimeoutMillis * 1000000L);

            // Clear the interrupted flag, just in case.
            Thread.interrupted();

            // Check whether we are done waiting yet.
            synchronized (mLock) {
                throwIfClosedLocked();

                final SQLiteConnection connection = waiter.mAssignedConnection;
                final RuntimeException ex = waiter.mException;
                // Whether the free available connection is given to the current waiting waiter, if so, returns directly
                if(connection ! =null|| ex ! =null) {
                    recycleConnectionWaiterLocked(waiter);
                    if(connection ! =null) {
                        return connection;
                    }
                    throw ex; // rethrow!
                }

                // No available connection was obtained, so the sleep time was reset
                final long now = SystemClock.uptimeMillis();
                if (now < nextBusyTimeoutTime) {
                    busyTimeoutMillis = now - nextBusyTimeoutTime;
                } else{ logConnectionPoolBusyLocked(now - waiter.mStartTime, connectionFlags); busyTimeoutMillis = CONNECTION_POOL_BUSY_MILLIS; nextBusyTimeoutTime = now + busyTimeoutMillis; }}}}finally {
        // Remove the cancellation listener.
        if(cancellationSignal ! =null) {
            cancellationSignal.setOnCancelListener(null); }}}Copy the code

Release the connection

public void releaseConnection(SQLiteConnection connection) {
    synchronized (mLock) {
        if(mIdleConnectionHandler ! =null) {
            mIdleConnectionHandler.connectionReleased(connection);
        }
        AcquiredConnectionStatus status = mAcquiredConnections.remove(connection);
        if (status == null) {
            throw new IllegalStateException("Cannot perform this operation "
                    + "because the specified connection was not acquired "
                    + "from this pool or has already been released.");
        }

        // The database is already closed
        if(! mIsOpen) { closeConnectionAndLogExceptionsLocked(connection); }else if (connection.isPrimaryConnection()) {
            // The main connection is not to be closed, then assign
            if (recycleConnectionLocked(connection, status)) {
                assert mAvailablePrimaryConnection == null;
                mAvailablePrimaryConnection = connection;
            }
            // Wake up the thread waiting to connect
            wakeConnectionWaitersLocked();
        // The non-primary connection exceeds the specified size of the pool and is closed
        } else if (mAvailableNonPrimaryConnections.size() >= mMaxConnectionPoolSize - 1) {
            closeConnectionAndLogExceptionsLocked(connection);
        } else {
            if (recycleConnectionLocked(connection, status)) {
                mAvailableNonPrimaryConnections.add(connection);
            }
            // Wake up the thread waiting to connectwakeConnectionWaitersLocked(); }}}Copy the code

Refer to the link

  • SQLite Locking
  • SQLite Transaction
  • Writing-Ahead Logging