从业务角度看下拉刷新

下拉刷新是移动应用里非常常用的一个组件,以前在 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 ,要去同步显示用户的资料,基本上是非常难去实现的。不过这相对服务端性能的改善,也算是可以接受的。

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