IM中群消息发送者信息刷新方案

在IM项目(Android)中,聊天页面,进入会展示历史消息,而历史消息存下来的发送者信息可能并不是最新的,所以需要去刷新数据。单聊场景只需要刷新对方一个人信息,实现较为简单。但是到群聊,发送者众多,不可能每次进入页面都去获取全部成员的信息(数量大,获取缓慢),所以需要制定策略去实现好的效果。

需求分析

期望:

  1. 只去刷新显示在屏幕上的发送者信息。
  2. 每个发送者只需要刷新一次。(做个缓存)
  3. 屏幕滚动很快,中途显示的不去刷新。
  4. 如果其他地方缓存过了这个成员,就不再去获取。
  5. 群成员信息修改,及时刷新缓存数据。

方案设计

设计:

  1. 在recycler的onBindVH里收集消息列表里的发送者的ID(imAccount)。
  2. 收集到数据池(只收集不是最新数据的,防止反复收集),对imAccount去重,大小为10。利用LRU的缓存淘汰imAccount。
  3. 静置0.5秒后开始将缓存池内容发射请求。(即屏幕停止了滑动,或滑动没时新的item添加到屏幕)。
  4. 每个imAccount对应一个锁对象,保证异步下同一个imAccount只会请求一次。
  5. 结合群成员信息做缓存。(群成员缓存获取过了,如进过群成员页等, 就不再去请求,直接使用缓存里的数据)
  6. 刷新成功一个imAccount则会把整个列表里同一个发送者的信息都刷新掉。
  7. 数据刷新成功,回调刷新UI列表。需要绑定聊天页面生命周期。
  8. 收到群成员信息修改通知消息,修改缓存数据。

流程图:

Sander流程图.jpg

代码实现

该部分功能需要结合成员缓存功能。请看:IM项目中群成员获取与缓存策略

class SenderHelper private constructor() : DefaultLifecycleObserver {

    companion object {
        private const val CACHE_MAX_SIZE = 10
        private const val COUNT_DOWN_DELAY = 500L
        // 保证一对一的关系。
        private val map = WeakHashMap<LifecycleOwner, SenderHelper>()

        fun with(owner: LifecycleOwner, observer: Observer<List<String>>): SenderHelper {
            return map[owner] ?: SenderHelper().apply {
                map[owner] = this
                with(owner, observer)
            }
        }

        fun get(owner: LifecycleOwner): SenderHelper? {
            return map[owner]
        }

        fun get(sessionId: String): SenderHelper? {
            return map.values.find { it.sessionId == sessionId }
        }
    }

    // 回调的 liveData。
    private val liveData = MutableLiveData<List<String>>()
    // rx。
    private var compositeDisposable = CompositeDisposable()
    // 入参缓存池。
    private val cache = LruCache<String, Unit>(CACHE_MAX_SIZE)
    // 结果列表。
    private val resultList = CopyOnWriteArrayList<String>()
    // 锁对象 map。
    private val lockMap = ConcurrentHashMap<String, Lock>()
    // data。
    private var groupCode: String = ""
    private var sessionId: String = ""
    private lateinit var dataList: (Unit) -> List<SenderModel>
    private val memberSet by lazy {
        MemberHelper.getIfAbsent(groupCode)
    }

    private val handler = Handler()
    private val runnable = Runnable {
        cache.snapshot().keys.apply {
            forEach { k -> cache.remove(k) }
            task(this.toList())
        }
    }

    /**
     * 初始化。
     */
    fun init(sessionId: String, groupCode: String, dataList: (Unit) -> List<SenderModel>) {
        this.sessionId = sessionId
        this.groupCode = groupCode
        this.dataList = dataList
    }

    /**
     * 获取最新数据。
     */
    fun bind(sender: SenderModel) {
        // 如果是自己,直接返回。
        if (sender.isSelf || sender.imAccount.isEmpty()) return
        // 如果最新,直接返回。
        memberSet.get(sender.imAccount)?.let {
            if (compare(sender, it).falseRun { changeListAndPost(it) }) return
        }
        // 存入缓存池。
        cache.get(sender.imAccount) ?: cache.put(sender.imAccount, Unit)
        countDown()
    }

    /**
     * 主动刷新名称。
     */
    fun updateNickname(imAccount: String, nickname: String) {
        memberSet.get(imAccount)?.let {
            it.nickName = nickname
            changeListAndPost(it)
        }
    }

