本文是对GreenDao框架的源码解析,若您对GreenDao的基本使用还不了解,可以先快速学习GreenDao,对GreenDao有了基本了解后在来阅读本文章。
GreenDao的初始化
为了更好地解析GreenDao的初始化过程,我们先创建一个实体类——Character类。这个类很简单,就只有id和名字。
@Entity
public class Character {
@Id(autoincrement = true)
Long id;
@NotNull
String name;
}
然后make一下项目,就会自动生成三个java文件。
接下来看看我们的常规初始化操作。官方其实也推荐我们在Application的onCreate方法里进行初始化。
public class MyApp extends Application {
private DaoSession mDaoSession;
@Override
public void onCreate() {
super.onCreate();
initGreenDao();
}
private void initGreenDao() {
//1
DaoMaster.DevOpenHelper helper = new DaoMaster.DevOpenHelper(this, "user.db");
SQLiteDatabase db = helper.getWritableDatabase();
//2
DaoMaster daoMaster = new DaoMaster(db);
//3
daoSession = daoMaster.newSession();
}
public DaoSession getDaoSession() {
return mDaoSession;
}
}
代码1这里的Helper用的是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);
}
}
DevOpenHelper是DaoMaster的一个静态内部类,上面有句警告说:更新的时候会把所有的表都给删了,只能在开发阶段使用!再看看onUpgrade方法,确实如此。其实官方说明文档也强调了这一点。
深入父构造方法看看。
/**
* Calls {@link #createAllTables(Database, boolean)} in {@link #onCreate(Database)} -
*/
public static abstract class OpenHelper extends DatabaseOpenHelper {
public OpenHelper(Context context, String name) {
super(context, name, SCHEMA_VERSION);
}
public OpenHelper(Context context, String name, CursorFactory factory) {
super(context, name, factory, SCHEMA_VERSION);
}
@Override
public void onCreate(Database db) {
Log.i("greenDAO", "Creating tables for schema version " + SCHEMA_VERSION);
createAllTables(db, false);
}
}
没有什么特殊的,只是继续调用了它的父构造方法,再深入看看。
public DatabaseOpenHelper(Context context, String name, CursorFactory factory, int version) {
super(context, name, factory, version);
this.context = context;
this.name = name;
this.version = version;
}
做的事情也很简单,就是初始化数据库名、版本号。
回过来看看上一级onCreate方法,就调用了createAllTables方法,看样子是要创建数据库表了。进去看看。
/** Creates underlying database table using DAOs. */
public static void createAllTables(Database db, boolean ifNotExists) {
CharacterDao.createTable(db, ifNotExists);
}
我们一开始写的Character类,在make之后就生成了CharacterDao类,这个地方调用了调用了createTable方法,进去看看。
/** Creates the underlying database table. */
public static void createTable(Database db, boolean ifNotExists) {
String constraint = ifNotExists? "IF NOT EXISTS ": "";
db.execSQL("CREATE TABLE " + constraint + "\"WEAPON\" (" + //
"\"_id\" INTEGER PRIMARY KEY AUTOINCREMENT ," + // 0: id
"\"NAME\" TEXT);"); // 1: name
}
可以看到,这个地方就是真正的create了,使用了SQL的create语句来创建Character表。记得Character只有id和name,且id是自增主键。这里的sql语句都是自动生成的。到这里,helper的初始化就算是走了一遍了。
接下来是DaoMaster的初始化。
public DaoMaster(SQLiteDatabase db) {
this(new StandardDatabase(db));
}
...
public DaoMaster(Database db) {
super(db, SCHEMA_VERSION);
registerDaoClass(CharacterDao.class);
}
DaoMaster的构造函数很简单,就是注册一下CharacterDao类。这里可以做一个有趣的实验,再创建一个实体类看看。
@Entity
public class Weapon {
@Id(autoincrement = true)
Long id;
String name;
}
make一下项目,再回到刚才那个构造函数。
public DaoMaster(Database db) {
super(db, SCHEMA_VERSION);
registerDaoClass(CharacterDao.class);
registerDaoClass(WeaponDao.class);
}
构造函数自动添加了注册WeaponDao的代码,跳进registerDaoClass看看。
protected void registerDaoClass(Class<? extends AbstractDao<?, ?>> daoClass) {
DaoConfig daoConfig = new DaoConfig(db, daoClass);
daoConfigMap.put(daoClass, daoConfig);
}
代码很简单,就是把Dao的设置信息保存到一个HashMap里,key值就是这个Dao的类名。再看看DaoConfig的初始化。代码较长,一点点分析。
//DaoConfig.java
//1
this.tablename = (String) daoClass.getField("TABLENAME").get(null);
//2
Property[] properties = reflectProperties(daoClass);
this.properties = properties;
allColumns = new String[properties.length];
...
//CharacterDao.java
...
//A
public static final String TABLENAME = "CHARACTER";
/**
* Properties of entity Character.<br/>
* Can be used for QueryBuilder and for referencing column names.
*/
//B
public static class Properties {
public final static Property Id = new Property(0, Long.class, "id", true, "_id");
public final static Property Name = new Property(1, String.class, "name", false, "NAME");
}
...
代码1获取CharacterDao对应的表的名字,也就是CHARACTER表(代码A处)。
当我们make项目后,就会自动生成对应于实体类的Properties类。我们写的Character类只有id和name,于是这里自动生成了两个Property分别对应于id和name。Properties是CharacterDao的静态内部类,等会反射会用到。
回到代码2处,为了获取CharacterDao的各Property,就需要把它们映射出来。跳进reflectProperties方法看看,主要代码如下。
private static Property[] reflectProperties(Class<? extends AbstractDao<?, ?>> daoClass)
throws ClassNotFoundException, IllegalArgumentException, IllegalAccessException {
//1
Class<?> propertiesClass = Class.forName(daoClass.getName() + "$Properties");
//2
Field[] fields = propertiesClass.getDeclaredFields();
ArrayList<Property> propertyList = new ArrayList<Property>();
//3
final int modifierMask = Modifier.STATIC | Modifier.PUBLIC;
//4
for (Field field : fields) {
// There might be other fields introduced by some tools, just ignore them (see issue #28)
//5
if ((field.getModifiers() & modifierMask) == modifierMask) {
Object fieldValue = field.get(null);
if (fieldValue instanceof Property) {
propertyList.add((Property) fieldValue);
}
}
}
...
代码1会获取一个xxx$Properties的类,例如传进来的daoClass是CharacterDao,那得到的就是CharacterDao$Properties,可以查看一下自动生成的class文件。
这里给大家推荐一个文件搜索工具,叫EveryThing。搜索的速度比Windows自带的搜索工具快非常多。
代码2通过反射获取各Property,对CharacterDao而言就是Id和Name。
从代码3可以知道GreenDao只关心static变量和public变量。
再看看代码块4,里面有一句英文注释,意思是说实体类的域也有可能是其他框架或工具产生的,如果是这样的域就直接无视掉。代码块5说明了一切,只要这个域不是Property类就无视掉。
现在终于知道GreenDao为什么要大费周折来生成Property类的代码了,其他框架也有可能会在实体类的代码上添加东西,为了避免它们的影响,每个GreenDao标记的域都会生成对应的Property,实体类上没有对应Property的域就会被GreenDao无视了。这个方法真的很巧妙。
这么一来就获得了CharacterDao所有的Property,继续分析。
...
List<String> pkColumnList = new ArrayList<String>();
List<String> nonPkColumnList = new ArrayList<String>();
Property lastPkProperty = null;
for (int i = 0; i < properties.length; i++) {
Property property = properties[i];
String name = property.columnName;
allColumns[i] = name;
if (property.primaryKey) {
pkColumnList.add(name);
lastPkProperty = property;
} else {
nonPkColumnList.add(name);
}
}
String[] nonPkColumnsArray = new String[nonPkColumnList.size()];
nonPkColumns = nonPkColumnList.toArray(nonPkColumnsArray);
String[] pkColumnsArray = new String[pkColumnList.size()];
pkColumns = pkColumnList.toArray(pkColumnsArray);
...
这一部分的工作很简单,就是把主键和非主键分离(一个表的主键可以有多个,也可以没有),继续。
...
if (pkProperty != null) {
Class<?> type = pkProperty.type;
//1
keyIsNumeric = type.equals(long.class) || type.equals(Long.class) || type.equals(int.class)
|| type.equals(Integer.class) || type.equals(short.class) || type.equals(Short.class)
|| type.equals(byte.class) || type.equals(Byte.class);
} else {
keyIsNumeric = false;
}
...
代码1处会分析主键是数字型还是字符串型,这里可以看到GreenDao是可以识别基本数据类型和包装类型的,而且数字型主键也不局限与Long类型,其他数字类型也是可以的。所以部分GreenDao教程文章说@Id标签只能用在Long类型上面其实是不对的。
这么一来,DaoConfig的初始化就完成了,再把它存入HashMap里DaoMaster的初始化也就完成了。
接下来就是DaoSession的初始化了。
...
//MyApp.java
daoSession = daoMaster.newSession();
...
...
//DaoMaster.java
public DaoSession newSession() {
return new DaoSession(db, IdentityScopeType.Session, daoConfigMap);
}
...
...
//DaoSession.java
public DaoSession(Database db, IdentityScopeType type, Map<Class<? extends AbstractDao<?, ?>>, DaoConfig>
daoConfigMap) {
super(db);
...
//1
registerDao(Character.class, characterDao);
//2
registerDao(Weapon.class, weaponDao);
}
...
protected <T> void registerDao(Class<T> entityClass, AbstractDao<T, ?> dao) {
entityToDao.put(entityClass, dao);
}
...
代码1,代码2就把刚刚CharacterDao和WeaponDao注册进去了,这么一来DaoSession的初始化也就完成了,这也表示GrennDao的初始化完成了。
注意事项
就本文章的项目而言,GreenDao为其生成的java文件有4个。
这些由GreenDao生成的java文件并不是只读文件(read-only),最好不要去修改。经过刚才的源码分析我们也看到大量自动生成的代码,很多都是一环扣一环的,修改里面的代码可能会导致工程无法编译成功。