ListView读取DB数据最佳实践

Last updated 14 days ago

开发中我们常常用listview来呈现DB中的数据,但常见的许多做法,都没能全面考虑读写并发,内存优化。这里希望由浅入深讨论得到一种ListView读DB的最佳实践。

  • 需求

从服务器获取 头像 + 名字,放到ListView中显示,本地存储 头像url + 名字 底部加载更多,加载服务器数据 无数据时显示空白页

这里只做逻辑层讨论,UI层,则不做不必要讨论。

  • 分析

    • 首先,从服务器获取,就涉及异步写DB,以及在写DB时的并发读

    • 用户随时可能发起写DB操作

    • 是否能够加载更多,是需要从server获取的另一个数据,listview的item数量与这个值相关(影响getCount)

    • 考虑listview数据源清空的正确姿势

  • 架构

    • 至少三个线程,UI主线程,读DB线程,网络请求写DB线程

    • M层:头像url + 名字

    • V层:listview,footer作为加载更多

    • C层:adapter

  • Adapter的设计

    • 方案一 Cursor读取DB,遍历Cursor把数据都放到一个list放在内存里,关掉cursor,listview访问list作为数据源

      优点:没有cursor关闭问题,以及cursorwindow被置换的风险

    缺点: 一次读出DB数据会占用大量内存空间,采取游标窗口的设计,需要实现java层的cursorwindow,成本太高

    • 方案二 Cursor读取DB,listview访问cursor作为数据源

      优点:cursor机制自动达到所取即所需的机制,不会占用太多内存。

    缺点:必须谨慎考虑关闭Cursor的时机。

对方案二做一些讨论:

  • Q 什么时候关闭cursor?

  • A 在cursor不与listview相关联时才能关闭,否则listview滑动中,getView调用cursor.getXXX()方法会报IllegalStateException: attemp to open a database or cursor which is already closed

  • Q 怎样读取新数据?

  • A 点击加载更多时,重新查询DB,得到新的cursor(读DB线程),并将新的cursor交给listview的adapter(adapter.setCursor),然后adapter.notifyDataSetChanged()(UI主线程),这就引出另一个问题。

  • Q adapter.setCursor应该在哪个线程做?

  • A 假设放在读DB线程,setCursor后,通知主线程adapter notify,那么在主线程执行notify前,DB线程setCursor后,这段时间内,如果listview的adapter执行getView,就会在onLayhout时产生IllegalStateException,item num of listview changed, but not notify。如果一定要放DB线程做setCursor也行,不过要在onLayout时,与setCursor线程竞争锁,一来影响体验,二来影响性能。所以应该放在主线程做。

  • Q 数据库数据变动后,应该如何改变adapter?

  • A 首先,cursor在读DB线程中必须执行moveToPosition()来达到预读的效果,否则会在主线程读DB造成延迟。所以要在子线程中读DB,而setCursor又要在主线程做,这就需要用handler之类的线程通信来实现,因为子线程不能直接更改listview的adapter使用的cursor,所以,DB中要有一个新的cursor来承载新数据,再通知主线程换掉adapter中的cursor并notifyDataSetChanged。换掉adapter中cursor后,应该在notify后,马上关闭旧cursor。当然这里可以做一个优化,即对cursor做一个引用计数,在activity生命周期结束时清理所有cursor。

  • Q 加载更多的实现?

  • A 一个footer,会计入listview的adapter的getCount中,adapter.count = localData.count + hasFooter?1:0;为了正确显示加载更多,实际上请求网络数据时,会请求服务器数据量svrCnt,hasFooter = svrCnt > localData.count;而localData.count又是一次异步查DB的过程,所以,为了得到新的数据,我们需要先查DB得到当前position相关的DB数据(一个cursor),同时做一次cursor.getCount()得到hasFooter(一个boolean),这里就涉及到一个刷新策略的问题:

  • Q 新数据与footer应该分别刷新listview还是等两个数据都拿到再刷新listview?

  • A 分别刷新有两个问题,1,性能问题,2,数据不一致问题,这两个不是致命的,只是影响体验