    /**
     * 主动刷新身份。
     */
    fun updateGroupRole(imAccount: String, groupRole: Int) {
        memberSet.get(imAccount)?.let {
            it.groupRole = groupRole
            changeListAndPost(it)
        }
    }

    override fun onDestroy(owner: LifecycleOwner) {
        compositeDisposable.clear()
        handler.removeCallbacksAndMessages(null)
        map.remove(owner)
    }

    //---------private method-----------//

    /**
     * 绑定生命周期和观察。
     */
    private fun with(owner: LifecycleOwner, observer: Observer<List<String>>) {
        owner.lifecycle.addObserver(this)
        liveData.observe(owner, observer)
    }

    /**
     * 延时计时。
     */
    private fun countDown() {
        handler.removeCallbacksAndMessages(null)
        handler.postDelayed(runnable, COUNT_DOWN_DELAY)
    }

    /**
     * 任务。
     */
    private fun task(imAccountList: List<String>) {
        Observable
                .fromIterable(imAccountList)
                .flatMap { work(it) }
                .doFinally {
                    if (resultList.isNotEmpty()) {
                        liveData.postValue(ArrayList(resultList))
                        resultList.clear()
                    }
                }
                .subscribe({}, {})
                .addToComposite()
    }

    /**
     * 工作。各自开辟子线程。
     */
    private fun work(imAccount: String): Observable<*> {
        return Observable.just(imAccount)
                .subscribeOn(Schedulers.io())
                .flatMap {
                    synchronized(getLock(it).lock) {
                        if (memberSet.get(it) == null) {
                            netWork(it)
                        } else {
                            Observable.just(it)
                        }
                    }
                }
    }

    /**
     * 网络操作。与工作同一个线程。
     */
    private fun netWork(imAccount: String): Observable<*> {
        return MemberHelper
                .loadMember(sessionId, imAccount)
                .filter { it.status && it.entry != null }
                .map { it.entry!! }
                .doOnNext {
                    resultList.add(it.imAccount.orEmpty())
                    memberSet.put(it)
                    updateDb(it)
                    changeList(it)
                }
    }

    /**
     *
     * 更新数据库数据。
     */
    private fun updateDb(bean: MemberBean) {
        ...修改数据库实现不重要...
    }

    /**
     * 刷洗数据及发送数据变化信号。
     */
    private fun changeListAndPost(bean: MemberBean) {
        changeList(bean).trueRun { liveData.postValue(arrayListOf(bean.imAccount.orEmpty())) }
    }

    /**
     * 刷新列表数据。
     */
    private fun changeList(bean: MemberBean): Boolean {
        val isChange: Boolean
        dataList()
                .filter { it.imAccount == bean.imAccount && compare(it, bean).not() }
                .apply { isChange = this.isNotEmpty() }
                .forEach {
                    it.nickName = bean.nickName.orEmpty()
                    it.avatar = bean.avatar?.toLoadUrl().orEmpty()
                    it.setGroupRole(bean.groupRole)
                }
        return isChange
    }

    /**
     * 比较是否最新了。
     */
    private fun compare(sender: SenderModel, bean: MemberBean): Boolean {
        return (bean.groupRole == sender.groupRole
                && bean.nickName == sender.nickName
                && bean.avatar?.toLoadUrl() == sender.avatar)
    }

    /**
     * 锁。
     */
    class Lock(val lock: Any = Any())

    /**
     * 获取锁对象。
     */
    private fun getLock(imAccount: String): Lock {
        return lockMap[imAccount] ?: Lock().apply { lockMap[imAccount] = this }
    }

    /**
     * add 到复合体。
     */
    private fun Disposable.addToComposite() {
        compositeDisposable.add(this)
    }

}

使用:

初始化:

SenderHelper
                .with(lifecyclerOwner, Observer { updateList() })
                .init(sessionId, groupCode) { getSenderList() }

在recyclerView适配器的onBindVH处:

 SenderHelper.get(lifecyclerOwner)?.bind(sender)

收到消息主动刷新缓存:

// 更新名称。
SenderHelper.get(sessionId)?.updateNickname(imAccount,nickName)
// 更新身份。
SenderHelper.get(sessionId)?.updateGroupRole(imAccount,groupRole)                

总结

要点:

  1. 收集最新进入的 imAccount,最多10个。
  2. 静置 0.5 秒,将收集的数据分别请求。
  3. 同一个 imAccount 只能请求一次。
  4. 绑定生命周期,一对一关系。
  5. 与群成员缓存结合。

PS:从这个方案中,可以扩展到列表内容局部数据请求接口刷新的场景。

推荐阅读更多精彩内容