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:从这个方案中,可以扩展到列表内容局部数据请求接口刷新的场景。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 219,188评论 6 508
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 93,464评论 3 395
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 165,562评论 0 356
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 58,893评论 1 295
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 67,917评论 6 392
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 51,708评论 1 305
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 40,430评论 3 420
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 39,342评论 0 276
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 45,801评论 1 317
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,976评论 3 337
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 40,115评论 1 351
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,804评论 5 346
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 41,458评论 3 331
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 32,008评论 0 22
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 33,135评论 1 272
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 48,365评论 3 373
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 45,055评论 2 355