學問Chat UI(2)

前言

  • 上文講了下要去作哪些事,重點分析了融雲Sdk中RongExtension這個擴展控件,本文來學習下一樣是融雲Sdk中的AutoRefreshListView如何適配多種消息的實現方式,寫的有不足之處還望指出。

AutoRefreshListView如何適配多種消息

本文不分析AutoRefreshListView內部源碼,從數據適配角度分析如何適配上文講到的多種聊天消息
既然從AutoRefreshListView開始,那先來了解下通常使用ListView的步驟:html

  • 佈局器尋找ListView控件,經過findViewById方法
  • 建立數據適配器
  • ListView設置數據適配器與經常使用事件
  • 新增數據到適配器並更新UI
    可是數據更新到UI,會遇到多種不一樣數據結構(多種消息類型),那麼能不能找到一種簡潔的方法,讓不一樣消息交給不一樣的消息處理者,以此來達到解耦的目的。那他是如何作到的?
    這才引出本文分析的重點:MessageListAdapter;

MessageListAdapter概述

  • MessageListAdapter繼承自BaseAdapter<UIMessage>BaseAdapter<T>泛型類重點分析下getView(int position, View convertView, ViewGroup parent)方法;
  • 其中兩個抽象方法newView與bindView,看名字有點頭緒是幹嗎的,newView是建立新的View,bindView是綁定數據到View;
  • 怎麼使用上面的抽象方法?判斷下convertView對象,若是爲空,調用newView方法,不然,賦值給臨時變量view,最後把數據綁定到view上,並返回view對象。
@Override
    public View getView(int position, View convertView, ViewGroup parent) {
        View view;
        if (convertView != null) {
            view = convertView;
        } else {
            view = this.newView(mContext, position, parent);
        }
        this.bindView(view, position, this.getItem(position));
        return view;
    }

 protected abstract View newView(Context context, int pos, ViewGroup parent);

 protected abstract void bindView(View convertView, int pos, T t);

MessageListAdapter的bindView方法

  • 繼承自抽象類BaseAdapter須要實現兩個方法newView與bindView;newView使用ViewHolder進行控件建立;
  • bindView消息數據與消息佈局綁定經過了下面代碼來實現的;這段代碼中涉及到provider與contentView對象,其中provider對象實現了接口IContainerItemProvider,而contentView對象是ProviderContainerView的實例,下面詳解這兩個類。
final View view = holder.contentView.inflate((IContainerItemProvider)provider);
   ((IContainerItemProvider)provider).bindView(view, position, data);
1.IContainerItemProvider接口與及其子類
  • 下面畫了相關類的UML類圖,省略了部分子類;

1.1如何獲取provider對象
  • 貼上獲取provider代碼,講下基本的思路:
  • 1.判斷消息是不是評論消息,若是不是,則根據消息類型獲取對應消息類型的provider;
  • 2.若是provider爲null,則匹配爲未知消息類型
  • 3.若是provider仍是爲空,則返回;不然,返回provider對象;
