一、需求描述
在快速变动的电商类APP中需要刷新商品信息尤其是数据,以保证商品信息的准确性。在保证数据刷新的同时,因用户的阅览习惯,在刷新的数据时还需要保证浏览位置不变动。因此,我们决定讨论如何能更用户更友好的进行局部刷新。
让我们分析先来这一类商品的共同点与要求:
- 商品一般为快速消费: 变动速度快.商品信息进度往往在几分钟内变动数次甚至下架。
- 商品信息变化大:及时性要求高,需要对用户做出快速反馈
- 一般收到受到客观限制:商品打折时间,库存等物理或活动上的限制
- 刷新不能过于频繁,要权衡服务器压力
因此,我们需要平衡刷新的时效性与流量限制,抉择刷新策略和机制。
一、刷新机制的策略讨论
经过一些方案与谈论,我们有了初始的两种策略。
基于两种游标的刷新策略:
1.页标游标刷新 (左)
使用页标与游标刷新:以当前页面为页标为记录,接入上拉与下拉加载功能。用以保证上拉与下拉后的数据为最新。
优势:能保证数据始终最新,能获得最新的商品信息。时效性及时。
劣势:处理逻辑复杂,需要加上拉加载功能并和上拉刷新兼容,工作量巨大。需要处理因数据页码变动产生的商品重复。本屏刷新策略与上下屏不一致,逻辑判断情况多。
2.唯一标识刷新 (右)
使用唯一标识刷新:保持已有的原数据与商品列表,通过唯一标识,只抓取商品的最新数据。进行时间,库存的刷新。(新数据的同步和抓取交于另一个定时器,数据刷新只负责取最新数据)
优势:最小化原则,职能单一,逻辑实现简单。要求简单,单个或多个键值唯一确定一个商品数据即可。最大的优势是用户体验好,感知度较低。既能实时刷新数据,又不会影响使用的直观性。因数据不会变动,位置回滚操作也不需要额外的工作。
劣势:不能即时的获取新上架的商品信息。如果商品为组合类商品,不容易通过唯一标示查找不能使用。请求次数较零散与频繁。且若该商品已下架需要通过标识告诉用户结果,以免误认为发生bug。
对比了以上两种方案,我们最终选择了处理逻辑统一,用户使用不太突兀的方案(2),并对初始方案进行了优化,以减少请求量,平衡服务器压力。因刷新场景的常见性,我们做成了SDK的形式希望在能多个项目中使用。
二、缓存策略
触发流程:
- 刷新机制: 用户滑动停止触发
- 获取刷新id数组与位置(IndexPath)
- 过滤数据,返回需要刷新的数组id
- 通过代理返回id数组及位置等信息
- -> 控制器发送请求
- -> 刷新数据更新时间
方案文案如下:
三、系统优化
1.处理用户拖动误触发,进行函数防抖
// 请求,是否对过滤数据
private func pullShopItemsData(filter: Bool = false){
// 0.5秒函数防抖, 合并请求再发送
self.timer?.invalidate()
self.timer = Timer.scheduledTimer(timeInterval: 0.5, target: self, selector: #selector(refreshOperaction(timer:)), userInfo: ["refreshRightNow":filter], repeats: false)
}
2.预测用户浏览轨迹,进行页面上下半屏预刷新
if (maxRow != -1 && minRow != listData.count+1) {
// 上下两屏幕
NSInteger rest = (10 - refreshIdList.count) > 0 ?(10 - refreshIdList.count):0;
NSInteger beforeNum = floor(rest/2.0);
// 前半屏
NSInteger startNum = (minRow - beforeNum>0?minRow - beforeNum:0);
for (NSInteger i = startNum; i<minRow; i++) {
NSIndexPath *path = [NSIndexPath indexPathForRow:i inSection:4];
ShopItemsDataSourceModel *model = [[ShopItemsDataSourceModel alloc] initWithSourceData:listData[i] indexPath:path];
[refreshIdList addObject:model];
}
// 后半屏
NSInteger afterNum = rest - beforeNum;
NSInteger endNum = maxRow+afterNum < listData.count-1 ? maxRow+afterNum : listData.count-1;
for (NSInteger i = maxRow+1; i<=endNum; i++) {
NSIndexPath *path = [NSIndexPath indexPathForRow:i inSection:4];
ShopItemsDataSourceModel *model = [[ShopItemsDataSourceModel alloc] initWithSourceData:listData[i] indexPath:path];
[refreshIdList addObject:model];
}
}
return refreshIdList;
3.减少请求数方差,使用旧数据填充
保证及时性,发送请求前进行最小请求量合并,进行节流。
// 请求数不足,填充老数据
private func itemsWithFillOldData(originalRefresItems: RefreshItemsInfoStruct, toNum minNum: Int) -> RefreshItemsInfoStruct {
var filledItems: RefreshItemsInfoStruct = originalRefresItems
let minRequestNum = minNum - originalRefresItems.count;
let sortedArray = self.refreshInfoDict
.filter({return !originalRefresItems.keys.contains($0.key)}) // 除去已确定要刷新元素
.sorted(by: {$0.value.lastRefreshStamp < $1.value.lastRefreshStamp}) // 根据刷新时间排序
let maxIndex = minRequestNum < (sortedArray.count)
? minRequestNum - 1
: (sortedArray.count) - 1
if ((sortedArray.count) > 0) {
// 切割数组
let sortedArray = sortedArray[0...maxIndex]
for infoTuple in sortedArray {
filledItems.updateValue(infoTuple.value, forKey: infoTuple.key)
}
}
return filledItems;
}
4.定时器周期性刷新
四、SDK 类图
使用代理模式, 传入想要刷新的cell信息,代理者只负责判断是否数据过期,返回需要刷新的列表,具体刷新操作由使用者去做。
五、接入说明
1.初始化代理,遵循 ShopItemsRefreshProxyProtocol 协议
// 初始化商品刷新代理
self.proxy = [[ShopItemsRefreshProxy alloc] initWithRefreshMaster:self];
2.实现两个 数据来源 与 请求操作 两个代理方法
// 数据来源
- (NSArray<ShopItemsPrepareInfoModel *> *)shopItemsPrepareToRefresh{
NSMutableArray<ShopItemsPrepareInfoModel *> *refreshIdList = [NSMutableArray array];
refreshIdList = //视野内你能见到的cell
return refreshIdList; // 传入所有你想刷新的cell,是否该刷新由代理决定
}
// 数据操作
- (void)shopItemsRefreshOperation:(ShopItemsRefreshProxy *)refreshProxy
itemsToRefresh:(NSDictionary<NSString *,ShopItemsInfoModel *> *)itemsToRefresh
allItemsInfoDic:(NSDictionary<NSString *,ShopItemsInfoModel *> *)allItemsInfoDic {
__weak typeof(self) weakSelf = self;
[self showHomeLoadView]; // 刷新动画
// 发送请求服务
[refreshItemsService refreshHomeCellWithRefreshIdDic:itemsToRefresh
completeHandle:^(NSDictionary<NSString *,id> * _Nullable respond, NSError * _Nullable error) {
// ...请求成功,刷新数据源
[refreshProxy updateRefreshTimeWithItems:refreshItems];// 刷新数据的跟新时间
[self hideHomeLoadView];// 停止刷新动画
}
}
3.进行刷新
// 在拖拽结束方法进行调用
- (void)scrollViewWillEndDragging:(UIScrollView *)scrollView withVelocity:(CGPoint)velocity targetContentOffset:(inout CGPoint *)targetContentOffset{
if (self.proxy) {
[self.proxy pullIntelligent]; // 进行刷新
}
}