IM中群消息發(fā)送者信息刷新方案

在IM項(xiàng)目(Android)中,聊天頁(yè)面幽崩,進(jìn)入會(huì)展示歷史消息遵蚜,而歷史消息存下來(lái)的發(fā)送者信息可能并不是最新的帖池,所以需要去刷新數(shù)據(jù)。單聊場(chǎng)景只需要刷新對(duì)方一個(gè)人信息谬晕,實(shí)現(xiàn)較為簡(jiǎn)單碘裕。但是到群聊,發(fā)送者眾多攒钳,不可能每次進(jìn)入頁(yè)面都去獲取全部成員的信息(數(shù)量大帮孔,獲取緩慢),所以需要制定策略去實(shí)現(xiàn)好的效果不撑。

需求分析

期望:

  1. 只去刷新顯示在屏幕上的發(fā)送者信息文兢。
  2. 每個(gè)發(fā)送者只需要刷新一次。(做個(gè)緩存)
  3. 屏幕滾動(dòng)很快焕檬,中途顯示的不去刷新姆坚。
  4. 如果其他地方緩存過了這個(gè)成員,就不再去獲取实愚。
  5. 群成員信息修改兼呵,及時(shí)刷新緩存數(shù)據(jù)。

方案設(shè)計(jì)

設(shè)計(jì):

  1. 在recycler的onBindVH里收集消息列表里的發(fā)送者的ID(imAccount)腊敲。
  2. 收集到數(shù)據(jù)池(只收集不是最新數(shù)據(jù)的击喂,防止反復(fù)收集),對(duì)imAccount去重碰辅,大小為10懂昂。利用LRU的緩存淘汰imAccount。
  3. 靜置0.5秒后開始將緩存池內(nèi)容發(fā)射請(qǐng)求没宾。(即屏幕停止了滑動(dòng)凌彬,或滑動(dòng)沒時(shí)新的item添加到屏幕)。
  4. 每個(gè)imAccount對(duì)應(yīng)一個(gè)鎖對(duì)象循衰,保證異步下同一個(gè)imAccount只會(huì)請(qǐng)求一次铲敛。
  5. 結(jié)合群成員信息做緩存。(群成員緩存獲取過了羹蚣,如進(jìn)過群成員頁(yè)等原探, 就不再去請(qǐng)求,直接使用緩存里的數(shù)據(jù))
  6. 刷新成功一個(gè)imAccount則會(huì)把整個(gè)列表里同一個(gè)發(fā)送者的信息都刷新掉。
  7. 數(shù)據(jù)刷新成功咽弦,回調(diào)刷新UI列表徒蟆。需要綁定聊天頁(yè)面生命周期。
  8. 收到群成員信息修改通知消息型型,修改緩存數(shù)據(jù)段审。

流程圖:

Sander流程圖.jpg

代碼實(shí)現(xiàn)

該部分功能需要結(jié)合成員緩存功能。請(qǐng)看:IM項(xiàng)目中群成員獲取與緩存策略

class SenderHelper private constructor() : DefaultLifecycleObserver {

    companion object {
        private const val CACHE_MAX_SIZE = 10
        private const val COUNT_DOWN_DELAY = 500L
        // 保證一對(duì)一的關(guān)系闹蒜。
        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 }
        }
    }

    // 回調(diào)的 liveData寺枉。
    private val liveData = MutableLiveData<List<String>>()
    // rx。
    private var compositeDisposable = CompositeDisposable()
    // 入?yún)⒕彺娉亍?    private val cache = LruCache<String, Unit>(CACHE_MAX_SIZE)
    // 結(jié)果列表绷落。
    private val resultList = CopyOnWriteArrayList<String>()
    // 鎖對(duì)象 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
    }

    /**
     * 獲取最新數(shù)據(jù)筐喳。
     */
    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()
    }

    /**
     * 主動(dòng)刷新名稱梳毙。
     */
    fun updateNickname(imAccount: String, nickname: String) {
        memberSet.get(imAccount)?.let {
            it.nickName = nickname
            changeListAndPost(it)
        }
    }

    /**
     * 主動(dòng)刷新身份。
     */
    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)
    }

    /**
     * 延時(shí)計(jì)時(shí)账锹。
     */
    private fun countDown() {
        handler.removeCallbacksAndMessages(null)
        handler.postDelayed(runnable, COUNT_DOWN_DELAY)
    }

    /**
     * 任務(wù)。
     */
    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)
                        }
                    }
                }
    }

    /**
     * 網(wǎng)絡(luò)操作。與工作同一個(gè)線程啤握。
     */
    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)
                }
    }

    /**
     *
     * 更新數(shù)據(jù)庫(kù)數(shù)據(jù)。
     */
    private fun updateDb(bean: MemberBean) {
        ...修改數(shù)據(jù)庫(kù)實(shí)現(xiàn)不重要...
    }

    /**
     * 刷洗數(shù)據(jù)及發(fā)送數(shù)據(jù)變化信號(hào)晶框。
     */
    private fun changeListAndPost(bean: MemberBean) {
        changeList(bean).trueRun { liveData.postValue(arrayListOf(bean.imAccount.orEmpty())) }
    }

    /**
     * 刷新列表數(shù)據(jù)排抬。
     */
    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())

    /**
     * 獲取鎖對(duì)象蹲蒲。
     */
    private fun getLock(imAccount: String): Lock {
        return lockMap[imAccount] ?: Lock().apply { lockMap[imAccount] = this }
    }

    /**
     * add 到復(fù)合體。
     */
    private fun Disposable.addToComposite() {
        compositeDisposable.add(this)
    }

}