if(data != null) {
            final MessageListAdapter.ViewHolder holder = (MessageListAdapter.ViewHolder)v.getTag();
            if(holder == null) {
                RLog.e("MessageListAdapter", "view holder is null !");
            } else {
                Object provider;//聲明provider對象
                ProviderTag tag;
                //判斷是不是評論消息
                if(this.getNeedEvaluate(data)) {
                    provider = RongContext.getInstance().getEvaluateProvider();
                    tag = RongContext.getInstance().getMessageProviderTag(data.getContent().getClass());
                } else {
                    if(RongContext.getInstance() == null || data == null || data.getContent() == null) {
                        RLog.e("MessageListAdapter", "Message is null !");
                        return;
                    }

                    provider = RongContext.getInstance().getMessageTemplate(data.getContent().getClass());
                    if(provider == null) {
                        provider = RongContext.getInstance().getMessageTemplate(UnknownMessage.class);
                        tag = RongContext.getInstance().getMessageProviderTag(UnknownMessage.class);
                    } else {
                        tag = RongContext.getInstance().getMessageProviderTag(data.getContent().getClass());
                    }

                    if(provider == null) {
                        RLog.e("MessageListAdapter", data.getObjectName() + " message provider not found !");
                        return;
                    }
                }
  • 仍是沒看到是如何獲取provider對象的,彆着急,下面讓咱們看看getMessageTemplate方法,看是如何經過消息對象獲取對應的消息provider*
  • 看第一行代碼發現:是經過mWeakTemplateMap獲取到provider,查看聲明發現是HashMap類型(弱引用),這個hashmap對象的數據是怎麼來的?
  • 看下面 (MessageProvider)((MessageProvider)this.mTemplateMap.get(type)).clone();這段代碼發現與上面有什麼不一樣的地方;一個是HashMap對象換成了mTemplateMap,另外一個是調用了clone(因爲實現了cloneable接口);
  • this.mWeakTemplateMap.put(type, provider);則是把clone的對象放到mWeakTemplateMap對象中,也解釋前面講的mWeakTemplateMap對象的數據是怎麼來的。

Why?爲何須要兩個HashMap對象以及clone方法調用的緣由。
下面一步一步分析看,首先mTemplateMap對象數據哪裏來的?這個是在融雲創建鏈接成功回調之後添加的消息模板;算法

  • 1.反着來看若是不復制(調用clone())的話,provider 對象是給外部使用;那麼退出聊天界面這個對象將被銷燬的,當再次進入聊天界面後mTemplateMap對象存放的MessageProvider爲空了,因此顯然mTemplateMap註冊的MessageProvider是創建鏈接後長期的;
    這樣的話,無論複製的MessageProvider怎麼操做,母體都不會影響。
  • 2.在聊天界面有可能發了多條重複或者類型相同的消息,那麼是否是能夠避免重複複製,畢竟複製須要時間與空間代價,因此能夠重複使用那些還未被銷燬的MessageProvider(弱引用對象),這樣能夠重複使用又不會出現潛在的內存泄漏。
  • 到此對於如何獲取provider對象的實現有了個具體的瞭解。
public MessageProvider getMessageTemplate(Class<? extends MessageContent> type) {
        MessageProvider provider = (MessageProvider)this.mWeakTemplateMap.get(type);
        if(provider == null) {
            try {
                if(this.mTemplateMap != null && this.mTemplateMap.get(type) != null) {
                    provider = (MessageProvider)((MessageProvider)this.mTemplateMap.get(type)).clone();
                    this.mWeakTemplateMap.put(type, provider);
                } else {
                    RLog.e("RongContext", "The template of message can\'t be null. type :" + type);
                }
            } catch (CloneNotSupportedException var4) {
                var4.printStackTrace();
            }
        }

        return provider;
    }
2.contentView對象--自定義ProviderContainerView
  • contentView是屬於ProviderContainerView,ProviderContainerView佈局繼承自FrameLayout
  • 自定義佈局控件提供了一個重要的方法public <T extends IContainerItemProvider> View inflate(T t)與兩個HashMap:mViewCounterMap--記錄使用頻率與mContentViewMap--緩存控件
  • 1.其餘代碼先不看,先來分析下最後一個判斷result==null的代碼,整理思路是newView將獲得view添加到ProviderContainerView當前容器中並返回view,固然這裏一樣作了緩存。
public <T extends IContainerItemProvider> View inflate(T t) {
        View result = null;
        if(this.mInflateView != null) {
            this.mInflateView.setVisibility(GONE);
        }

        if(this.mContentViewMap.containsKey(t.getClass())) {
            result = (View)this.mContentViewMap.get(t.getClass());
            this.mInflateView = result;
            ((AtomicInteger)this.mViewCounterMap.get(t.getClass())).incrementAndGet();
        }

        if(result != null) {
            if(result.getVisibility() == GONE) {
                result.setVisibility(VISIBLE);
            }

            return result;
        } else {
            this.recycle();
            result = t.newView(this.getContext(), this);
            if(result != null) {
                super.addView(result);
                this.mContentViewMap.put(t.getClass(), result);
                this.mViewCounterMap.put(t.getClass(), new AtomicInteger());
            }

            this.mInflateView = result;
            return result;
        }
    }
  • 2.那麼問題來了:recycle方法是用來幹啥的?緩存View是如何實現的?
  • 先來看下recycle方法,其中容易發現mMaxContainSize=3是限定條件,超過或者相等則會進行移除控件,那若是移除的話應該怎麼移除是最優的?
  • 這裏用到了最近最少使用算法,也就是若是這個控件好久沒使用了那麼下次用到可能性相對來講比較小,那麼超過限定條件mMaxContainSize>=3後,應該先刪除這個控件。
  • 那他是如何作到不一樣控件的使用頻率的呢?這要回到inflate方法,根據傳入的消息處理者類型,若是mContentViewMap中存在了對應控件,mViewCounterMap找到對應鍵並自動+1(這裏的鍵的類型是AtomicInteger,自增或者自是減線程安全的),那數值大的表明最近剛剛使用過;若是mContentViewMap不存在的話,則把消息處理器添加到mContentViewMap與mViewCounterMap兩個HashMap中,起到緩存做用。經過以上兩步,使緩存效率獲得優化。
private void recycle() {
        if(this.mInflateView != null) {
            int count = this.getChildCount();
            if(count >= this.mMaxContainSize) {
                Map.Entry min = null;

                Map.Entry item;
                for(Iterator view = this.mViewCounterMap.entrySet().iterator(); view.hasNext(); min = ((AtomicInteger)min.getValue()).get() > ((AtomicInteger)item.getValue()).get()?item:min) {
                    item = (Map.Entry)view.next();
                    if(min == null) {
                        min = item;
                    }
                }

                this.mViewCounterMap.remove(min.getKey());
                View view1 = (View)this.mContentViewMap.remove(min.getKey());
                this.removeView(view1);
            }

        }
    }
  • 先講到這裏,後續會分析插件功能。。。
相關文章
相關標籤/搜索