在前文GreenDAO 3.2 源码分析(1):查询语句与Query实例的构造中,我们分析了greenDAO是如何构建SQL语句并维护Query对象的。为了提高在多线程的操作中的效率,greenDAO并没有采用上锁的机制,而是对每一个线程都单独分配一个Query对象来执行查询操作,那Query对象是获得查询结果的呢?让我们在本文中一探究竟吧。
2. 1查询的种类
在Query类中一共定义了四种获得查询结果的方法:
- list() : 将全部结果都放在内存中,以list形式返回;
- listLazy() : 只用真正使用到数据时, 才回去访问数据库获得数据。注意需要主动关闭游标;
- listLazyUncached(): 和listLazy()类似,但是没有使用cache,所以数据不能复用,每次都要去数据库中读取;
- listIterator(): 以List迭代体的形式返回结果,当迭代体遍历会后,游标关闭。
看到这么多方法供选择,是不是满脑子都是问号:每种方法在实现上又有什么不同呢?在设计上有什么考虑呢?应该在哪种场景中使用呢?有问题就有意义,让我们从每个方法的源码上开始分析吧。
2.2 list()
简单粗暴,先来源码。
/** Executes the query
* and returns the result as a list
* containing all entities loaded into memory. */
public List list() {
//1. 检查当前线程和Query是否匹配;
checkThread();
//2. 获得游标;
Cursor cursor = dao.getDatabase().rawQuery(sql, parameters);
//3. 通过该游标将所有的数据都遍历读取存入内存中
return daoAccess.loadAllAndCloseCursor(cursor);
}
正如源码中注释所说的,list()是将所有的查询结果都读入内存中,然后以List的形式返回查询结果,List中的每一个元素都是一行查询结果。代码的逻辑也很清晰,一共三行:
- 首先检查当前线程和当前的Query实例是否匹配。前文中已经提过了,greenDAO框架中,执行查询之前,会为不同线程分配不同Query, 以避免多线程查询的冲突,所以在创建Query实例时,Query实例就会把创建它的线程对象保存起来。当要执行查询时,首先要判断当前调用它的线程是否就是创建它的线程,如果不是,就会抛出异常,其代码如下:
protected void checkThread() {
if (Thread.currentThread() != ownerThread) {
throw new DaoException(
"Method may be called only in owner thread, use forCurrentThread to get an instance for this thread");
}
}
- 获得游标。这是数据库查询的标准动作,有了游标才能一行一行的处理查询结果,而重点就是如何处理;
- 使用游标获得所有的查询结果,并把它放到LIst中。list()通过游标遍历所欲的查询结果,把获取到的所有数据都存在内存中。执行的过程中,最终会调用* loadAllFromCursor*方法,其代码如下:
/** Reads all available rows from the given cursor and returns a list of entities. */
List loadAllFromCursor(Cursor cursor){
.....
// 1. 确保游标在开始处
if (cursor.moveToFirst()) {
if (identityScope != null) {
identityScope.lock();
identityScope.reserveRoom(count);
}
try {
if (!useFastCursor && window != null && identityScope != null) {
loadAllUnlockOnWindowBounds(cursor, window, list);
} else {
//2. 循环语句开始
do {
//3. 将查询结果放入list中
list.add(loadCurrent(cursor, 0, false));
} while (cursor.moveToNext()); //4. 通过循环遍历每一行数据,将其保存在list中;
}
} finally {
if (identityScope != null) {
identityScope.unlock();
}
}
}
该函数中有大量的维护查询操作的代码,以上只是节选的部分,请注意注释2,3,4所标识的do-while循环,正是这里把所有的查询结果都放入了List中。
3. listLazy() & listLazyUncached()
这两个方法关系紧密,所以放在一起说。通过方法名字可能有些读者已经猜到了:既然list方法是把所有结果都取出来放在内存中,那带有Lazy的方法就是比较“懒”的,并不一次性把结果取出来。事实上的确如此,这两个方法获得游标后,并不着急把全部结果都取出来,而是等到真正要使用某个结果时,再去数据库中读出数据。两种方法的差别在于,listLazy()使用缓存机制,一个结果被使用过后会被保留下来,下次再使用该结果时,就不用再去数据库中读取,而 listLazyUncached方法不使用缓存机制,所有结果在使用后不保存,每一次都需要去数据中读取。
/**
* Executes the query and returns the result as a list that lazy loads the entities
* on first access. Entities are cached, so accessing the same entity more than
* once will not result in loading an entity from the underlying cursor again.
* Make sure to close it to close the underlying cursor.
*/
public LazyList listLazy() {
checkThread();
Cursor cursor = dao.getDatabase().rawQuery(sql, parameters);
return new LazyList(daoAccess, cursor, true);
}
public LazyList listLazyUncached() {
checkThread();
Cursor cursor = dao.getDatabase().rawQuery(sql, parameters);
return new LazyList(daoAccess, cursor, false);
}
二者的源码和list方法很像,只不过是最后一部返回的是自定义类型* LazyList,而listLazy* 和 listLazyUncached的区别就体现在如何构建LazyList。下面是其构造函数
* A thread-safe, unmodifiable list that reads entities
* once they are accessed from an underlying database cursor.
LazyList(InternalQueryDaoAccess daoAccess, Cursor cursor, boolean cacheEntities) {
this.cursor = cursor;
this.daoAccess = daoAccess;
size = cursor.getCount();
if (cacheEntities) { //1. 如果使用Cache
//2. 虽然创建了大小和游标查询数量相同的list,但是没有添加对象
entities = new ArrayList(size);
for (int i = 0; i < size; i++) {
entities.add(null);//3. 都是null
}
} else {
entities = null; //不适用cache
}
if (size == 0) {
cursor.close();
}
lock = new ReentrantLock();
}
LazyList是一个不可修改的、多线程安全的List。
由源码可以发现,当使用缓存时,LazyList构建了一个和查询结果个数相同大小的List entities,但是并没有将查询结果取出,而是把这个List里所有的元素都设为空引用;当不适用cache的模式下,entities直接为空。那何时真正获得要查询的结果呢?别忘记,LazyList也是当做List使用的,只有当真正去get(int position)时,entities才去加载内容。这是有些类似于代理模式,表面上是使用LazyList,实际上还是由List entities来完成操作的。
@Override
public E get(int location) {
if (entities != null) {
E entity = entities.get(location);
//1. 第一次获得对象
if (entity == null) {
lock.lock(); //上锁,在释放锁之前都要考虑多线程操作的问题
try {
//2. 第二次获得对象,因为在因上锁而等待期间,
// 可能有别的进程已经获得了该对象,所以需要再判断一遍
entity = entities.get(location);
if (entity == null) {
//3. 正真从数据库中获得对象
entity = loadEntity(location);
entities.set(location, entity);//将该对象添加到entities中
// Ignore FindBugs: increment of volatile is fine here because we use a lock
//4. 标记已经获得对象的个数,如果对象已经全部获得,则可以关闭游标
loadedCount++;
if (loadedCount == size) {
cursor.close();
}
}
} finally {
lock.unlock();//释放锁
}
}
return entity;
} else { //5. 不用cache
lock.lock();
try {
return loadEntity(location);//6. 直接获取对象,并不使用缓存
} finally {
lock.unlock();
}
}
}
get方法正是体现出LazyList多线程安全的地方,它考虑到多个线程可能对其内部List所带来的影响:
- 首先判断要获取的对象是否有缓存;
- 如果没有则上锁,然后第二次查看该对象是否有缓存,以防止在上锁之前有线程已经获查询了该对象,从而避免重复获取;
- 两次判断都没有获得对象缓存之后,才正在从数据库中获得该对象,并将其放入缓存;
- 对象加入缓存后,记录当前已经获得对象的个数,如果entities已经被填充完毕之后,则关闭游标;
- 如果不使用缓存,每次都需要从数据库中获取数据。
在上锁之前之后都查询对象缓存是否存在,保证避免多线程操作中冲突和重复,这种设计通用而有效的。
也许有的读者会有疑问,greenDAO是不会通过给每个进程都分配Query对象来避免上锁吗,这里为什么还是加锁? 其实这个不矛盾,greanDAO避免的是数据库操作的上锁,而这里是对查询结果List上锁,因为懒加载的原因,listLazy的结果可能已经被使用者获得,而这个结果集合LazyList可能是被多个进程使用的,所以要在获得查询结果上加锁。
值得注意的是,LazyList是不可修改的,所以其覆盖了list中add,set以及remore方法,如果要修改list中的元素就会报错,比如:
@Override
public boolean add(E object) {
throw new UnsupportedOperationException();
}
使用完毕之后如果需要关闭游标,需要自己手动调用LazyList对象的close方法
4. listIterator()
有了List,自然就有其迭代器。listIterator()就是返回listLazy中的迭代器。
public CloseableListIterator listIterator() {
return listLazyUncached().listIteratorAutoClose();
}
public CloseableListIterator listIteratorAutoClose() {
return new LazyIterator(0, true);
}
LazyIterator是LazyList中的内置迭代器,其源码如下:
protected class LazyIterator implements CloseableListIterator {
private int index;
private final boolean closeWhenDone;
public LazyIterator(int startLocation, boolean closeWhenDone) {
index = startLocation;
this.closeWhenDone = closeWhenDone;
}
@Override
public void add(E object) {
throw new UnsupportedOperationException();
}
@Override
/** FIXME: before hasPrevious(), next() must be called. */
public boolean hasPrevious() {
return index > 0;
}
@Override
public int nextIndex() {
return index;
}
@Override
/** FIXME: before previous(), next() must be called. */
public E previous() {
if (index <= 0) {
throw new NoSuchElementException();
}
index--;
E entity = get(index);
// if (index == size && closeWhenDone) {
// close();
// }
return entity;
}
@Override
public int previousIndex() {
return index - 1;
}
@Override
public void set(E object) {
throw new UnsupportedOperationException();
}
@Override
public boolean hasNext() {
return index < size;
}
@Override
public E next() {
if (index >= size) {
throw new NoSuchElementException();
}
E entity = get(index);
index++;
if (index == size && closeWhenDone) {
close();
}
return entity;
}
@Override
public void remove() {
throw new UnsupportedOperationException();
}
@Override
public void close() {
LazyList.this.close();
}
}
这里的代码就很直接明白了,在next方法中调用listLazy的get方法,其他要改变list元素的方法都被定义成抛出异常。另外如参数closeWhenDone参数被设置为true的话,当迭代器遍历全部内容之后,就会自动关闭游标。
2.4 使用场景
- list() : 将全部结果都放在内存中,这样最为直接和常见,但是如果结果集特别大的话,这样做对于内存的压力比较大;此外,如果只是使用结果集中的一小部分,内存就会很是浪费;
- listLazy() : 只用真正使用到数据时, 才回去访问数据库获得数据,数据加载有延迟,但是比较节省内存,如果不是立刻使用数据结果集,可以考虑;
- listLazyUncached(): 和listLazy()类似,但是没有使用cache,所以数据不能复用,每次都要去数据库中读取,如果结果集的数据不是被反复使用的话,这样做是最为节省内存的;
- listIterator(): 以迭代体的形式返回结果,这样的做法更为自由, 如果需要自定义处理过程的话,可以考虑该方法遍历所有结果。