前言
上次针对GreenDao使用的源码流程进行了一个分析,了解了它内部是如何来实现对数据库的读写,对于缓存相关的IdentityScope只做了一个简单的概括。本节将针对该数据库的缓存进行一个分析。
ps因为最近刚好被问到过这个问题,第一反应是GreenDao有缓存??文件缓存还是内存缓存?脑海里搜索了半天,把IdentityScope给忘了。
解决的问题
GreenDao的一个优点是性能高,存取速度快,为什么?存的时候我们知道是利用了SQLiteStatement预编译,解决了SQL注入问题和提高了效率,那么取数据又如何速度快?体现在哪呢?
针对GreenDao的增、删、改、查源码分析见**Android GreenDao 源码分析**
本文关注点:缓存
目录
一、从缓存中取数据
具体源码分析可见 Android GreenDao 源码分析
为了探究取数据为何速度块?我们主要针对取数据进行分析
下面贴的代码是 Android GreenDao 源码分析 中简单使用部分的查找
//查找
@Override
public List<Custom> find(Object... args) {
if (args == null || args.length == 0) {
//注释 1
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;
}
注释 1 处代码,在之前的文章中已经分析过了,它内部会调用到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;
}
}
//key就是rowId,先取出rowId
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;
}
}
.....
}
我们看注释2处,先从这个 IdentityScope 中找数据,没有的话,走到注释 3 处,调用 readEntity 读取数据。
然而lock为false,所以是从identityScopeLong.get2NoLock(key) 方法取出数据,那么 这个 get2NoLock 方法做了什么?
public T get2NoLock(long key) {
Reference<T> ref = map.get(key);
if (ref != null) {
return ref.get();
} else {
return null;
}
}
private final LongHashMap<Reference<T>> map;
IdentityScopeLong是持有一个 LongHashMap 结构的 map,内部结构为数据 + 链表形式,并不是继承了HashMap。
到这里,取缓存结束了,那你可能会问为什么上面 loadCurrent 方法注释 1 处 identityScopeLong != null ?
因为在API使用过程中,会创建DaoSession,这个过程内部 指定了 IdentityScopeType 的值为 IdentityScopeType.Session,然后会根据你有没有设置primaryKey,或者设置的Key是 int类型还是short类型或者是long类型,来初始化为 IdentityScopeLong类型。对应于实体类中 @Id(autoincrement = true) 注解。
代码如下
//DaoMaster
public DaoSession newSession() {
return new DaoSession(db, IdentityScopeType.Session, daoConfigMap);
}
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);
}
//DaoConfig
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);
}
}//keyIsNumeric判断
//在DaoConfig初始化构造函数时执行
if (pkProperty != null) { Class<?> type = pkProperty.type;
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;
}
二、将数据存入缓存
同样,在之前的文章中分析过,在插入数据过程中,(见 executeInsertInTx 注释 4处) 它会为entity实体类设置ID,并调用attachEntity将ID和实体类关系映射到IdentityScope中。前提是你设置了主键,主键是否设置从CustomDao中获取。
protected void updateKeyAfterInsertAndAttach(T entity, long rowId, boolean lock) {
if (rowId != -1) {
K key = updateKeyAfterInsert(entity, rowId);
attachEntity(key, entity, lock);
} else {
// TODO When does this actually happen? Should we throw instead?
DaoLog.w("Could not insert row (executeInsert returned -1)");
}
}
只要插入数据成功,rowId就不为 -1,就会执行 if 里面的两行代码
//CustomDao
@Override
protected final Long updateKeyAfterInsert(Custom entity, long rowId) {
entity.setId(rowId);
return rowId;
}
可以看到第一行代码内部直接设置了Custom的 id 。
接下啦看第二行代码
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);
}
}
}
我们先看一下attachEntity做了什么?
protected void attachEntity(T entity) {
}
啥也没做。。
因为lock 参数传递的是false ,所以直接看 identityScope.putNoLock方法,这里留意一下,key是rowId,设置给了Custom的id,entity是Custom类。
//IdentityScopelong类
@Override
public void putNoLock(Long key, T entity) {
put2NoLock(key, entity);
}
public void put2NoLock(long key, T entity) {
map.put(key, new WeakReference<T>(entity));
}
map的结构是 LongHashMap<Reference<T>>
这里直接将 key和Custom类的弱引用做了一个映射放到map中。
我们看一下是如何存放的
public T put(long key, T value) {
//根据key计算出index的位置, long是 8个字节 64位 int 32位 ,高32位 ^ key低32位 然后 &
//这个计算过程对于hash冲突是友好的,加上避免了 long % capacity 还是long类型的问题
final int index = ((((int) (key >>> 32)) ^ ((int) (key))) & 0x7fffffff) % capacity;
final Entry<T> entryOriginal = table[index];
for (Entry<T> entry = entryOriginal; entry != null; entry = entry.next) {
if (entry.key == key) {
T oldValue = entry.value;
entry.value = value;
return oldValue;
}
}
table[index] = new Entry<T>(key, value, entryOriginal);
size++;
if (size > threshold) {
//扩容
setCapacity(2 * capacity);
}
return null;
}
我们再看一下LongHashMap 是如何扩容的,和HashMap一样,都需要重新计算index位置
public void setCapacity(int newCapacity) {
@SuppressWarnings("unchecked")
Entry<T>[] newTable = new Entry[newCapacity];
int length = table.length;
for (int i = 0; i < length; i++) {
Entry<T> entry = table[i];
while (entry != null) {
long key = entry.key;
//重新根据key分配index
int index = ((((int) (key >>> 32)) ^ ((int) (key))) & 0x7fffffff) % newCapacity;
Entry<T> originalNext = entry.next;
entry.next = newTable[index];
newTable[index] = entry;
entry = originalNext;
}
}
table = newTable;
capacity = newCapacity;
threshold = newCapacity * 4 / 3;
}
三、引发的问题
1、实体类不支持继承,继承父类的成员变量不能直接存储到数据库中
一般写数据库实体类不会继承,例如Custom类,你会写一个A extends Custom ? 不会。
2、数据库查询缓存不一致问题
你是否遇到过更新数据库后或者是更新数据后,反复查询但是得不到更新后的数据问题?
还有这问题?
反正我没遇到过,平时开发使用的都是同事以前封装好的依赖库,经过查询和验证,确实存在该问题,并且需要在合适的事件清理缓存。
问题发生原由
因为有缓存,每次取数据都是从缓存里面取,所以你更新了数据库,取数据也不是最新的
问题解决
a、在创建DaoSession对象时,指定IdentityScopeType.None,这样,获取的 identityScope为空,缓存就用不了了。
b、在更新数据前,调用 DaoSession 的 clear方法,内部会将 identityScope 缓存清除,一般都是采用这个做法,根据业务需要,在合适的时机清空缓存。
c、通过Dao实现类,例如CustomDao ,调用 deleteAll 方法,不建议,因为内部会删除表。
public void deleteAll() {
// String sql = SqlUtils.createSqlDelete(config.tablename, null);
// db.execSQL(sql);
db.execSQL("DELETE FROM '" + config.tablename + "'");
if (identityScope != null) {
identityScope.clear();
}
}