从业务角度看下拉刷新

下拉刷新是移动应用里非常常用的一个组件,以前在 Android 里官方是没有提供下拉刷新的组件的,后来也有了,一般下拉刷新还会配合上滑加载更多来使用,最早时候,只有 ListView ,现如今不仅有 ListView ,还有其替代品 RecyclerView ,甚至是 TextView ,ImageView 等各种 View 和 ViewGroup 都可以扩展下拉刷新,当然今天要讨论的仅限于最常用的 ListView 和 RecyclerView 。

常见处理

一般情况下,根据业务需求,做下来刷新,我们都是这样处理,下拉刷新,从服务端获取最新数据,假如 是 20 条,然后缓存在本地,等再进入到这个界面,然后先加载缓存里的数据,还是 20 条,再从服务端获取最新数据,覆盖缓存里的数据;当上滑加载更多的时候,在之前的获取最新数据的集合里再 add 进来加载到的更多数据,当然,一般会通过分页也处理,需要传递 page 和 size 参数,而这时候,缓存是不动的,缓存里永远是最新的数据。基本就是这个流程,至少我见过的很多项目的下拉刷新都是这么处理的,这是处理起来最简单的,也基 本上很少出错的。

常见方案的问题

但是这是不是最优的方案呢?按照我的理解,某种程度上,是的。但是,也有很多场景下, 这不是最优的方案。之前自己也接触过一些后台的东西,也做了一些,不过基本上所有工作都还是集中在前台,一直以来,我都有种感受,如果单纯地一直做前台的 东西,视野还是太窄了,面对很多问题,都会很无力。这里所说的场景和从业务角度,就是考虑到后台的一些东西。任何业务,必须要前后台配合工作,才会有更高 的性能。

这样,我再来看下拉刷新的处理,“下拉刷新,从服务端获取最新数据”,也就是说,每次下拉刷新,都要从服务端获取一次数据,而服务端返回数据的时 候,也是从头开始,取 20 条返回。如果没有最新数据,依然会返回 20 条,这样怎么想都不合理啊,如果服务端数量非常大,这样服务端压力真的是会很大啊,而且本地已经有了这 20 条的缓存了。当然,如果这 20 条中其中某一条被删除了,这样确实也不会出错,但是服务端的成本也有点高了吧,而且本地做的缓存也将一定程度上失去意义。如果是类似于新浪微博那样的一条 条动态,原微博有了转发之后,再删除原微博,而转发还存在,在服务端可能并不会真正地删除原微博,而是通过标识设置原微博的存在或者可见状态,以减轻服务 端的压力,因为动态太多了,每个删除操作都要耗费大量资源。那么如果能更好地利用缓存,同时让服务端尽可能少地去操作数据库,那效率就会提高很多。

再看上滑加载更多,一般都是做分页处理,传参 page 和 size ,这样就又出现另一个问题,需要在服务端将所有的数据拆分为页,然后再由客户端去请求,那如果服务端不需要考虑分页,效率就能再提高一些了。

对常见方案的改进

这样分析之后,我们就可以从两方面去对现有业务去整改。其实也很容易想到,在服务 端数据库里,类似于这种动态,都有自己的 id ,如果我们能记下获取到数据的最大的 id 和最小的 id ,下拉刷新的时候,把最大 id 传给服务端,服务端只返回比最大 id 还要大的数据,上滑加载更多的时候,返回比最小 id 还小的数据就好了,当然 size 还是要传的,只是将 page 这个参数替换成了最大 id 或者最小 id 。这只是第一步。

第一步做完了,其实是有很大的一个问题的,如果我传的 size 比服务端现有的最新数据要小,那服务端返回的数据就不是最新的了,而是比最大 id 大一个 size 的数据,这时候,如果有一个标识,来标记服务端是否按照最大 id 返回数据就好了。这个标识要服务端来返回,客户端需要记录下来,客户端根据这个标识来决定怎么处理本地的数据集合。当然这个标识只有在获取最新的时候才有 意义,如果是上滑加载更多,就用不到这个标识了。假设这个标识为 more ,本地负责展示的数据集合是 localDynamics ,如果服务端返回的 more 为 1 ,说明服务端不是按最大 id 返回的数据,而是返回比最大 id 还要大的最新的数据,这时候我们需要清空前面的本地数据集合 localDynamics ;如果 more 为 0 ,说明服务端是按照最大 id 返回的数据,返回的数据条数小于或者等于 size ,当然也是最新的,这时候就不能清空前面的本地数据集合 localDynamics ,这样就能完美地接上前面显示的数据列表了。

然后就是处理加载更多,加载更多时,只需要保留之前的 localDynamics ,在它的基础上再 add 进来加载更多的 size 条数据,然后刷新界面即可。其实,这里还有一个小技巧,加载更多的 size 可以按常规来就是 20 条;获取最新数据的 size ,不能太小,也不能太大。太小了,可能就需要频繁地去加载更多,用户之前的加载更多获取的数据,很可能被冲刷掉;如果太大了,对用户的流量和网络状况就是 比较大的考验了。如果加载更多是 20 条,那加载最新是 50 条就可以了。

接着,就是处理缓存,上面的处理完了,缓存就简单多了,根据上面的分析,由于在获取最新数据的时候 localDynamics 始终都是最新的数据,如果要缓存 20 条,只需要从 localDynamics 取出 id 最大的 20 条存起来就好了。当然,先加载本地数据,再加载服务端数据,覆盖缓存,刷新界面这个原则是不变的。

最后,在服务端处理请求的时候,根据返回的最大 id 或者最小 id ,按顺序取比最大 id 大的数据或者比最小 id 小的数据就好了。

当然,这样处理,其实还是有一些问题的,比如,如果条目里有用户资料,用户先是加载了一些数据,本地有了缓存,过了比较长时间,在服务端有了最新数 据,但是最新数据的条数是小于加载最新的 size 的话,用户修改了个人资料,然后再去加载最新数据,这样显示在界面里的数据其中一部分就是以前缓存里的数据,而无法体现个人资料的修改了。而由于是 ListView 或者 RecyclerView ,要去同步显示用户的资料,基本上是非常难去实现的。不过这相对服务端性能的改善,也算是可以接受的。

其实,研究一下新浪微博客户端,可以发现,他们的实现效果跟我上面讲的那些也都是一样的,包括最后讲到的那个问题。