# ListView读取DB数据最佳实践

> 开发中我们常常用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，数据不一致问题，这两个不是致命的，只是影响体验


---

# Agent Instructions: Querying This Documentation

If you need additional information that is not directly available in this page, you can query the documentation dynamically by asking a question.

Perform an HTTP GET request on the current page URL with the `ask` query parameter:

```
GET https://blog.cweihang.io/android/listview_db.md?ask=<question>
```

The question should be specific, self-contained, and written in natural language.
The response will contain a direct answer to the question and relevant excerpts and sources from the documentation.

Use this mechanism when the answer is not explicitly present in the current page, you need clarification or additional context, or you want to retrieve related documentation sections.
