Android GreenDao 源码分析

2,120 阅读8分钟

前言

原生数据库直接操作不好用吗?

为什么要使用GreenDao?

GreenDao的优势又在哪里?

长久不用数据库,都已经生疏了,本文接下来将对GreenDao 在使用层进行分析,没有 GreenDao 模板配套生成静态代码等的原理分析。

目录

一、SQLite

SQLite是一个轻量级、跨平台的关系型数据库。

使用原生数据库SQLite,需要借助Android提供的辅助类 SQLiteOpenHelper,继承自它就可以实现数据库的创建、升级、更新数据库时的某些操作等等。前提是你需要对SQL语句有一定的了解 SQLite教程

优点:

  • 轻量级 体积小

  • 无需安装

  • 跨平台/可移植

  • 数据库信息在一个文件内

缺点:

  • 并发场景,数据库可能会被写操作独占,从而导致其它读写操作阻塞或出错

  • SQL标准支持不全

使用注意事项:

  • Sqlite 写操作 不是线程安全的

  • Sqlite 读操作是线程安全的

  • 插入大量数据时最好开启事务,避免单独循环插入,多次读写磁盘,Sqlite默认一个插入语句是一个事务

  • 使用execSQL方法可能会导致注入攻击

对于SQL语句的执行,可以用 SQLiteDatabase的execSQL() 方法,也可以用ContentValues来实现插入、修改数据等。

在查询时,我们可能需要写类似如下代码

  List<Person> result = new ArrayList<>();
  Cursor cursor = null;
  SQLiteDatabase readableDatabase = mDbHelper.getReadableDatabase();
  try {
      cursor = readableDatabase.rawQuery("select * from person where age > ? order by age desc", new String[]{"18"});
​
      while (cursor.moveToNext()) {
             String id = cursor.getString(cursor.getColumnIndex("_id"));
             String name = cursor.getString(cursor.getColumnIndex("NAME"));
             String sex = cursor.getString(cursor.getColumnIndex("SEX"));
             String age = cursor.getString(cursor.getColumnIndex("AGE"));
             Person person = new Person(id,name,sex,age);
             result.add(person);
      }
   } catch (Exception e) {
        e.printStackTrace();
   } finally {
         //关闭游标
        if (cursor != null) {
             cursor.close();
             cursor = null;
        }
        //关闭数据库
        readableDatabase.close();

  }

我们在解析数据时,需要操作游标然后遍历匹配,构建对象,返回数据集合,这对于应用层开发人员来说,不是很友好。

二、GreenDao

GreenDao是一个对象关系映射型框架,提供一个接口通过操作对象的方法去操作关系型数据库。

GreenDao官网

1、优缺点

优点:

  • 性能高,Android最快的关系型数据库,存取速度快

  • 依赖体积小

  • 内存开销小

  • 支持SQLCipher加密

  • 强大的社区支持

缺点:

  • 需要获取Dao类,修改实体,需要重新Rebuild

  • 默认数据库升级会删除所有的表,重新建立,这里需要注意适配老版本的APP,检查数据库类型是否有变更

