Android | 分析greenDAO 3.2实现原理

MaxEllingto 7年前
   <p>将项目从greenDAO从2.x版本升级到最新的3.2版本,最大变化是可以用注解代替以前的java生成器。实现这点,需要引入相应的gradle插件,具体配置参考官网。</p>    <p style="text-align:center"><img src="https://simg.open-open.com/show/a31a136ab6f109933bdf94167f5610d7.png"></p>    <p style="text-align:center">图1</p>    <p>图1是从官网盗来的主结构图,注解Entity后,只需要build工程,DaoMaster、DaoSession和对应的Dao文件就会自动生成。分析greenDAO的实现原理,将会依照这幅图的路线入手,分析各个部分的作用,最重要是研究清楚greenDAO是怎样调用数据库的CRUD。</p>    <h3><strong>DaoMaster</strong></h3>    <p>DaoMaster是greenDAO的入口,它的父类AbstractDaoMaster维护了数据库重要的参数,分别是实例、版本和Dao的信息。</p>    <pre>  <code class="language-java">//AbstractDaoMaster的参数  protected final Database db;  protected final int schemaVersion;  protected final Map<Class<? extends AbstractDao<?, ?>>, DaoConfig> daoConfigMap;</code></pre>    <p>创建DaoMaster需要传入Android原生数据库SQLiteDatabase的实例,接着传递给StandardDatabase:</p>    <pre>  <code class="language-java">//DaoMaster的构造函数  public DaoMaster(SQLiteDatabase db) {      this(new StandardDatabase(db));  }    public DaoMaster(Database db) {      super(db, SCHEMA_VERSION);      registerDaoClass(UserDao.class);  }</code></pre>    <pre>  <code class="language-java">public class StandardDatabase implements Database {      private final SQLiteDatabase delegate;        public StandardDatabase(SQLiteDatabase delegate) {          this.delegate = delegate;      }        @Override      public void execSQL(String sql) throws SQLException {          delegate.execSQL(sql);      }        //其余省略  }</code></pre>    <p>StandardDatabase实现了Database接口,方法都是SQLiteDatabase提供的,所以SQLite的操作都委托给AbstractDaoMaster的参数db去调用。</p>    <pre>  <code class="language-java">protected void registerDaoClass(Class<? extends AbstractDao<?, ?>> daoClass) {      DaoConfig daoConfig = new DaoConfig(db, daoClass);      daoConfigMap.put(daoClass, daoConfig);  }</code></pre>    <p>所有Dao都需要创建DaoConfig,通过AbstractDaoMaster的registerDaoClass注册进daoConfigMap,供后续使用。</p>    <h3><strong>数据库升级</strong></h3>    <pre>  <code class="language-java">DbUpgradeHelper helper = new DbUpgradeHelper(context, dbName, null);  DaoMaster daoMaster = new DaoMaster(helper.getReadableDatabase());</code></pre>    <p>生成数据库可以使用类似上面的语句,通过getReadableDatabase获取数据库实例传递给DaoMaster。DbUpgradeHelper是自定义对象,向上查找父类,可以找到熟悉SQLiteOpenHelper。</p>    <p>DbUpgradeHelper --> DaoMaster.OpenHelper --> DatabaseOpenHelper --> SQLiteOpenHelper</p>    <p>SQLiteOpenHelper提供了onCreate、onUpgrade、onOpen等空方法。继承SQLiteOpenHelper,各层添加了不同的功能:</p>    <ul>     <li>DatabaseOpenHelper:使用EncryptedHelper加密数据库;</li>     <li>DaoMaster.OpenHelper:onCreate时调用createAllTables,继而调用各Dao的createTable;</li>     <li>DbUpgradeHelper:自定义,一般用来处理数据库升级。</li>    </ul>    <p>DatabaseOpenHelper和DaoMaster.OpenHelper的代码简单,就不贴了。数据库升级涉及到表结构和表数据的变更,需要判断版本号处理各版本的差异,处理方法可以参考下面的DbUpgradeHelper:</p>    <pre>  <code class="language-java">public class DbUpgradeHelper extends DaoMaster.OpenHelper {      public DbUpgradeHelper(Context context, String name, SQLiteDatabase.CursorFactory factory) {          super(context, name, factory);      }        @Override      public void onUpgrade(Database db, int oldVersion, int newVersion) {          if (oldVersion == newVersion) {              LogUtils.d("数据库是最新版本" + oldVersion + ",不需要升级");              return;          }          LogUtils.d("数据库从版本" + oldVersion + "升级到版本" + newVersion);          switch (oldVersion) {              case 1:                  String sql = "";                  db.execSQL(sql);              case 2:              default:                  break;          }      }  }</code></pre>    <p>数据库变更语句的执行,可以利用switch-case没有break时连续执行的特性,实现数据库从任意旧版本升级到新版本。</p>    <h3><strong>DaoSession</strong></h3>    <pre>  <code class="language-java">public DaoSession newSession() {          return new DaoSession(db, IdentityScopeType.Session, daoConfigMap);  }  public DaoSession newSession(IdentityScopeType type) {          return new DaoSession(db, type, daoConfigMap);  }</code></pre>    <p>DaoSession通过调用DaoMaster的newSession创建。对同一个数据库,可以根据需要创建多个Session分别操作。参数IdentityScopeType涉及到是否启用greenDAO的缓存机制,后文会进一步分析。</p>    <pre>  <code class="language-java">public DaoSession(Database db, IdentityScopeType type, Map<Class<? extends AbstractDao<?, ?>>, DaoConfig> daoConfigMap) {      super(db);      userDaoConfig = daoConfigMap.get(UserDao.class).clone();      userDaoConfig.initIdentityScope(type);        userDao = new UserDao(userDaoConfig, this);        registerDao(User.class, userDao);  }</code></pre>    <p>创建DaoSession时,将会获取每个Dao的DaoConfig,这是从之前的daoConfigMap中直接clone出来。并且Dao还需要在DaoSession注册,registerDao在父类AbstractDaoSession中的实现:</p>    <pre>  <code class="language-java">public class AbstractDaoSession {      private final Database db;      private final Map<Class<?>, AbstractDao<?, ?>> entityToDao;        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);      }        /** Convenient call for {@link AbstractDao#insert(Object)}. */      public <T> long insert(T entity) {          @SuppressWarnings("unchecked")          AbstractDao<T, ?> dao = (AbstractDao<T, ?>) getDao(entity.getClass());          return dao.insert(entity);      }        public AbstractDao<?, ?> getDao(Class<? extends Object> entityClass) {          AbstractDao<?, ?> dao = entityToDao.get(entityClass);          if (dao == null) {              throw new DaoException("No DAO registered for " + entityClass);          }          return dao;      }        //其余略  }</code></pre>    <p>registerDao将使用Map维持Class->Dao的关系。AbstractDaoSession提供了insert、update、delete等泛型方法,支持对数据库表的CURD。原理就是从Map获取对应的Dao,再调用Dao对应的操作方法。</p>    <h3><strong>Dao</strong></h3>    <p>每个Dao都有一个对应的DaoConfig,创建时通过反射机制,为Dao准备好TableName、Property、Pk等一系列具体的参数。所有Dao都继承自AbstractDao,表的通用操作方法就定义在这里。</p>    <p><strong>表的新增和删除</strong></p>    <pre>  <code class="language-java">public static void createTable(Database db, boolean ifNotExists) {      String constraint = ifNotExists? "IF NOT EXISTS ": "";      db.execSQL("CREATE TABLE " + constraint + "\"USER\" (" + //              "\"ID\" INTEGER PRIMARY KEY ," + // 0: id              "\"USER_NAME\" TEXT NOT NULL ," + // 1: user_name              "\"REAL_NAME\" TEXT NOT NULL ," + // 2: real_name              "\"EMAIL\" TEXT," + // 3: email              "\"MOBILE\" TEXT," + // 4: mobile              "\"UPDATE_AT\" INTEGER," + // 5: update_at              "\"DELETE_AT\" INTEGER);"); // 6: delete_at  }    public static void dropTable(Database db, boolean ifExists) {      String sql = "DROP TABLE " + (ifExists ? "IF EXISTS " : "") + "\"USER\"";      db.execSQL(sql);  }</code></pre>    <p>简单的先讲,每个Dao里都有表的新增和删除方法,很直接地拼Sql执行,注意传参可以支持判断表是否存在。</p>    <p><strong>SQLiteStatement</strong></p>    <p>下面开始研究greenDAO如何调用SQLite的CRUD,首先要理解什么是ORM。简单来说,SQLite是一个关系数据库,Java用的是对象,对象和关系之间的数据交互需要一个东西去转换,这就是greenDAO的作用。转换过程也不复杂,数据库的列对应Java对象里的参数就行。</p>    <p>SQLiteStatement是封装了对数据库操作和相关数据的对象</p>    <p>SQLiteStatement由Android提供,它的父类SQLiteProgram有两个重要的参数,是执行数据库操作前要提供的:</p>    <pre>  <code class="language-java">private final String mSql;   //操作数据库用的Sql  private final Object[] mBindArgs;  //列和数据值的关系</code></pre>    <p>参数mBindArgs描述了数据库列和数据的关系,SQLiteStatement为不同数据类型提供bind方法,结果保存在mBindArgs,最终交给SQLite处理。</p>    <p>和StandardDatabase一样,SQLiteStatement的方法委托给DatabaseStatement调用,所以greenDAO操作数据库前需要先获取DatabaseStatement。</p>    <p><strong>生成Sql</strong></p>    <p>sql的获取需要用到TableStatements,它的对象维护在DaoConfig里,由它负责创建和缓存DatabaseStatement,下面是insert的DatabaseStatement获取过程:</p>    <pre>  <code class="language-java">public DatabaseStatement getInsertStatement() {      if (insertStatement == null) {          String sql = SqlUtils.createSqlInsert("INSERT INTO ", tablename, allColumns);          DatabaseStatement newInsertStatement = db.compileStatement(sql);          synchronized (this) {              if (insertStatement == null) {                  insertStatement = newInsertStatement;              }          }          if (insertStatement != newInsertStatement) {              newInsertStatement.close();          }      }      return insertStatement;  }</code></pre>    <p>sql语句通过SqlUtils工具拼接,由Database调用compileStatement将sql存入DatabaseStatement。可知,DatabaseStatement的实现类是StandardDatabaseStatement:</p>    <pre>  <code class="language-java">@Override  public DatabaseStatement compileStatement(String sql) {      return new StandardDatabaseStatement(delegate.compileStatement(sql));  }</code></pre>    <p>拼接出来的sql是包括表名和字段名的通用插入语句,生成的DatabaseStatement是可以复用的,所以第一次获取的DatabaseStatement会缓存在insertStatement参数,下次直接使用。</p>    <p>其他例如count、update、delete等操作获取DatabaseStatement原理是一样的,就不介绍了。</p>    <p><strong>执行insert</strong></p>    <p>insert和insertOrReplace都调用了executeInsert,区别之处是入参DatabaseStatement的获取方法不同。</p>    <pre>  <code class="language-java">private long executeInsert(T entity, DatabaseStatement stmt, boolean setKeyAndAttach) {      long rowId;      if (db.isDbLockedByCurrentThread()) {          rowId = insertInsideTx(entity, stmt);      } else {          // Do TX to acquire a connection before locking the stmt to avoid deadlocks          db.beginTransaction();          try {              rowId = insertInsideTx(entity, stmt);              db.setTransactionSuccessful();          } finally {              db.endTransaction();          }      }      if (setKeyAndAttach) {          updateKeyAfterInsertAndAttach(entity, rowId, true);      }      return rowId;  }     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();          }      }  }</code></pre>    <p>当前线程获取数据库锁的情况下,直接执行insert操作即可,否则需要使用事务保证操作的原子性和一致性。insertInsideTx方法里,isStandardSQLite判断当前是不是SQLite数据库(留下扩展的伏笔?)。关键来了,获取原始的SQLiteStatement,调用了bindValues。</p>    <pre>  <code class="language-java">@Override  protected final void bindValues(SQLiteStatement stmt, User entity) {      stmt.clearBindings();        Long id = entity.getId();      if (id != null) {          stmt.bindLong(1, id);      }      stmt.bindString(2, entity.getUser_name());      stmt.bindString(3, entity.getReal_name());  }</code></pre>    <p>bindValues由各自的Dao实现,描述index和数据的关系,最终保存进mBindArgs。到这里,应该就能明白greenDao的核心作用。greenDao将我们熟悉的对象,转换成sql语句和执行参数,再提交SQLite执行。</p>    <p>update和delete的操作和insert大同小异,推荐自行分析。</p>    <h3><strong>数据Load与缓存机制</strong></h3>    <pre>  <code class="language-java">userDaoConfig = daoConfigMap.get(UserDao.class).clone();  userDaoConfig.initIdentityScope(type);</code></pre>    <p>创建DaoSession并获取DaoConfig时,调用了initIdentityScope,这里是greenDAO缓存的入口。</p>    <pre>  <code class="language-java">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);      }  }</code></pre>    <p>DaoSession的入参IdentityScopeType现在可以解释了,None时不启用缓存,Session时启用缓存。缓存接口IdentityScope根据主键是不是数字,分为两个实现类IdentityScopeLong和IdentityScopeObject。两者的实现类似,选IdentityScopeObject来研究。</p>    <pre>  <code class="language-java">private final HashMap<K, Reference<T>> map;</code></pre>    <p>缓存机制很简单,一个保存pk和entity关系的Map,再加上get、put、detach、remove、clear等操作方法。其中get、put方法分无锁版本和加锁版本,对应当前线程是否获得锁的情况。</p>    <pre>  <code class="language-java">map.put(key, new WeakReference<T>(entity));</code></pre>    <p>注意,将entity加入Map时使用了弱引用,资源不足时GC会主动回收对象。</p>    <p>下面是load方法,看缓存扮演了什么角色。</p>    <pre>  <code class="language-java">public T load(K key) {      assertSinglePk();      if (key == null) {          return null;      }      //1      if (identityScope != null) {          T entity = identityScope.get(key);          if (entity != null) {              return entity;          }      }     //2      String sql = statements.getSelectByKey();      String[] keyArray = new String[]{key.toString()};      Cursor cursor = db.rawQuery(sql, keyArray);      return loadUniqueAndCloseCursor(cursor);  }</code></pre>    <p>在执行真正的数据加载前,标记1处先查找缓存,如果有就直接返回,无就去查数据库。标记2处准备sql语句和参数,交给rawQuery查询,得到Cursor。</p>    <p>用主键查询,只可能有一个结果,调用loadUnique,最终调用loadCurrent。loadCurrent会先尝试从缓存里获取数据,代码很长,分析identityScopeLong != null这段就可以体现原理:</p>    <pre>  <code class="language-java">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);          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;          }      }</code></pre>    <pre>  <code class="language-java">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);          }      }  }</code></pre>    <p>AbstractDao同时维护identityScope和identityScopeLong对象,entity会同时put进它们两者。如果主键是数字,优先从identityScopeLong获取缓存,速度更快;如果主键不是数字,就尝试从IdentityScopeObject获取;如果没有缓存,只能通过游标读取数据库。</p>    <h3><strong>数据Query</strong></h3>    <p>QueryBuilder使用链式结构构建Query,灵活地支持where、or、join等约束的添加。具体代码是简单的数据操作,没必要细说,数据最终会拼接成sql。Query的unique操作和上面的load一样,而list操作在调用rawQuery获取Cursor后,最终调用AbstractDao的loadAllFromCursor:</p>    <pre>  <code class="language-java">protected List<T> loadAllFromCursor(Cursor cursor) {      int count = cursor.getCount();      if (count == 0) {          return new ArrayList<T>();      }      List<T> list = new ArrayList<T>(count);      //1      CursorWindow window = null;      boolean useFastCursor = false;      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);              }          }      }      //2      if (cursor.moveToFirst()) {          if (identityScope != null) {              identityScope.lock();              identityScope.reserveRoom(count);          }          try {              if (!useFastCursor && window != null && identityScope != null) {                  loadAllUnlockOnWindowBounds(cursor, window, list);              } else {                  do {                      list.add(loadCurrent(cursor, 0, false));                  } while (cursor.moveToNext());              }          } finally {              if (identityScope != null) {                  identityScope.unlock();              }          }      }      return list;  }</code></pre>    <p>标记1处尝试使用Android提供的CursorWindow以获取一个更快的Cursor。SQLiteDatabase将查询结果保存在CursorWindow所指向的共享内存中,然后通过Binder把这片共享内存传递到查询端。Cursor不是本文要讨论的内容,详情可以参考其他资料。</p>    <p>标记2处通过移动Cursor,利用loadCurrent进行批量操作,结果保存在List中返回。</p>    <h3><strong>一对一和一对多</strong></h3>    <p>greenDAO支持一对一和一对多,但并不支持多对多。</p>    <pre>  <code class="language-java">@ToOne(joinProperty = "father_key")  private CheckItem father;    @Generated  public CheckItem getFather() {      String __key = this.father_key;      if (father__resolvedKey == null || father__resolvedKey != __key) {          __throwIfDetached();          CheckItemDao targetDao = daoSession.getCheckItemDao();          CheckItem fatherNew = targetDao.load(__key);          synchronized (this) {              father = fatherNew;              father__resolvedKey = __key;          }      }      return father;  }</code></pre>    <p>一对一,使用@ToOne标记,greenDAO会自动生成get方法,并标记为@Generated,代表是自动生成的,不要动代码。get方法利用主键load出对应的entity即可。</p>    <pre>  <code class="language-java">@ToMany(joinProperties = {      @JoinProperty(name = "key", referencedName = "father_key")  })  private List<CheckItem> children;</code></pre>    <p>一对多的形式和一对一类似,使用@ToMany标记,get方法是利用QueryBuild查询目标List,代码简单就不贴了。</p>    <h3><strong>后记</strong></h3>    <p>到此,过了一遍greenDAO主要功能,还有些高级特性用到再研究吧。纵观下来,greenDAO还是挺简单的,但也很实用,简化了数据库调用的复杂度,具体的执行就交给原生的Android数据库管理类。</p>    <p> </p>    <p> </p>    <p>来自:http://www.jianshu.com/p/0d3cbe6278fb</p>    <p> </p>