一、背景
最近针对某个需求模块进行review时,发现了一处技术方案设计不合理的地方。总结下来发现比较有意思,在这里总结分享一下。
1、需求背景
在某一个需求中,我们需要在联系人名称右侧展示用户的标签状态。简单来讲就是用户的考勤状态如正常、请假、外出、出差,还有用户在离职状态。在我们的App有以下几个位置需要展示用户的标签【联系人Tab】【聊天页面】【群成员列表】等。
2、数据的获取与更新
在上面提到的几个页面中,【聊天页面】产品要求数据需要尽可能的实时显示,所以每次进入聊天页面都会调用接口拉取一次最新的数据。而【联系人Tab】【群成员列表】都不需要更新非常及时,超过30分钟拉取一次就可以。
二、当前现状
1、当前实现方案
可以看到当前实现方式比较简单,进入联系人Tab、群成员列表页面均会检查联系人的标签。
如果距离上次批量拉取未超过30分钟,直接从缓存中加载标签数据。距离上次批量拉取超过30分钟,则开始批量拉取当前列表中所有的联系人数据。
2、当前技术方案存在问题
问题1:全量拉取
每次进入联系人Tab、群成员列表页面,当超过30分钟后,会全量拉取所有联系人的标签数据,即使这个联系人没有展示还不需要标签数据。假设用户存在10000个联系人,以50人一组拉取用户标签数据,那么进入联系人Tab就会触发拉取200次联系人标签数据。
问题2:全量刷新
拉取到用户标签后,直接全量刷新联系人列表。 首先全量拉取所有联系人的标签状态会造成列表进行多次不必要的刷新,其次刷新未使用局部刷新体验不好。
问题3:时间戳控制不够细致
当前时间戳记录的批量用户拉取标签的时间,不是根据个人的。
问题4:加载标签不够优雅
当前伪代码:
标签声明
data class UserLabel(val attendance: Int, val empType: Int, val uid: String)
标签管理接口
interface IUserLabelManager {
/**
* 加载标签
*/
fun loadUserLabel(uid: List<String>, listener: IUserLabelListener)
/**
* 加载标签
*/
fun loadUserLabel(uid: String, listener: IUserLabelListener)
/**
* 释放资源
*/
fun clear()
}
标签回调接口
interface IUserLabelListener{
/**
* 标签加载成功
*/
fun onLoadUserLabel(labelList:List<UserLabel>?)
/**
* 标签加载失败
*/
fun onLoadFailed()
}
外部加载标签
fun showUserLabel() {
val uids = mutableListOf<String>()
UserLabelManager.loadUserLabel(uids, object : IUserLabelListener{
override fun onLoadUserLabel(labelList: List<UserLabel>?) {
//更新列表
}
override fun onLoadFailed() {}
})
}
通过伪代码可以看到,在 onLoadUserLabel接口回调之后,我们需要根据UserLabel中的uid,找到对应的好友在列表中的位置然后刷新。
这样的方案不是不好,而是不够优雅。作为Android开发应该都使用过Glide加载图片,将ImageView对象以及Url传递给Glide,后续内部就会下载图片、加载到控件。那么我们是否也可以仿照Glide设计类似的接口?
三、改造优化方案
1、优化方向
在上一节中介绍了当前方案存在的问题,那么在这里我们就总结一下有哪些优化点:
- 按需拉取,避免全量拉取
- 保持批量加载逻辑,防止接口调用过于频繁
- 多级缓存,超时重新拉取
- 自动加载,UI层使用方便
2、新版逻辑图
3、接口设计
新增ILabelShow接口
interface ILabelShow {
/**
* 用户id
*/
fun userId(): String
/**
* 展示标签
*/
fun labelShow(label: UserLabel)
/**
* 当本地存在时是否使用缓存
*/
fun useCache(): Boolean
}
新接口ILabelShow中有三个方法,让调用者自定义
- userId() 告诉框架当前加载标签的用户
- labelShow() 展示标签
- useCache() 告诉框架是否使用缓存
IUserLabelManager接口新增一个loadUserLabel方法,入参是ILabelShow
interface IUserLabelManager {
/**
* 加载标签
*/
fun loadUserLabel(showTxt: ILabelShow)
...
}
新增一个IDrawableLabel接口
interface IDrawableLabel {
fun setTag(key: Int, id: Any?)
fun getTag(key: Int): Any?
fun appendDrawable(drawable: Drawable)
fun clearDrawable()
}
IDrawableLabel用于定义TextView的行为,自定义TextView实现IDrawableLabel,提供appendDrawable()、clearDrawable()功能,用于展示、清除标签的能力。
4、实现类
提供一个默认的DefaultUserLabelShow实现
class DefaultUserLabelShow(
private val userId: String,
private val wfLabel: WeakReference<IDrawableLabel>,
private val cache: Boolean = true
) : ILabelShow {
init {
wfLabel.get()?.clearDrawable()
wfLabel.get()?.setTag(R.id.user_label_task_id, userId)
}
override fun userId(): String {
return userId
}
override fun labelShow(label: UserLabel) {
val taskId = wfLabel.get()?.getTag(R.id.user_label_task_id)
if (taskId != null && TextUtils.equals(taskId.toString(), label.uid)) {
val drawable = getDrawable(label)
if (drawable != null) {
wfLabel.get()?.appendDrawable(drawable)
} else {
wfLabel.get()?.clearDrawable()
}
wfLabel.get()?.setTag(R.id.user_label_task_id, null)
}
}
override fun useCache(): Boolean {
return cache
}
fun getDrawable(label: UserLabel): Drawable {
...
}
}
DefaultUserLabelShow 类提供了基本的定制能力,告诉框架当前需要加载标签的账号是多少,是否使用缓存,获取标签对应的Drawable对象。
后续在其他业务模块如果有新的业务需求考虑,当DefaultUserLabelShow不满足使用时,也可以自行实现ILabelShow。
5、使用
UI层加载标签仅需调用
UserLabelCacheManager.loadUserLabel(new DefaultUserLabelShow(uid, new WeakReference<>(tvName), true));
相比初版的设计,在调用上变得非常简洁了。不再需要遍历所有的用户数据,然后一次更新了。
四、总结
至此针对标签展示这样一个小需求的方案设计就优化完了,新版方案相比旧版有以下特点:
- 新版方案稍显复杂,新增多个接口
- 新版方案按需加载,展示时才加载标签,旧版方案进入页面后触发全部加载
- 新版方案UI调用方案相对简单,类似Glide
虽然加载标签是一个小的需求,但App实则就是基于这样一个个小需求组合起来的,如果每个方案无法做到尽可能优,那么App性能最后会被各种需求拖累积重难返,所以针对每一个需求,方案都要尽可能的好,本方案虽然说不上完美,但是相较于旧版方案有了很大的进步。