2、简单使用

  • 导入

    主工程 build.gradle

      classpath 'org.greenrobot:greendao-gradle-plugin:3.3.0'
    

    模块 build.gradle

    apply plugin: 'org.greenrobot.greendao'
    dependencies {
        api 'org.greenrobot:greendao:3.2.0'
    }
    
  • 编写实体类

    Custom.class

    @Entity
    public class Custom {
        @Id(autoincrement = true)
        private Long id;
    ​
        private String shopName;
    ​
        private String shopDescription;
      }
    

    Rebuild 或者 同步一下 Custom 类如下

    @Entity
    public class Custom {
        @Id(autoincrement = true)
        private Long id;
    ​
        private String shopName;
    ​
        private String shopDescription;
    ​
        @Generated(hash = 567200405)
        public Custom(Long id, String shopName, String shopDescription) {
            this.id = id;
            this.shopName = shopName;
            this.shopDescription = shopDescription;
        }
    ​
        @Generated(hash = 62298964)
        public Custom() {
        }
    ​
        public Long getId() {
            return this.id;
        }
    ​
        public void setId(Long id) {
            this.id = id;
        }
    ​
        public String getShopName() {
            return this.shopName;
        }
    ​
        public void setShopName(String shopName) {
            this.shopName = shopName;
        }
    ​
        public String getShopDescription() {
            return this.shopDescription;
        }
    ​
        public void setShopDescription(String shopDescription) {
            this.shopDescription = shopDescription;
        }
    }
    

    并且对应目录会生成此类文件

  • 获取并使用Dao类

    示例 增、删、更新、查找

    DBManager.class

    private DBManager(){
            if(openHelper == null) {
                openHelper = new DaoMaster.DevOpenHelper(Global.getInstance().getApplicationContext(), "custom_db");
                SQLiteDatabase sqLiteDatabase = openHelper.getWritableDatabase();
                daoSession = new DaoMaster(sqLiteDatabase).newSession();
            }
        }
        public DaoSession getDaoSession(){
            return  daoSession;
        }
    

    DBOperationInterface

    public interface DBOperationInterface<T> {
        void insert(List<T> data);
        void delete(List<T> data);
        void update(List<T> data);
        List<T> find(Object... args);
    }
    

    CustomOperation

    public class CustomOperation extends BaseOperation<Custom> {
        //增
        @Override
        public void insert(List<Custom> data) {
            if (data == null || data.isEmpty()) {
                return;
            }
            getDaoSession().getCustomDao().saveInTx(data);
        }
    ​
        //删
        @Override
        public void delete(List<Custom> data) {
            if (data == null || data.isEmpty()) {
                return;
            }
            getDaoSession().getCustomDao().deleteInTx(data);
        }
        //更新
        @Override
        public void update(List<Custom> data) {
            if (data == null || data.isEmpty()) {
                return;
            }
            getDaoSession().getCustomDao().updateInTx(data);
        }
        //查找
        @Override
        public List<Custom> find(Object... args) {
    ​
            if (args == null || args.length == 0) {
                return getDaoSession().getCustomDao().loadAll();
            }
            QueryBuilder<Custom> queryBuilder = getDaoSession().getCustomDao().queryBuilder();
            if (args.length == 1 && args[0] instanceof String) {
                return queryBuilder.where(CustomDao.Properties.ShopName.eq(args[0])).list();
            } else if (args.length == 2 ) {
                if (args[0] instanceof String && args[1] instanceof String) {
                    return queryBuilder.where(CustomDao.Properties.ShopName.eq(args[0]), CustomDao.Properties.ShopDescription.eq(args[1])).list();
                }
            }
            return null;
        }
    ​
    }
    ​
    

3、原理分析

以下将对GreenDao使用原理进行分析,由于插入和更新、删除差不多,这里仅对插入fe

3.1、DevOpenHelper创建

类图如下

    /** WARNING: Drops all table on Upgrade! Use only during development. */
    public static class DevOpenHelper extends OpenHelper {
        public DevOpenHelper(Context context, String name) {
            super(context, name);
        }
​
        public DevOpenHelper(Context context, String name, CursorFactory factory) {
            super(context, name, factory);
        }
​
        @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);
        }
    }

构造函数调用的是父类OpenHelper的构造函数,在 onUpgrade 时会先删除所有表然后再调用父类onCreate 重新 新建表,如果不想这么操作,可以自定义一个类继承OpenHelper来替代官方默认的DevOpenHelper

OpenHelper是继承自DatabaseOpenHelper的,在此类中,有类似方法需要注意一下,这里内部代表了两种不同类型的数据库,分别是StandardDatabaseEncryptedDatabase,前者没有加密,后者使用了SQLCipher加密

 public Database getWritableDb() {
     return wrap(getWritableDatabase());
 }
 public Database getEncryptedWritableDb(String password) {
     EncryptedHelper encryptedHelper = checkEncryptedHelper();
     return encryptedHelper.wrap(encryptedHelper.getReadableDatabase(password));
 }

3.2、DaoSession 的创建

相关过程图如下

DaoSession创建过程中会先构造DaoMaster ,通过传递DataBase 或者是 SQLiteDatabase ,如果传递的是android系统的SQLiteDataase,构造函数内将会对它包装一下,使用StandardDatabase 来代理 SQLiteDatabase

public DaoMaster(SQLiteDatabase db) {
    this(new StandardDatabase(db));
}
public DaoMaster(Database db) {
     super(db, SCHEMA_VERSION);
     registerDaoClass(CustomDao.class);
 }

DaoMaster的构造方法里,还有通过 super 调用父类AbstractDaoMaster的构造函数,初始化HashMap daoConfigMap ,

然后通过接下来的 registerDaoClass(CustomDao.class) 来构建DaoConfig对象 (这里会创建一个跟SQL语句执行相关的TableStatements类) ,并且存放daoClassDaoConfig的映射关系。

构造完DaoMaster类后,通过调用其**newSession**方法返回DaoSeesion类

public DaoSession newSession(IdentityScopeType type) {
      return new DaoSession(db, type, daoConfigMap);
}

