preface

In the first two articles, we analyzed the core source code of Android’s network infrastructure framework OKHttp and encapsulation framework Retrofit in detail. If you are not familiar with OKHttp or Retrofit’s internal mechanics, check out the main Android triad library source code analysis (a) and Android triad library source code analysis (b). In addition to the popular network library source code, We also analyzed the most widely used picture loading framework Glide loading process, we read this source analysis strength will have a lot of improvement, interested can look at the Android mainstream three party library source code analysis (three, deep understanding of Glide source code). In this article, we will take a closer look at GreenDao, currently the best performing Android database framework.

1. Basic usage process

1. Import GreenDao’s code generation plug-ins and libraries

// Build. Gradle buildscript {... Dependencies {classpath 'com. Android. View the build: gradle: 2.3.0' classpath 'org. Greenrobot: greendao - gradle - plugin: 3.2.1' }} // Build. Gradle apply plugin: 'com.android.application' apply Plugin: 'org.greenrobot. Greendao '... dependencies { ... The compile 'org. Greenrobot: greendao: 3.2.0'}Copy the code

Create an entity class, in this case HistoryData

@Entity
public class HistoryData {

    @Id(autoincrement = true)
    private Long id;

    private long date;

    private String data;
}
Copy the code

3, Select ReBuild Project, HistoryData will be automatically added to Set/get method, and generate the DaoMaster, DaoSession class of the entire Project, and HistoryDataDao corresponding to the entity HistoryData.

@Entity public class HistoryData { @Id(autoincrement = true) private Long id; private long date; private String data; @Generated(hash = 1371145256) public HistoryData(Long id, long date, String data) { this.id = id; this.date = date; this.data = data; } @Generated(hash = 422767273) public HistoryData() { } public Long getId() { return this.id; } public void setId(Long id) { this.id = id; } public long getDate() { return this.date; } public void setDate(long date) { this.date = date; } public String getData() { return this.data; } public void setData(String data) { this.data = data; }}Copy the code

Here’s how these classes work:

  • DaoMaster: The owner of all Dao classes, responsible for running the entire library. The internal static abstract subclass DevOpenHelper inherits and overwrites Android’s SqliteOpenHelper.
  • DaoSession: as a role at the session layer, it is used to generate corresponding Dao objects, register Dao objects, and operate Dao objects.
  • XxDao (HistoryDataDao) : generated Dao objects used to perform specific database operations.

4. Obtain and use the corresponding Dao object to add, delete, change and check

DaoMaster.DevOpenHelper devOpenHelper = new DaoMaster.DevOpenHelper(this, Constants.DB_NAME); SQLiteDatabase database = devOpenHelper.getWritableDatabase(); DaoMaster daoMaster = new DaoMaster(database); mDaoSession = daoMaster.newSession(); HistoryDataDao historyDataDao = daoSession.getHistoryDataDao(); // omit the code to create historyData... / / add historyDataDao. Insert (historyData); / / delete historyDataDao. Delete (historyData); / / change historyDataDao. Update (historyData); List<HistoryData> historyDataList = historyDatao.loadAll ();Copy the code

This article will use the above procedures to analyze the source code of GreenDao step by step. Finally, it will analyze some excellent features of GreenDao, so that readers can have a deeper understanding of GreenDao.

Ii. GreenDao use process analysis

Create database helper object daomaster.devOpenHelper

DaoMaster.DevOpenHelper devOpenHelper = new DaoMaster.DevOpenHelper(this, Constants.DB_NAME);
Copy the code

Create GreenDao internal database help class object devOpenHelper, the core source code is as follows:

public class DaoMaster extends AbstractDaoMaster { ... public static abstract class OpenHelper extends DatabaseOpenHelper { ... @Override public void onCreate(Database db) { Log.i("greenDAO", "Creating tables for schema version " + SCHEMA_VERSION);  createAllTables(db, false); } } public static class DevOpenHelper extends OpenHelper { ... @Override public void onUpgrade(Database db, int oldVersion, int newVersion) { Log.i("greenDAO", "Upgrading schema from version " + oldVersion + " to " + newVersion + " by dropping all tables"); dropAllTables(db, true); onCreate(db); }}}Copy the code

DevOpenHelper implements the updated logic itself, which is to discard all tables and call the onCreate method implemented by OpenHelper to create all tables. DevOpenHelper inherits from OpenHelper, OpenHelper itself inherits from DatabaseOpenHelper, so what does DatabaseOpenHelper do?

