QQ邮箱APP漂流瓶开发记录

2014-12-26

QQ邮箱4.0增加了漂流瓶功能,记录一下做漂流瓶过程中碰到的问题。

p1

增量更新

漂流瓶有我的瓶子列表,需要跟后台保持同步,于是当然就需要个增量更新方案,在常见的两种增量更新方案—对比和日志中,选择了后面一种,因为按目前漂流瓶后台的情况,可以很容易地支持这种方式,不需要记录每一步操作,只需要按时间作为key查找这段时间内数据的增删改就行了。这种方案对前端来说这种方式更是方便,只需要记录一个sync-key,往上请求就行。

具体实现方式是:
1.请求列表,返回数据,以及后端当前服务器时间svrtime
2.下一次请求列表,客户端带上上次返回的svrtime,后端从数据库查出svrtime和当前时间这段时间内列表数据的增删改,返回给客户端

后端瓶子有updateTime字段,数据新增或更新时会记录当前时间,所以很容易找到一段时间内所有增改的数据。对于删除,后端删除瓶子时是不会删除瓶子记录的,只会把瓶子标记成已删除,有个标志日期,所以也可以找到时间段内删除的瓶子。

加载更多

瓶子列表不会一次性全部加载,每次请求返回20个瓶子,所以需要个加载更多的功能把后面的瓶子拉回来。实现的方式是把客户端列表最后一个瓶子的id上法给服务端,服务端把所有瓶子按时间排序后找到这个瓶子id对应的位置,返回它后面的20条数据给客户端。

瓶子是按时间排序的,瓶子的时间会变化,例如回复一个很早的瓶子,这个瓶子时间就会更新,跑到列表的最上面来,这样的场景导致加载更多会有问题。

p2

1.最后一条数据被更新
如上图,客户端列表当前状态是Bottle1-Bottle40排列,这时服务端Bottle40已经被更新,跑到最上面了,客户端还没有更新过来,这时点击客户端的加载更多,会把Bottle40作为游标上发给服务端,期望返回Bottle40以后的数据,结果服务端返回的是Bottle1-Bottle20。结果导致的效果是加载更多完成后列表上的数据完全没变化,再点多少次加载更多都一样。

2.最后一条数据被删除
再看看瓶子被删除的情况,上发Bottle40到服务端后,服务端已经无法找到Bottle40的位置,只能什么都不返回,表现跟上述瓶子被更新的情况一样。

加载更多的实现是以前后端数据一致为前提的,上述问题就是前后端数据不一致导致的,解决方法也挺简单,就是加载更多会对列表做一次增量更新,若有更新,直接返回增量更新的内容,若无更新才执行加载更多的流程,确保数据一致的前提下才进行加载更多。

时序问题

客户端的愿望是跟服务端数据保持一致,但在请求发出->返回的间隙中会出现一些数据不一致的问题,例如:

p3

发送漂流瓶->后台接收到请求,客户端尚未接收到返回
->加载漂流瓶列表->把发送成功的瓶子拉回来了->本地还有一个发送中的瓶子(没有id)->出现两个一样的瓶子
->发送漂流瓶请求返回客户端

除了发送瓶子,发送聊天,回复海滩瓶也有同样的问题。为解决这个问题,为这些可能会关联依赖的请求做了个队列,让发送漂流瓶的请求完成后再发出请求,就不会出现上述问题了。

这个队列属于逻辑层,不能在网络层直接做,而请求是异步的,用原生的GDC队列无法让异步的请求排队,为此做了个简单的异步队列:

@implementation FMAsyncQueue {
    NSMutableArray *_taskQueue;
}
- (id)init
{
    self = [super init];
    if (self) {
        _taskQueue = [[NSMutableArray alloc] init];
    }
    return self;
}
- (void)addTask:(FMBlockTask)blockTask
{
    [_taskQueue safeAddObject:[blockTask copy]];
    if (_taskQueue.count == 1) {
        blockTask(self);
    }
}
- (void)next
{
    [_taskQueue removeFirstObject];
    if (_taskQueue.count) {
        ((FMBlockTask)[_taskQueue firstObject])(self);
    }
}
@end

使用大致是这样:

FMBlockTask blockTask = ^(FMAsyncQueue *queue) {
    httpCompleteCallback oldOncomplete = [callback.oncomplete copy];
    callback.oncomplete = ^(QMHttpResponse *response, QMHttpError *error) {
        oldOncomplete(response, error);
        [queue next];
    };
    [QMCGIManager post:url withCallback:callback];
};
[_asyncQueue addTask:blockTask];

把请求排起队来,可能会导致速度慢,但对漂流瓶来说足够了,影响不大。

断网

另外还有个问题,如果发送过程中断网了,也回导致数据出现异常:

p4

发送漂流瓶->后台接收到请求->返回过程中手机断网接收不到->本地多了发送失败,后台实际已发送成功->更新列表后,出现两个一样的瓶子
这种情况目前没有处理,考虑到处理这种情况的性价比低,还会让逻辑变得复杂,这种罕见的情况造成的后果也只是多出一个发送失败的瓶子,用户手动删除即可,不造成其他影响,所以暂不处理。

其他

离线操作

漂流瓶的删除是支持离线操作的,没有网络也可以删除瓶子,用的是QQ邮箱已有的离线操作库,原理是把类,调用的方法,调用的参数包装成一个Operation,序列化保存到本地,下次启动如果有网络,把这些Operation全部拿出来,用NSInvocation执行这个方法。函数方法和参数可以保存,但callback block是保存不了的,无法回调请求onSuccess等方法,不过也不需要,做这类型的操作都是执行后就当发送成功,进行相关处理,离线操作库会不断尝试把这条请求发送出去。

TableView不等高Cell

瓶子列表和聊天列表每一行字数不一样,行高也就不一样,行高是要根据文字渲染后占的高度决定的,但UITableView每次刷新都要知道每一行的高度,才能确定整个TableView的高度,如果每次TableView刷新都把每一行数据渲染一遍算出高度,显然数据多了会有性能问题。这个问题应该是很常见了,微博就是这样,解决方法也很简单,就是把渲染后的高度存入数据库,下次获取高度时就不需要渲染,直接返回保存的高度就行了。

漂流瓶整体还是比较简单的,碰到的问题应该是很多需要跟后端同步数据的APP共有的问题,在用户量还没有上去的情况下没再做更多的优化,先记录下来做个备忘。

分类:技术文章
评论

*

*

2014年12月27日 10:54

赞~

2015年4月1日 11:03

送你32个赞

2015年5月26日 14:04

[…] QQ邮箱APP漂流瓶开发记录: QQ邮箱4.0增加了漂流瓶功能,记录一下做漂流瓶过程中碰到的问题。 – /tech/2502/ […]

2015年6月1日 8:58

学到了不少

2015年8月5日 19:21

设置target queue

2015年9月19日 15:50

`用原生的GDC队列无法让异步的请求排队`,这个用 dispatch_group 可以解决

Baidu
sogou