DaoSession的构造函数里做了些什么呢?

    public DaoSession(Database db, IdentityScopeType type, Map<Class<? extends AbstractDao<?, ?>>, DaoConfig>
            daoConfigMap) {
        super(db);
​
        customDaoConfig = daoConfigMap.get(CustomDao.class).clone();
        customDaoConfig.initIdentityScope(type);
​
        customDao = new CustomDao(customDaoConfig, this);
​
        registerDao(Custom.class, customDao);
    }
    protected <T> void registerDao(Class<T> entityClass, AbstractDao<T, ?> dao) {
        entityToDao.put(entityClass, dao);
    }

初始化了entityToDao,生成了customDaoConfig对象,并存放了Custom.classCustomDao的映射关系到entityToDao中,还有customDaoConfig 也对IdentityScope进行了初始化

    public void initIdentityScope(IdentityScopeType type) {
        if (type == IdentityScopeType.None) {
            identityScope = null;
        } else if (type == IdentityScopeType.Session) {
            if (keyIsNumeric) {
                identityScope = new IdentityScopeLong();
            } else {
                identityScope = new IdentityScopeObject();
            }
        } else {
            throw new IllegalArgumentException("Unsupported type: " + type);
        }
    }

这个IdentityScope 用于存放ID和实体类映射关系的缓存。

3.3、插入

原理图

分析

  public void saveInTx(Iterable<T> entities) {
        int updateCount = 0;
        int insertCount = 0;
        //注释 1
        for (T entity : entities) {  
            if (hasKey(entity)) {
                updateCount++;
            } else {
                insertCount++;
            }
        }
        if (updateCount > 0 && insertCount > 0) {
           //注释 2
            List<T> toUpdate = new ArrayList<>(updateCount);
            List<T> toInsert = new ArrayList<>(insertCount);
            for (T entity : entities) {
                if (hasKey(entity)) {
                    toUpdate.add(entity);
                } else {
                    toInsert.add(entity);
                }
            }
​
            db.beginTransaction();
            try {
                updateInTx(toUpdate);
                insertInTx(toInsert);
                db.setTransactionSuccessful();
            } finally {
                db.endTransaction();
            }
        } else if (insertCount > 0) {
            //注释 3
            insertInTx(entities);
        } else if (updateCount > 0) {
            updateInTx(entities);
        }
    }

注释 1 处 会判断 需要更新的和需要插入的数据的数量,如果都有,则走注释 2,注释 2 内会根据 hasKey 方法 判断 该实体类对象有没有 ID,有IDhasKey 返回 true,然后开启事务,更新数据、插入数据。但是作为插入调用 saveInTx 方法一般都是直接走 注释 3 处,调用 insertInTx 方法

public void insertInTx(Iterable<T> entities) {
     insertInTx(entities, isEntityUpdateable()); // isEntityUpdateable 返回的是 固定的 true
}
public void insertInTx(Iterable<T> entities, boolean setPrimaryKey) {
      DatabaseStatement stmt = statements.getInsertStatement();
      executeInsertInTx(stmt, entities, setPrimaryKey);
}

最关键的是 executeInsertInTx 方法

private void executeInsertInTx(DatabaseStatement stmt, Iterable<T> entities, boolean setPrimaryKey) {
        db.beginTransaction();
        try {
            synchronized (stmt) {
                if (identityScope != null) {
                    identityScope.lock(); // 上锁 保证读写正常
                }
                try {
                    if (isStandardSQLite) {
                        //注释 1
                        SQLiteStatement rawStmt = (SQLiteStatement) stmt.getRawStatement();
                        for (T entity : entities) {
                            //注释 2
                            bindValues(rawStmt, entity);
                            if (setPrimaryKey) {
                                //注释 3
                                long rowId = rawStmt.executeInsert();
                                //注释 4
                                updateKeyAfterInsertAndAttach(entity, rowId, false);
                            } else {
                                rawStmt.execute();
                            }
                        }
                    } else {
                      ......
                    }
                } finally {
                    if (identityScope != null) {
                        identityScope.unlock();
                    }
                }
            }
            db.setTransactionSuccessful();
        } finally {
            db.endTransaction();
        }
    }

我们使用的是不加密的数据库,所以 isStandardSQLitetrue, 注释 1 处 是通过 stmt 得到 Android API中的SQLiteStatement对象,然后注释 2 处,为SQLiteStatement 设置参数值,注释 3 处执行插入语句,得到 ID 值,然后 注释 4 处会为entity实体类设置ID,并调用 attachEntityID和实体类关系映射存放在 IdentityScope 中。bindValuesattachEntity 方法如下

    protected final void bindValues(SQLiteStatement stmt, Custom entity) {
        stmt.clearBindings();

        Long id = entity.getId();
        if (id != null) {
            stmt.bindLong(1, id);
        }

        String shopName = entity.getShopName();
        if (shopName != null) {
            stmt.bindString(2, shopName);
        }

        String shopDescription = entity.getShopDescription();
        if (shopDescription != null) {
            stmt.bindString(3, shopDescription);
        }
    }