public abstract class DatabaseOpenHelper extends SQLiteOpenHelper { ... Public Database getWritableDb() {return wrap(getWritableDatabase()); } public Database getReadableDb() { return wrap(getReadableDatabase()); } protected Database wrap(SQLiteDatabase sqLiteDatabase) { return new StandardDatabase(sqLiteDatabase); }... Public Database getEncryptedWritableDb(String Password) {EncryptedHelper = EncryptedHelper checkEncryptedHelper(); return encryptedHelper.wrap(encryptedHelper.getWritableDatabase(password)); } public Database getEncryptedReadableDb(String password) { EncryptedHelper encryptedHelper = checkEncryptedHelper(); return encryptedHelper.wrap(encryptedHelper.getReadableDatabase(password)); }... private class EncryptedHelper extends net.sqlcipher.database.SQLiteOpenHelper { ... protected Database wrap(net.sqlcipher.database.SQLiteDatabase sqLiteDatabase) { return new EncryptedDatabase(sqLiteDatabase); }}Copy the code

DatabaseOpenHelper is a helper class that implements SQLiteOpenHelper. There are two different types of database that can be retrieved from DatabaseOpenHelper. The other type of database is EncryptedDatabase, which internally returns the corresponding database type via wrap.

Public class StandardDatabase implements Database {/ / the SQLiteDatabase here is android. Database. Sqlite. SQLiteDatabase package private final SQLiteDatabase delegate; public StandardDatabase(SQLiteDatabase delegate) { this.delegate = delegate; } @Override public Cursor rawQuery(String sql, String[] selectionArgs) { return delegate.rawQuery(sql, selectionArgs); } @Override public void execSQL(String sql) throws SQLException { delegate.execSQL(sql); }... } public class EncryptedDatabaseStatement implements DatabaseStatement { // The SQLiteStatement here is net. Sqlcipher. Database. SQLiteStatement package under the private final SQLiteStatement delegate; public EncryptedDatabaseStatement(SQLiteStatement delegate) { this.delegate = delegate; } @Override public void execute() { delegate.execute(); }... }Copy the code

StandardDatabase and EncryptedDatabase both use the proxy mode internally to add different implementations to the same interface. StandardDatabase naturally uses SQLiteDatabase under the Android package. And EncryptedDatabaseStatement in order to realize the encrypted database functions, the use of a database encryption called sqlcipher tripartite library, a database if your project requires preservation of the more important data, GetEncryptedWritableDb method can be used instead of getdWritableDb method to encrypt the database, so that our subsequent database operations will indirectly use the API provided by sqlcipher in the form of proxy mode to operate the database.

Create a DaoMaster object

SQLiteDatabase database = devOpenHelper.getWritableDatabase();
DaoMaster daoMaster = new DaoMaster(database);
Copy the code

First, the DaoMaster, as the host of all Dao objects, must have an internal SQLiteDatabase object, so, A DaoMaster object is created from the getWritableDatabase method of devOpenHelper.

public class DaoMaster extends AbstractDaoMaster { ... public DaoMaster(SQLiteDatabase db) { this(new StandardDatabase(db)); } public DaoMaster(Database db) { super(db, SCHEMA_VERSION); registerDaoClass(HistoryDataDao.class); }... }Copy the code

In the constructor for DaoMaster, it first executes the super(DB, SCHEMA_VERSION) method, which is the constructor for its parent, AbstractDaoMaster.

public abstract class AbstractDaoMaster { ... public AbstractDaoMaster(Database db, int schemaVersion) { this.db = db; this.schemaVersion = schemaVersion; daoConfigMap = new HashMap<Class<? extends AbstractDao<? ,? >>, DaoConfig>(); } protected void registerDaoClass(Class<? extends AbstractDao<? ,? >> daoClass) { DaoConfig daoConfig = new DaoConfig(db, daoClass); daoConfigMap.put(daoClass, daoConfig); }... }Copy the code

In the constructor of an AbstractDaoMaster object, in addition to recording the current database object DB and version SchemScented, A daoConfigMap object of type HashMap

, DaoConfig>() is also created to hold DaoConfig, the data configuration object corresponding to each DAO, and DaoConfig holds the data required for the DAO. Finally, the HistoryDataDao class object is registered using the registerDaoClass(HistoryDatadao.class) method in the DaoMaster constructor. In fact, Create an DaoConfig object for the HistoryDataDao Dao object and store it in the daoConfigMap object.

Create DaoSession object

mDaoSession = daoMaster.newSession();
Copy the code

Create a DaoSession object using the newSession method in the DaoMaster object.

public DaoSession newSession() {
    return new DaoSession(db, IdentityScopeType.Session, daoConfigMap);
}
Copy the code

What else is done in the DaoSeesion construction method?

public class DaoSession extends AbstractDaoSession {

    ...

    public DaoSession(Database db, IdentityScopeType type, Map<Class<?     extends AbstractDao<?, ?>>, DaoConfig>
            daoConfigMap) {
        super(db);

        historyDataDaoConfig = daoConfigMap.get(HistoryDataDao.class).clone();
        historyDataDaoConfig.initIdentityScope(type);

        historyDataDao = new HistoryDataDao(historyDataDaoConfig, this);

        registerDao(HistoryData.class, historyDataDao);
    }
    
    ...
}
Copy the code

First, a constructor of the parent class AbstractDaoSession is called.

public class AbstractDaoSession { ... public AbstractDaoSession(Database db) { this.db = db; this.entityToDao = new HashMap<Class<? >, AbstractDao<? ,? > > (); } protected <T> void registerDao(Class<T> entityClass, AbstractDao<T, ? > dao) { entityToDao.put(entityClass, dao); }... }Copy the code

The AbstractDaoSession constructor creates a set of mappings between entities and Dao objects. Next, two more things are done in DaoSession’s constructor:

  • Create DaoConfig object for each Dao, in this case historyDataDaoConfig, and initialize a corresponding IdentityScope based on the type of IdentityScopeType. IdentityScopeObject and IdentityScopeLong, which cache entity data based on the primary key. When the primary key is numeric, such as long/ long, int/Integer, short/ short, and byte/ byte, the entity data is cached using IdentityScopeLong. When the primary key is not numeric, the entity data is cached using IdentityScopeLong. Use IdentityScopeObject to cache entity data.
  • Create Dao historyDataDao object according to DaoSession object and DaoConfig object corresponding to each Dao. Create Dao historyDataDao object based on DaoSession object and DaoConfig object. The next step is to register the Dao object, which is to store the entity and the corresponding Dao object in the entityToDao mapping collection.

4. Insert source code analysis

HistoryDataDao historyDataDao = daoSession.getHistoryDataDao(); / / add historyDataDao. Insert (historyData);Copy the code

The Dao object HistoryDataDao is obtained in DaoSession, and then a historyData entity object is inserted. HistoryDataDao inherits AbstractDao<HistoryData, Long>.

public class HistoryDataDao extends AbstractDao<HistoryData, Long> {
    ...
}
Copy the code

So, what does this AbstractDao do?

public abstract class AbstractDao<T, K> { ... public List<T> loadAll() { Cursor cursor = db.rawQuery(statements.getSelectAll(), null); return loadAllAndCloseCursor(cursor); }... public long insert(T entity) { return executeInsert(entity, statements.getInsertStatement(), true); }... public void delete(T entity) { assertSinglePk(); K key = getKeyVerified(entity); deleteByKey(key); }... }Copy the code

AbstractDao is the base class for all Dao objects. It implements operations on entity data such as add, delete, change and query. AbstractDao insert method is called in AbstractDao insert method. In this method, the statements in the second argument is a TableStatements object that is fetched from the DaoConfig object when AbstractDao initializes the constructor, and is a helper class that creates SQL statements based on the specified table. Use statements. GetInsertStatement () is to obtain an insert statement. The third parameter is a flag to determine whether it is a primary key.

public class TableStatements { ... public DatabaseStatement getInsertStatement() { if (insertStatement == null) { String sql = SqlUtils.createSqlInsert("INSERT INTO ", tablename, allColumns); DatabaseStatement newInsertStatement = db.compileStatement(sql); . } return insertStatement; }... }Copy the code

In the TableStatements getInsertStatement method, two main things are done:

  • Create the inserted SQL statement using SqlUtils.
  • 2, according to different database types (standard database or encrypted database) to compile SQL statements into the current database corresponding statements.

Let’s move on to the executeInsert execution process.

private long executeInsert(T entity, DatabaseStatement stmt, boolean setKeyAndAttach) {
    long rowId;
    if (db.isDbLockedByCurrentThread()) {
        rowId = insertInsideTx(entity, stmt);
    } else {
        db.beginTransaction();
        try {
            rowId = insertInsideTx(entity, stmt);
            db.setTransactionSuccessful();
        } finally {
            db.endTransaction();
        }
    }
    if (setKeyAndAttach) {
        updateKeyAfterInsertAndAttach(entity, rowId, true);
    }
    return rowId;
}
Copy the code

Check whether the database is locked by the current thread. If yes, insert data directly. Otherwise, start a database transaction and insert data again to avoid deadlocks. Finally, if a primary key is set, the value of the primary key is updated after data is inserted and the corresponding entity is cached into the corresponding identityScope. The code flow for this section is as follows:

protected void updateKeyAfterInsertAndAttach(T entity, long rowId, boolean lock) { if (rowId ! = -1) { K key = updateKeyAfterInsert(entity, rowId); attachEntity(key, entity, lock); } else { ... } } protected final void attachEntity(K key, T entity, boolean lock) { attachEntity(entity); if (identityScope ! = null && key ! = null) { if (lock) { identityScope.put(key, entity); } else { identityScope.putNoLock(key, entity); }}}Copy the code

Then, following the main thread, insertInsideTx is called in the executeInsert method to insert data.

private long insertInsideTx(T entity, DatabaseStatement stmt) { synchronized (stmt) { if (isStandardSQLite) { SQLiteStatement rawStmt = (SQLiteStatement) stmt.getRawStatement(); bindValues(rawStmt, entity); return rawStmt.executeInsert(); } else { bindValues(stmt, entity); return stmt.executeInsert(); }}}Copy the code

In order to prevent concurrency, pessimistic locks are used to ensure data consistency. In AbstractDao, this kind of lock is widely used to ensure thread-safety. Then, if the current database is a standard database, the original statement corresponding to the STMT DatabaseStatement class is retrieved for binding of the entity field attributes and, finally, performing the insert operation. In the case of an encrypted database, the entity field attributes are bound and the final insert is performed directly using the insert statement to which the current encrypted database belongs. The implementation class for the bindValues method is our HistoryDataDao class.

public class HistoryDataDao extends AbstractDao<HistoryData, Long> { ... @Override protected final void bindValues(DatabaseStatement stmt, HistoryData entity) { stmt.clearBindings(); Long id = entity.getId(); if (id ! = null) { stmt.bindLong(1, id); } stmt.bindLong(2, entity.getDate()); String data = entity.getData(); if (data ! = null) { stmt.bindString(3, data); } } @Override protected final void bindValues(SQLiteStatement stmt, HistoryData entity) { stmt.clearBindings(); Long id = entity.getId(); if (id ! = null) { stmt.bindLong(1, id); } stmt.bindLong(2, entity.getDate()); String data = entity.getData(); if (data ! = null) { stmt.bindString(3, data); }}... }Copy the code

As you can see, all of the HistoryData fields are bound using the corresponding database statements. Finally, mentioning here, if the current database is encrypted, will use the most began to mention DatabaseStatement encryption implementation class EncryptedDatabaseStatement application proxy mode to use sqlcipher the encryption type database insert method.

5, query source code analysis

After the analysis of the inserted source code, I believe that you have some understanding of the internal mechanism of GreenDao, because the process of deleting and updating the internal is relatively simple, and with the inserted source code has the same wonderful, here will not repeat the details. Finally, we analyze the source code of the query, query process call chain is long, so its core process source code is directly given.

List<HistoryData> historyDataList = historyDataDao.loadAll(); public List<T> loadAll() { Cursor cursor = db.rawQuery(statements.getSelectAll(), null); return loadAllAndCloseCursor(cursor); } protected List<T> loadAllAndCloseCursor(Cursor cursor) { try { return loadAllFromCursor(cursor); } finally { cursor.close(); } } protected List<T> loadAllFromCursor(Cursor cursor) { int count = cursor.getCount(); . boolean useFastCursor = false; if (cursor instanceof CrossProcessCursor) { window = ((CrossProcessCursor) cursor).getWindow(); if (window ! = null) { if (window.getNumRows() == count) { cursor = new FastCursor(window); useFastCursor = true; } else { ... } } } if (cursor.moveToFirst()) { ... try { if (! useFastCursor && window ! = null && identityScope ! = null) { loadAllUnlockOnWindowBounds(cursor, window, list); } else { do { list.add(loadCurrent(cursor, 0, false)); } while (cursor.moveToNext()); } } finally { ... } } return list; }Copy the code

Finally, the loadAll method calls the loadAllFromCursor method. First, if the current cursor is a cross-process cursor and the number of rows is the same, an accelerated FastCursor object is used for cursor traversal. Then, both perform loadAllUnlockOnWindowBounds this method and direct load current data list. Add (loadCurrent (cursor, 0, false)), The list. Add (loadCurrent(cursor, 0, false)) code is called at the end of the list.

final protected T loadCurrent(Cursor cursor, int offset, boolean lock) { if (identityScopeLong ! = null) { ... T entity = lock ? identityScopeLong.get2(key) : identityScopeLong.get2NoLock(key); if (entity ! = null) { return entity; } else { entity = readEntity(cursor, offset); attachEntity(entity); if (lock) { identityScopeLong.put2(key, entity); } else { identityScopeLong.put2NoLock(key, entity); } return entity; } } else if (identityScope ! = null) { ... T entity = lock ? identityScope.get(key) : identityScope.getNoLock(key); if (entity ! = null) { return entity; } else { entity = readEntity(cursor, offset); attachEntity(key, entity, lock); return entity; } } else { ... T entity = readEntity(cursor, offset); attachEntity(entity); return entity; }}Copy the code

Let’s understand the execution strategy inside the loadCurrent method. First of all, if you have the entity data cache identityScopeLong/identityScope, first from the cache, if not in the cache, using the corresponding Dao entity object, here is HistoryDataDao, Internally, it returns a new HistoryData entity object based on the data retrieved by the cursor.

@Override
public HistoryData readEntity(Cursor cursor, int offset) {
    HistoryData entity = new HistoryData( //
        cursor.isNull(offset + 0) ? null : cursor.getLong(offset + 0), // id
        cursor.getLong(offset + 1), // date
        cursor.isNull(offset + 2) ? null : cursor.getString(offset + 2) // data
    );
    return entity;
}
Copy the code

Finally, if the cache type is not identityScopeLong, that is, if it belongs to identityScope, the above data will also be cached in identityScope. If there is no entity data cache, just call readEntity to assemble the data and return it.

Note: With the nature of the GreenDao cache, there may be a bug where you don’t get the latest data, so if this happens, you can remove the cache using DaoSession’s clear method.

How does GreenDao combine with ReactiveX?

First, take a look at the usage flow with RX:

RxDao<HistoryData, Long> xxDao = daoSession.getHistoryDataDao().rx();
xxDao.insert(historyData)
        .observerOn(AndroidSchedulers.mainThread())
        .subscribe(new Action1<HistoryData>() {
            @Override
            public void call(HistoryData entity) {
                // insert success
            }
        });
Copy the code

In AbstractDao’s.rx() method, an rxDao object is created that executes on an IO thread by default.

@Experimental
public RxDao<T, K> rx() {
    if (rxDao == null) {
        rxDao = new RxDao<>(this, Schedulers.io());
    }
    return rxDao;
}
Copy the code

Next, analyze the rxDao’s insert method.

@Experimental public Observable<T> insert(final T entity) { return wrap(new Callable<T>() { @Override public T call() throws Exception { dao.insert(entity); return entity; }}); }Copy the code

It’s the wrap method that does the trick, which calls rxutils.fromCallable (callable).

@Internal class RxBase { ... protected <R> Observable<R> wrap(Callable<R> callable) { return wrap(RxUtils.fromCallable(callable)); } protected <R> Observable<R> wrap(Observable<R> observable) { if (scheduler ! = null) { return observable.subscribeOn(scheduler); } else { return observable; }}... }Copy the code

In the fromCallable method of RxUtils, the deferred operator defer is used to send observed events. The main purpose is to ensure that an Observable is subscribed before executing. Finally, if the scheduler scheduler exists, the execution environment is scheduled to the IO thread via an external wrap method.

@Internal class RxUtils { @Internal static <T> Observable<T> fromCallable(final Callable<T> callable) { return Observable.defer(new Func0<Observable<T>>() { @Override public Observable<T> call() { T result; try { result = callable.call(); } catch (Exception e) { return Observable.error(e); } return Observable.just(result); }}); }}Copy the code

Four,

After analyzing the core source code of GreenDao, I found that GreenDao is one of the best database frameworks for a certain reason. First, it avoids performance hoops like reflection by using its own plug-in to generate the required static code with the corresponding Freemarker template. Second, it provides internal entity data mapping caching mechanism, can further speed up the query. For the SQL statements corresponding to different databases, different DataBaseStatement implementation classes combined with the agent mode are encapsulated, shielding the tedious details of database operations. Finally, it uses sqlcipher to provide encryption database function, to a certain extent to ensure security, at the same time, combined with RxJava, we can achieve asynchronous database operation more succinct. GreenDao source analysis here is really the end, the next article, the author will be on the RxJava core source detailed explanation, in order to let you have a more in-depth understanding of RxJava.

Reference links:

GreenDao V3.2.2 source code

2, GreenDao source code analysis

3, GreenDao source analysis


Thank you for reading this article and I hope you can share it with your friends or technical group, it means a lot to me.