在IM项目(Android)中,我们需要获取群成员,往往数据较大,获取缓慢,所以需要做个缓存,提升用户体验。
策略
- 如果本地(缓存里)有完整数据,则取缓存里数据,否则从网路获取。(可能多个地方需要加载成员列表,但无法保证那处先加载。比如查看群成员,选择AT群内某人,管理员列表等等。)
- 从网络获取全部成员时,分页获取。采用递归请求,直到没有下一页。
- 最多缓存15个群的成员,如果超过15个,淘汰掉最近最少使用的那个群的成员。
- 单个成员信息获取,会存在临时成员里。(用途是,在群聊天页,实时刷新调天发送者信息。)如果这个群已经有完成的群成员列表,则删除临时成员。(聊天页发送者信息直接从完整的群成员表里取出对比)。
- 支持修改缓存数据。包括增加,删除,修改成员。
方案设计
缓存:
定义成员集合接口:
/**
* 成员集合接口。
*/
interface IMemberSet {
/**
* 放入单个成员。
*/
fun put(member: MemberBean)
/**
* 获取成员。
*/
fun get(imAccount: String): MemberBean?
/**
* 放入全部成员。
*/
fun putAll(memberList: ArrayList<MemberBean>)
/**
* 获取全部成员。
*/
fun getAll(): ArrayList<MemberBean>
/**
* 大小。
*/
fun size(): Int
/**
* 是否全部。
*/
fun isAll(): Boolean
}
定义缓存器接口:
/**
* 缓存器接口。可自定义缓存策略。
*/
private interface IMemberCache {
fun put(groupCode: String, memberSet: IMemberSet)
fun get(groupCode: String): IMemberSet?
fun remove(groupCode: String)
fun clear()
}
获取:
先判断缓存,再递归获取全部群成员。
private val repository by lazy {
MemberRepository(RetrofitHelper.instance.createApiService(MemberApi::class.java))
}
/**
* 加载群成员。
* @param groupCode 群号。
* @param justByNet 只通过网络获取。会把结果缓存。
*/
fun loadMemberList(groupCode: String, justByNet: Boolean = false): Observable<ArrayList<MemberBean>> {
val cacheSet = cache.get(groupCode)
return if (cacheSet != null && cacheSet.isAll() && justByNet.not()) {
// 从缓存中获取。
Observable.just(cacheSet.getAll())
} else {
// 从网络获取。
val list = ArrayList<MemberBean>()
getMemberListFromNet(groupCode, 1, list)
.doOnNext {
val set = cacheSet ?: MemberSet()
set.putAll(it)
cache.put(groupCode, set)
}
}
}
//---------private method-----------//
/**
* 递归获取群成员。
*/
private fun getMemberListFromNet(groupCode: String, pageNo: Int, beanList: ArrayList<MemberBean>)
: Observable<ArrayList<MemberBean>> {
return repository
.postGroupMembers(groupCode, pageNo, pageSize)
.filter { it.status.falseRun { throw RuntimeException(it.message.orEmpty()) } }
.flatMap {
it.entry?.groupMemberList?.let { l -> beanList.addAll(l) }
return@flatMap if (it.entry?.hasNextPage == true) {
getMemberListFromNet(groupCode, pageNo + 1, beanList)
} else {
Observable.just(beanList)
}
}
}
完整代码:
object MemberHelper {
private const val pageSize: Int = 500
// 缓存15个群。
private const val maxCount: Int = 15
/**
* 缓存器接口。可自定义缓存策略。
*/
private interface IMemberCache {
fun put(groupCode: String, memberSet: IMemberSet)
fun get(groupCode: String): IMemberSet?
fun remove(groupCode: String)
fun clear()
}
/**
* 成员集合接口。
*/
interface IMemberSet {
/**
* 放入单个成员。
*/
fun put(member: MemberBean)
/**
* 获取成员。
*/
fun get(imAccount: String): MemberBean?
/**
* 放入全部成员。
*/
fun putAll(memberList: ArrayList<MemberBean>)
/**
* 获取全部成员。
*/
fun getAll(): ArrayList<MemberBean>
/**
* 大小。
*/
fun size(): Int
/**
* 是否全部。
*/
fun isAll(): Boolean
}
/**
* 默认用 LRU 缓存。
*/
private val cache: IMemberCache = LruMemberCache(maxCount)
private val repository by lazy {
MemberRepository(RetrofitHelper.instance.createApiService(MemberApi::class.java))
}
/**
* 移除一个群成员的缓存。
*/
fun remove(groupCode: String) {
cache.remove(groupCode)
}
/**
* 获取缓存里的集合。
*/
fun get(groupCode: String): IMemberSet? {
return cache.get(groupCode)
}
/**
* 获取 IMemberSet 如果没有,就存一个进去。
*/
fun getIfAbsent(groupCode: String): IMemberSet {
return cache.get(groupCode) ?: MemberSet().apply {
cache.put(groupCode, this)
}
}
/**
* 清空。
*/
fun clear() {
cache.clear()
}
/**
* 加载单个成员在这个群的信息。
* @param sessionId 会话。
* @param imAccount IM号。
*/
fun loadMember(sessionId: String, imAccount: String): Observable<HttpResponse<MemberBean>> {
return repository.getUserInfoBySession(sessionId, imAccount)
}
/**
* 加载群成员。
* @param groupCode 群号。
* @param justByNet 只通过网络获取。会把结果缓存。
*/
fun loadMemberList(groupCode: String, justByNet: Boolean = false): Observable<ArrayList<MemberBean>> {
val cacheSet = cache.get(groupCode)
return if (cacheSet != null && cacheSet.isAll() && justByNet.not()) {
// 从缓存中获取。
Observable.just(cacheSet.getAll())
} else {
// 从网络获取。
val list = ArrayList<MemberBean>()
getMemberListFromNet(groupCode, 1, list)
.doOnNext {
val set = cacheSet ?: MemberSet()
set.putAll(it)
cache.put(groupCode, set)
}
}
}
//---------private method-----------//
/**
* 递归获取群成员。
*/
private fun getMemberListFromNet(groupCode: String, pageNo: Int, beanList: ArrayList<MemberBean>)
: Observable<ArrayList<MemberBean>> {
return repository
.postGroupMembers(groupCode, pageNo, pageSize)
.filter { it.status.falseRun { throw RuntimeException(it.message.orEmpty()) } }
.flatMap {
it.entry?.groupMemberList?.let { l -> beanList.addAll(l) }
return@flatMap if (it.entry?.hasNextPage == true) {
getMemberListFromNet(groupCode, pageNo + 1, beanList)
} else {
Observable.just(beanList)
}
}
}
/**
* LRU缓存器。
*/
private class LruMemberCache(maxSize: Int) : IMemberCache {
private val lruCache = LruCache<String, IMemberSet>(maxSize)
override fun put(groupCode: String, memberSet: IMemberSet) {
lruCache.put(groupCode, memberSet)
}
override fun get(groupCode: String): IMemberSet? {
return lruCache.get(groupCode)
}
override fun remove(groupCode: String) {
lruCache.remove(groupCode)
}
override fun clear() {
lruCache.evictAll()
}
}
/**
* 成员集合。
*/
private class MemberSet : IMemberSet {
// 全部成员列表。
private val list = ArrayList<MemberBean>()
// 是否全部成员。
private var all: Boolean = false
// 临时存放的成员。信息不全的bean。
private val map = HashMap<String, MemberBean>()
override fun putAll(memberList: ArrayList<MemberBean>) {
list.clear()
list.addAll(memberList)
all = true
map.clear()
}
override fun getAll(): ArrayList<MemberBean> = list
override fun size(): Int = list.size + map.size
override fun isAll(): Boolean = all
override fun put(member: MemberBean) {
val have = all && list.find {
it.imAccount == member.imAccount
}?.apply {
nickName = member.nickName
groupRole = member.groupRole
avatar = member.avatar
} != null
if (!have) {
map[member.imAccount.orEmpty()] = member
}
}
override fun get(imAccount: String): MemberBean? {
return list.find { it.imAccount == imAccount } ?: map[imAccount]
}
}
}
注:lruCache 计算size,不能用成员个数(即list.size()
)来计算,因为一个群的成员在存入后中途是大小会发生变化,会导致lruCache内部维护的size计算小于0 ,trimToSize
方法 抛出IllegalStateException 异常。
if (size < 0 || (map.isEmpty() && size != 0)) {
throw new IllegalStateException(getClass().getName()
+ ".sizeOf() is reporting inconsistent results!");
}
使用
- 获取数据:loadMemberList()。可以指定强制从网络获取。
- 群成员新增时,一般是清空这个群的缓存数据。让下次重新获取,因为新增群成员时,并不能拿到这个成员在这个群的详情。
- 删除和修改直接修改缓存里的数据。
- 结合聊天页面单个消息发送者去刷新数据。(这个下一篇会讲)。
总结
- 设计好一个数据结构,能使开发实现更有条理。
- lruCache,计算size,对同一个元素,size应当不可变。
网友评论