3.4、查找

原理图

分析

我们这里分析下 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();
     }
 }

最终调用的是 loadAllAndCloseCursor 方法

    protected List<T> loadAllFromCursor(Cursor cursor) {
        int count = cursor.getCount();
        if (count == 0) {
            return new ArrayList<T>();
        }
        List<T> list = new ArrayList<T>(count);
        CursorWindow window = null;
        boolean useFastCursor = false;
        //注释 1
        if (cursor instanceof CrossProcessCursor) {
            window = ((CrossProcessCursor) cursor).getWindow();
            if (window != null) { // E.g. Robolectric has no Window at this point
                if (window.getNumRows() == count) {
                    cursor = new FastCursor(window);
                    useFastCursor = true;
                } else {
                    DaoLog.d("Window vs. result size: " + window.getNumRows() + "/" + count);
                }
            }
        }
​
        if (cursor.moveToFirst()) {
            if (identityScope != null) {
                identityScope.lock();
                identityScope.reserveRoom(count);
            }
​
            try {
                //注释 2
                if (!useFastCursor && window != null && identityScope != null) {
                    loadAllUnlockOnWindowBounds(cursor, window, list);
                } else {
                    // 注释 3
                    do {
                        list.add(loadCurrent(cursor, 0, false));
                    } while (cursor.moveToNext());
                }
            } finally {
                if (identityScope != null) {
                    identityScope.unlock();
                }
            }
        }
        return list;
    }

注释 1 处 会判断 Cursor 是不是跨进程的,并且如果 numRows == count 没有偏差,就创建FastCurosr 进行遍历,然后会走注释 3 处,否则走注释 2 处。注释 3 处数据收集 是通过 loadCurrent 方法,其实 注释 2 处也是

    private void loadAllUnlockOnWindowBounds(Cursor cursor, CursorWindow window, List<T> list) {
        int windowEnd = window.getStartPosition() + window.getNumRows();
        for (int row = 0; ; row++) {
            list.add(loadCurrent(cursor, 0, false));
            row++;
          ......
                if (!cursor.moveToNext()) {
                    break;
           ......
        }
    }

所以我们直接分析 loadCurrent 方法即可

    final protected T loadCurrent(Cursor cursor, int offset, boolean lock) {
        //注释 1
        if (identityScopeLong != null) {
            if (offset != 0) {
                // Occurs with deep loads (left outer joins)
                if (cursor.isNull(pkOrdinal + offset)) {
                    return null;
                }
            }

            long key = cursor.getLong(pkOrdinal + offset);
            //注释 2 
            T entity = lock ? identityScopeLong.get2(key) : identityScopeLong.get2NoLock(key);
            if (entity != null) {
                return entity;
            } else {
                //注释 3
                entity = readEntity(cursor, offset);
                attachEntity(entity);
                if (lock) {
                    identityScopeLong.put2(key, entity);
                } else {
                    identityScopeLong.put2NoLock(key, entity);
                }
                return entity;
            }
        } 
        .....
    }

由于我们创建的实体类 CustomIDlong类型,所以 注释 1 处 identityScopeLong不为空。

还记得之前分析插入数据时 ,提到过会将 ID 和实体类关系映射存放到 IdentityScope 中吗?注释2处就是先从这个 IdentityScope 中找数据,没有的话,走到注释 3 处,调用 readEntity 读取数据

    @Override
    public Custom readEntity(Cursor cursor, int offset) {
        Custom entity = new Custom( //
            cursor.isNull(offset + 0) ? null : cursor.getLong(offset + 0), // id
            cursor.isNull(offset + 1) ? null : cursor.getString(offset + 1), // shopName
            cursor.isNull(offset + 2) ? null : cursor.getString(offset + 2) // shopDescription
        );
        return entity;
    }

三、总结

SQLite是一个关系型数据库,在代码中使用 需要 自己手动实现 创建 游标进行查询、删除等操作代码,操作很繁琐。

而作为对象关系映型框架GreenDao,它将大部分我们需要自己实现的代码都封装好了

  • 针对不同SQL语句,都结合了代理模式进行封装

  • 针对查找还具备缓存功能,

  • 提供了SQLCipher加密和不加密两种方式,使用及其方便

  • 插入数据时间 都差不多,但是上万条数据的删除和更新,快 1 比 40 了

如有错误,请指出,谢谢!