使用:

初始化:

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

在recyclerView適配器的onBindVH處:

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

收到消息主動(dòng)刷新緩存:

// 更新名稱侵贵。
SenderHelper.get(sessionId)?.updateNickname(imAccount,nickName)
// 更新身份届搁。
SenderHelper.get(sessionId)?.updateGroupRole(imAccount,groupRole)                

總結(jié)

要點(diǎn):

  1. 收集最新進(jìn)入的 imAccount,最多10個(gè)。
  2. 靜置 0.5 秒卡睦,將收集的數(shù)據(jù)分別請(qǐng)求宴胧。
  3. 同一個(gè) imAccount 只能請(qǐng)求一次。
  4. 綁定生命周期表锻,一對(duì)一關(guān)系恕齐。
  5. 與群成員緩存結(jié)合。

PS:從這個(gè)方案中瞬逊,可以擴(kuò)展到列表內(nèi)容局部數(shù)據(jù)請(qǐng)求接口刷新的場(chǎng)景显歧。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個(gè)濱河市确镊,隨后出現(xiàn)的幾起案子士骤,更是在濱河造成了極大的恐慌,老刑警劉巖蕾域,帶你破解...
    沈念sama閱讀 216,544評(píng)論 6 501
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件拷肌,死亡現(xiàn)場(chǎng)離奇詭異,居然都是意外死亡束铭,警方通過查閱死者的電腦和手機(jī)廓块,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,430評(píng)論 3 392
  • 文/潘曉璐 我一進(jìn)店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái)契沫,“玉大人带猴,你說(shuō)我怎么就攤上這事⌒竿颍” “怎么了拴清?”我有些...
    開封第一講書人閱讀 162,764評(píng)論 0 353
  • 文/不壞的土叔 我叫張陵,是天一觀的道長(zhǎng)会通。 經(jīng)常有香客問我口予,道長(zhǎng),這世上最難降的妖魔是什么涕侈? 我笑而不...
    開封第一講書人閱讀 58,193評(píng)論 1 292
  • 正文 為了忘掉前任沪停,我火速辦了婚禮,結(jié)果婚禮上裳涛,老公的妹妹穿的比我還像新娘木张。我一直安慰自己,他們只是感情好端三,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,216評(píng)論 6 388
  • 文/花漫 我一把揭開白布舷礼。 她就那樣靜靜地躺著,像睡著了一般郊闯。 火紅的嫁衣襯著肌膚如雪妻献。 梳的紋絲不亂的頭發(fā)上蛛株,一...
    開封第一講書人閱讀 51,182評(píng)論 1 299
  • 那天,我揣著相機(jī)與錄音育拨,去河邊找鬼谨履。 笑死,一個(gè)胖子當(dāng)著我的面吹牛至朗,可吹牛的內(nèi)容都是我干的屉符。 我是一名探鬼主播,決...
    沈念sama閱讀 40,063評(píng)論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼锹引,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼矗钟!你這毒婦竟也來(lái)了?” 一聲冷哼從身側(cè)響起嫌变,我...
    開封第一講書人閱讀 38,917評(píng)論 0 274
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤吨艇,失蹤者是張志新(化名)和其女友劉穎,沒想到半個(gè)月后腾啥,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體东涡,經(jīng)...
    沈念sama閱讀 45,329評(píng)論 1 310
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,543評(píng)論 2 332
  • 正文 我和宋清朗相戀三年倘待,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了疮跑。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 39,722評(píng)論 1 348
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡凸舵,死狀恐怖祖娘,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情啊奄,我是刑警寧澤渐苏,帶...
    沈念sama閱讀 35,425評(píng)論 5 343
  • 正文 年R本政府宣布,位于F島的核電站菇夸,受9級(jí)特大地震影響琼富,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜庄新,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,019評(píng)論 3 326
  • 文/蒙蒙 一鞠眉、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧择诈,春花似錦凡蚜、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,671評(píng)論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)恶迈。三九已至涩金,卻和暖如春谱醇,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背步做。 一陣腳步聲響...
    開封第一講書人閱讀 32,825評(píng)論 1 269
  • 我被黑心中介騙來(lái)泰國(guó)打工副渴, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人全度。 一個(gè)月前我還...
    沈念sama閱讀 47,729評(píng)論 2 368
  • 正文 我出身青樓煮剧,卻偏偏與公主長(zhǎng)得像,于是被迫代替她去往敵國(guó)和親将鸵。 傳聞我的和親對(duì)象是個(gè)殘疾皇子勉盅,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 44,614評(píng)論 2 353