前言

在IM项目(Android)中,我们需要获取群成员,往往数据较大,获取缓慢,所以需要做个缓存,提升用户体验。

策略

  1. 如果本地(缓存里)有完整数据,则取缓存里数据,否则从网路获取。(可能多个地方需要加载成员列表,但无法保证那处先加载。比如查看群成员,选择AT群内某人,管理员列表等等。)
  2. 从网络获取全部成员时,分页获取。采用递归请求,直到没有下一页。
  3. 最多缓存15个群的成员,如果超过15个,淘汰掉最近最少使用的那个群的成员。
  4. 单个成员信息获取,会存在临时成员里。(用途是,在群聊天页,实时刷新调天发送者信息。)如果这个群已经有完成的群成员列表,则删除临时成员。(聊天页发送者信息直接从完整的群成员表里取出对比)。
  5. 支持修改缓存数据。包括增加,删除,修改成员。

方案设计

缓存:
定义成员集合接口:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
/**
* 成员集合接口。
*/
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
}

定义缓存器接口:

1
2
3
4
5
6
7
8
9
/**
* 缓存器接口。可自定义缓存策略。
*/
private interface IMemberCache {
fun put(groupCode: String, memberSet: IMemberSet)
fun get(groupCode: String): IMemberSet?
fun remove(groupCode: String)
fun clear()
}

获取:
先判断缓存,再递归获取全部群成员。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
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)
}
}
}

完整代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
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 异常。

1
2
3
4
if (size < 0 || (map.isEmpty() && size != 0)) {
throw new IllegalStateException(getClass().getName()
+ ".sizeOf() is reporting inconsistent results!");
}

使用

  1. 获取数据:loadMemberList()。可以指定强制从网络获取。
  2. 群成员新增时,一般是清空这个群的缓存数据。让下次重新获取,因为新增群成员时,并不能拿到这个成员在这个群的详情。
  3. 删除和修改直接修改缓存里的数据。
  4. 结合聊天页面单个消息发送者去刷新数据。(这个下一篇会讲)。

总结

  1. 设计好一个数据结构,能使开发实现更有条理。
  2. lruCache,计算size,对同一个元素,size应当不可变。

配一张图