看完感受我 RecyclerView 白學了!

一、前言

今天要說的那個東西其實你們都很是熟悉,那就是RecyclerView,沒錯你們都會用,但不知道對於RecyclerView的一些優化有多少人專門去研究過,不知道是否是一些開發者還只是停留在只會調用setadapter,而後配合notifyDataSetChanged這種萬金油的方式上,又或者說是使用了一些優秀的三方庫可是確只是簡單停留在調用上就完事。java

其實RecyclerView作爲android開發一個很是經常使用的控件,能夠這麼說,通常普通的ui頁面均可以經過RecyclerView去實現,我的以爲RecyclerView能夠徹底去替換掉scrollview,這裏說的普通的ui頁面特指那些沒有酷炫交互方式的頁面。android

深刻理解RecyclerView優化方面的技術對於發揮RecyclerView的性能是很是有幫助的。面試

寫這篇文章的原因仍是以前項目在使用同事封裝的adapter庫時,bugly上報崩潰,在解決問題的過程當中有機會深刻理解RecyclerView的部分源碼,結合網上一些文章,本身總結出來的心得體會,有興趣的能夠去看看我原先的那篇文章bugly關於RecyclerView崩潰問題研究算法

借用一句如今流行的網絡用語就是,RecyclerView不止眼前的setadapter和notify,還有詩和遠方。閒話扯到這,接下來就來看一下RecyclerView優化方面的東西。api

關於RecyclerView的優化,本身會將它們分爲兩大類,一類是RecyclerView自帶的系統優化,另外一類就是咱們經過代碼實現的手動優化,先來介紹下RecyclerView自帶的系統優化。系統優化咱們不能作太多的干預,可是經過理解RecyclerView的系統優化可以讓咱們更好的理解RecyclerView的工做機制。緩存

二、預取功能(Prefetch)

這個功能是rv在版本25以後自帶的,也就是說只要你使用了25或者以後版本的rv,那麼就自帶該功能,而且默認就是處理開啓的狀態,經過LinearLayoutManager的setInitialItemPrefetchCount()咱們能夠手動控制該功能的開啓關閉,可是通常狀況下不必也不推薦關閉該功能,預取功能的原理比較好理解,如圖所示性能優化

咱們都知道android是經過每16ms刷新一次頁面來保證ui的流暢程度,如今android系統中刷新ui會經過cpu產生數據,而後交給gpu渲染的形式來完成,從上圖能夠看出當cpu完成數據處理交給gpu後就一直處於空閒狀態,須要等待下一幀纔會進行數據處理.網絡

而這空閒時間就被白白浪費了,如何才能壓榨cpu的性能,讓它一直處於忙碌狀態,這就是rv的預取功能(Prefetch)要作的事情,rv會預取接下來可能要顯示的item,在下一幀到來以前提早處理完數據,而後將獲得的itemholder緩存起來,等到真正要使用的時候直接從緩存取出來便可。架構

預取代碼理解

雖然說預取是默認開啓不須要咱們開發者操心的事情,可是明白原理仍是能加深該功能的理解。下面就說下本身在看預取源碼時的一點理解。實現預取功能的一個關鍵類就是gapworker,能夠直接在rv源碼中找到該類app

GapWorker mGapWorker;

rv經過在ontouchevent中觸發預取的判斷邏輯,在手指執行move操做的代碼末尾有這麼段代碼

case MotionEvent.ACTION_MOVE: {
   ......
        if (mGapWorker != null && (dx != 0 || dy != 0)) {
            mGapWorker.postFromTraversal(this, dx, dy);
        }
    }
} break;

經過每次move操做來判斷是否預取下一個可能要顯示的item數據,判斷的依據就是經過傳入的dx和dy獲得手指接下來可能要移動的方向,若是dx或者dy的偏移量會致使下一個item要被顯示出來則預取出來,可是並非說預取下一個可能要顯示的item必定都是成功的.

其實每次rv取出要顯示的一個item本質上就是取出一個viewholder,根據viewholder上關聯的itemview來展現這個item。而取出viewholder最核心的方法就是

tryGetViewHolderForPositionByDeadline(int position,boolean dryRun, long deadlineNs)

名字是否是有點長,在rv源碼中你會時不時見到這種巨長的方法名,看方法的參數也能找到和預取有關的信息,deadlineNs的通常取值有兩種,一種是爲了兼容版本25以前沒有預取機制的狀況,兼容25以前的參數爲:

static final long FOREVER_NS = Long.MAX_VALUE;

另外一種就是實際的deadline數值,超過這個deadline則表示預取失敗,這個其實也好理解,預取機制的主要目的就是提升rv總體滑動的流暢性,若是要預取的viewholder會形成下一幀顯示卡頓強行預取的話那就有點本末倒置了。

關於預取成功的條件經過調用

boolean willCreateInTime(int viewType, long approxCurrentNs, long deadlineNs) {
    long expectedDurationNs = getScrapDataForType(viewType).mCreateRunningAverageNs;
    return expectedDurationNs == 0 || (approxCurrentNs + expectedDurationNs < deadlineNs);
}

來進行判斷,approxCurrentNs的值爲

long start = getNanoTime();
if (deadlineNs != FOREVER_NS
            && !mRecyclerPool.willCreateInTime(type, start, deadlineNs)) {
     // abort - we have a deadline we can't meet
    return null;
}

而mCreateRunningAverageNs就是建立同type的holder的平均時間,感興趣的能夠去看下這個值如何獲得,不難理解就不貼代碼了。關於預取就說到這裏,感興趣的能夠本身去看下其他代碼的實現方式,能夠說google對於rv仍是至關重視的,煞費苦心提升rv的各類性能,聽說最近推出的viewpager2控件就是經過rv來實現的,大有rv控件一統天下的感受。

三、四級緩存

rv設計中另外一個提升滑動流暢性的東西就是這個四級緩存了,若是說預取是25版本外來的務工人員,那麼這個四級緩存就是一個本地土著了,自rv出現以來就一直存在,相比較listview的2級緩存機制,rv的四級看起來是否是顯得更加的高大上。借用一張示意圖來看下rv的四級緩存

rv中經過recycler來管理緩存機制,關於如何使用緩存能夠在tryGetViewHolderForPositionByDeadline找到,沒錯又是這個方法,看來名字起的長存在感也會比較足。

tryGetViewHolderForPositionByDeadline依次會從各級緩存中去取viewholer,若是取到直接丟給rv來展現,若是取不到最終纔會執行咱們很是熟悉的oncreatviewholder和onbindview方法,一句話就把tryGetViewHolderForPositionByDeadline的功能給講明白了,內部實現無非是如何從四級緩存中去取確定有個優先級的順序。

能夠先來看下recycler中關於這四級緩存的代碼部分:

public final class Recycler {
        final ArrayList<ViewHolder> mAttachedScrap = new ArrayList<>();
        ArrayList<ViewHolder> mChangedScrap = null;

        final ArrayList<ViewHolder> mCachedViews = new ArrayList<ViewHolder>();

        private ViewCacheExtension mViewCacheExtension;

        RecycledViewPool mRecyclerPool;
}

四級緩存的真面目能夠在這看到,其中兩個scrap就是第一級緩存,是recycler在獲取viewholder時最早考慮的緩存,接下來的mCachedViews,mViewCacheExtension,mRecyclerPool分別對應2,3,4級緩存。

各級緩存做用

scrap:

rv之因此要將緩存分紅這麼多塊確定在功能上是有必定的區分的,它們分別對應不一樣的使用場景,scrap是用來保存被rv移除掉但最近又立刻要使用的緩存,好比說rv中自帶item的動畫效果。

本質上就是計算item的偏移量而後執行屬性動畫的過程,這中間可能就涉及到須要將動畫以前的item保存下位置信息,動畫後的item再保存下位置信息,而後利用這些位置數據生成相應的屬性動畫。如何保存這些viewholer呢,就須要使用到scrap了,由於這些viewholer數據上是沒有改變的,只是位置改變而已,因此放置到scrap最爲合適。

稍微仔細看的話就能發現scrap緩存有兩個成員mChangedScrap和mAttachedScrap,它們保存的對象有些不同,通常調用adapter的notifyItemRangeChanged被移除的viewholder會保存到mChangedScrap,其他的notify系列方法(不包括notifyDataSetChanged)移除的viewholder會被保存到mAttachedScrap中。

cached:

也是rv中很是重要的一個緩存,就linearlayoutmanager來講cached緩存默認大小爲2,它的容量很是小,所起到的做用就是rv滑動時剛被移出屏幕的viewholer的收容所。

由於rv會認爲剛被移出屏幕的viewholder可能接下來立刻就會使用到,因此不會當即設置爲無效viewholer,會將它們保存到cached中,但又不能將全部移除屏幕的viewholder都視爲有效viewholer,因此它的默認容量只有2個,固然咱們能夠經過:

public void setViewCacheSize(int viewCount) {
    mRequestedCacheMax = viewCount;
    updateViewCacheSize();
}

來改變這個容量大小,這個就看實際應用場景了。

extension:

第三級緩存,這是一個自定義的緩存,沒錯rv是能夠自定義緩存行爲的,在這裏你能夠決定緩存的保存邏輯,可是這麼個自定義緩存通常都沒有見過具體的使用場景,並且自定義緩存須要你對rv中的源碼很是熟悉才行,不然在rv執行item動畫,或者執行notify的一系列方法後你的自定義緩存是否還能有效就是一個值得考慮的問題。

因此通常不太推薦使用該緩存,更多的我以爲這多是google自已留着方便擴展來使用的,目前來講這還只是個空實現而已,從這點來看其實rv所說的四級緩存本質上還只是三級緩存。

pool:

又一個重要的緩存,這也是惟一一個咱們開發者能夠方便設置的一個(雖然extension也能設置,可是難度大),並且設置方式很是簡單,new一個pool傳進去就能夠了,其餘的都不用咱們來處理,google已經給咱們料理好後事了,這個緩存保存的對象就是那些無效的viewholer,雖然說無效的viewholer上的數據是無效的,可是它的rootview仍是能夠拿來使用的,這也是爲何最先的listview有一個convertView參數的緣由,固然這種機制也被rv很好的繼承了下來。

pool通常會和cached配合使用,這麼來講,cached存不下的會被保存到pool中畢竟cached默認容量大小隻有2,可是pool容量也是有限的當保存滿以後再有viewholder到來的話就只能會無情拋棄掉,它也有一個默認的容量大小

private static final int DEFAULT_MAX_SCRAP = 5;
int mMaxScrap = DEFAULT_MAX_SCRAP;

這個大小也是能夠經過調用方法來改變,具體看應用場景,通常來講正常使用的話使用默認大小便可。

以上就是rv的四級緩存介紹,rv在設計之初就考慮到了這些問題,固然裏面的一些細節仍是比較多的,這個就須要感興趣的本身去研究了,也正是由於google給咱們考慮到這麼多的優化這些纔會顯得rv的源碼有些龐大,光一個rv差很少就1萬3千多行,這還不包括layoutmanager的實現代碼,這也是爲何不少人在遇到rv崩潰問題的時候會比較抓狂,根本緣由仍是在於沒能好好研究過一些相關源碼。

四、咱們能夠作的

上面都在說rv中自帶的一些優化技術,雖然google爸爸想方設法給咱們提供好了不少能夠給rv使用的優化api,可是這也架不住不少人不會使用啊,飯都到你嘴邊了你本身都不會張嘴那就沒人能幫你了,因此接下來就能夠來講說咱們在代碼能夠作哪些事情來充分發揮rv的性能。

下降item的佈局層次

其實這個優化不光適用於rv,activity的佈局優化也一樣適用,下降頁面層次能夠必定程度下降cpu渲染數據的時間成本,反應到rv中就是下降mCreateRunningAverageNs的時間,不光目前顯示的頁面能加快速度,預取的成功率也能提升,關於如何下降佈局層次仍是要推薦下google的強大控件ConstraintLayout,具體使用就自行百度吧。

比較容易上手,這裏吐槽下另外一個控件CoordinatorLayout的上手難度確實是有點大啊,不瞭解CoordinatorLayout源碼可能會遇到一些奇葩問題。下降item的佈局層次能夠說是rv優化中一個對於rv源碼不須要了解也能徹底掌握的有效方式。

去除冗餘的setitemclick事件

rv和listview一個比較大的不一樣之處在於rv竟然沒有提供setitemclicklistener方法,這是當初本身在使用rv時一個很是不理解的地方,其實如今也不是太理解,可是好在咱們能夠很方便的實現該功能。

一種最簡單的方式就是直接在onbindview方法中設置,這實際上是一種不太可取的方式,onbindview在item進入屏幕的時候都會被調用到(cached緩存着的除外),而通常狀況下都會建立一個匿名內部類來實現setitemclick,這就會致使在rv快速滑動時建立不少對象,從這點考慮的話setitemclick應該放置到其餘地方更爲合適。

本身的作法就是將setitemclick事件的綁定和viewholder對應的rootview進行綁定,viewholer因爲緩存機制的存在它建立的個數是必定的,因此和它綁定的setitemclick對象也是必定的。

還有另外一種作法能夠經過rv自帶的addOnItemTouchListener來實現點擊事件,原理就是rv在觸摸事件中會使用到addOnItemTouchListener中設置的對象,而後配合GestureDetectorCompat實現點擊item,示例代碼以下:

recyclerView.addOnItemTouchListener(this);
gestureDetectorCompat = new GestureDetectorCompat(recyclerView.getContext(), new SingleClick());

@Override
public boolean onInterceptTouchEvent(RecyclerView rv, MotionEvent e) {
    if (gestureDetectorCompat != null) {
        gestureDetectorCompat.onTouchEvent(e);
    }
    return false;
}

private class SingleClick extends GestureDetector.SimpleOnGestureListener {

    @Override
    public boolean onSingleTapConfirmed(MotionEvent e) {
        View view = recyclerView.findChildViewUnder(e.getX(), e.getY());
        if (view == null) {
            return false;
        }
        final RecyclerView.ViewHolder viewHolder = recyclerView.getChildViewHolder(view);
        if (!(viewHolder instanceof ViewHolderForRecyclerView)) {
            return false;
        }
        final int position = getAdjustPosition(viewHolder);
        if (position == invalidPosition()) {
            return false;
        }
        /****************/
        點擊事件設置能夠考慮放在這裏
        /****************/
        return true;
    }
}

相對來講這是一個比較優雅點的實現,可是有一點侷限在於這種點擊只能設置整個item的點擊,若是item內部有兩個textview都須要實現點擊的話就可能不太適用了,因此具體使用哪一種看你們的實際應用場景,能夠考慮將這兩種方式都封裝到adapter庫中,目前項目中使用的adapter庫就是採用兩種結合的形式。

複用pool緩存

四級緩存中我已經介紹過了,複用自己並不難,調用rv的setRecycledViewPool方法設置一個pool進去就能夠,可是並非說每次使用rv場景的狀況下都須要設置一個pool,這個複用pool是針對item中包含rv的狀況才適用,若是rv中的item都是普通的佈局就不須要複用pool

如上圖所示紅框就是一個item中嵌套rv的例子,這種場景仍是比較常見,若是有多個item都是這種類型那麼複用pool就很是有必要了,在封裝adapter庫時須要考慮的一個點就是如何找到item中包含rv,能夠考慮的作法就是遍歷item的根佈局若是找到包含rv的,那麼將對該rv設置pool,全部item中的嵌套rv都使用同一個pool便可,查找item中rv代碼能夠以下.

private List<RecyclerView> findNestedRecyclerView(View rootView) {
    List<RecyclerView> list = new ArrayList<>();
    if (rootView instanceof RecyclerView) {
        list.add((RecyclerView) rootView);
        return list;
    }
    if (!(rootView instanceof ViewGroup)) {
        return list;
    }
    final ViewGroup parent = (ViewGroup) rootView;
    final int count = parent.getChildCount();
    for (int i = 0; i < count; i++) {
        View child = parent.getChildAt(i);
        list.addAll(findNestedRecyclerView(child));
    }
    return list;
}

獲得該list以後接下來要作的就是給裏面的rv綁定pool了,能夠將該pool設置爲adapter庫中的成員變量,每次找到嵌套rv的item時直接將該pool設置給對應的rv便可。

關於使用pool源碼上有一點須要在乎的是,當最外層的rv滑動致使item被移除屏幕時,rv其實最終是經過調用.

removeview(view)完成的,裏面的參數view就是和holder綁定的rootview,若是rootview中包含了rv,也就是上圖所示的狀況,會最終調用到嵌套rv的onDetachedFromWindow方法:

@Override
public void onDetachedFromWindow(RecyclerView view, RecyclerView.Recycler recycler) {
    super.onDetachedFromWindow(view, recycler);
    if (mRecycleChildrenOnDetach) {
        removeAndRecycleAllViews(recycler);
        recycler.clear();
    }
}

注意裏面的if分支,若是進入該分支裏面的主要邏輯就是會清除掉scrap和cached緩存上的holder並將它們放置到pool中,可是默認狀況下mRecycleChildrenOnDetach是爲false的,這麼設計的目的就在於放置到pool中的holder要想被拿來使用還必須調用onbindview來進行從新綁定數據,因此google默認將該參數設置爲了false,這樣即便rv會移除屏幕也不會使裏面的holder失效,下次再次進入屏幕的時候就能夠直接使用避免了onbindview的操做。

可是google仍是提供了setRecycleChildrenOnDetach方法容許咱們改變它的值,若是要想充分使用pool的功能,最好將其置爲true,由於按照通常的用戶習慣滑出屏幕的item通常不會回滾查看,這樣接下來要被滑入的item若是存在rv的狀況下就能夠快速複用pool中的holder,這是使用pool複用的時候一個須要注意點的地方。

保存嵌套rv的滑動狀態

原來開發的時候產品就提出過這種需求,須要將滑動位置進行保存,不然每次位置被重置開起來很是奇怪,具體是個什麼問題呢,仍是以上圖嵌套rv爲例,紅框中的rv能夠看出來是滑動到中間位置的,若是這時將該rv移出屏幕,而後再移動回屏幕會發生什麼事情。

這裏要分兩種狀況,一種是移出屏幕一點後就直接從新移回屏幕,另外一種是移出屏幕一段距離再移回來。

你會發現一個比較神奇的事就是移出一點回來的rv會保留原先的滑動狀態,而移出一大段距離後回來的rv會丟失掉原先的滑動狀態,形成這個緣由的本質是在於rv的緩存機制,簡單來講就是剛滑動屏幕的會被放到cache中而滑出一段距離的會被放到pool中,而從pool中取出的holder會從新進行數據綁定,沒有保存滑動狀態的話rv就會被重置掉,那麼如何才能作到即便放在pool中的holder也能保存滑動狀態。

其實這個問題google也替咱們考慮到了,linearlayoutmanager中有對應的onSaveInstanceState和onRestoreInstanceState方法來分別處理保存狀態和恢復狀態,它的機制其實和activity的狀態恢復很是相似,咱們須要作的就是當rv被移除屏幕調用onSaveInstanceState,移回來時調用onRestoreInstanceState便可。

須要注意點的是onRestoreInstanceState須要傳入一個參數parcelable,這個是onSaveInstanceState提供給咱們的,parcelable裏面就保存了當前的滑動位置信息,若是本身在封裝adapter庫的時候就須要將這個parcelable保存起來:

private Map<Integer, SparseArrayCompat<Parcelable>> states;

map中的key爲item對應的position,考慮到一個item中可能嵌套多個rv因此value爲SparseArrayCompat,最終的效果

能夠看到幾個rv在被移出屏幕後再移回來可以正確保存滑動的位置信息,而且在刪除其中一個item後states中的信息也能獲得同步的更新,更新的實現就是利用rv的registerAdapterDataObserver方法,在adapter調用完notify系列方法後會在對應的回調中響應,對於map的更新操做能夠放置到這些回調中進行處理。

視狀況設置itemanimator動畫

使用過listview的都知道listview是沒有item改變更畫效果的,而rv默認就是支持動畫效果的,以前說過rv內部源碼有1萬多行,其實除了rv內部作了大量優化以外,爲了支持item的動畫效果google也沒少下苦功夫,也正是由於這樣才使得rv源碼看起來很是複雜。

默認在開啓item動畫的狀況下會使rv額外處理不少的邏輯判斷,notify的增刪改操做都會對應相應的item動畫效果,因此若是你的應用不須要這些動畫效果的話能夠直接關閉掉,這樣能夠在處理增刪改操做時大大簡化rv的內部邏輯處理,關閉的方法直接調用setItemAnimator(null)便可。

diffutil一個神奇的工具類

diffutil是配合rv進行差別化比較的工具類,經過對比先後兩個data數據集合,diffutil會自動給出一系列的notify操做,避免咱們手動調用notifiy的繁瑣,看一個簡單的使用示例:

data = new ArrayList<>();
data.add(new MultiTypeItem(R.layout.testlayout1, "hello1"));
data.add(new MultiTypeItem(R.layout.testlayout1, "hello2"));
data.add(new MultiTypeItem(R.layout.testlayout1, "hello3"));
data.add(new MultiTypeItem(R.layout.testlayout1, "hello4"));
data.add(new MultiTypeItem(R.layout.testlayout1, "hello5"));
data.add(new MultiTypeItem(R.layout.testlayout1, "hello6"));
data.add(new MultiTypeItem(R.layout.testlayout1, "hello7"));

newData = new ArrayList<>();
//改
newData.add(new MultiTypeItem(R.layout.testlayout1, "new one"));
newData.add(new MultiTypeItem(R.layout.testlayout1, "hello2"));
newData.add(new MultiTypeItem(R.layout.testlayout1, "hello3"));
newData.add(new MultiTypeItem(R.layout.testlayout1, "hello4"));
//增
newData.add(new MultiTypeItem(R.layout.testlayout1, "add one"));
newData.add(new MultiTypeItem(R.layout.testlayout1, "hello5"));
newData.add(new MultiTypeItem(R.layout.testlayout1, "hello6"));
newData.add(new MultiTypeItem(R.layout.testlayout1, "hello7"));

先準備兩個數據集合分別表明原數據集和最新的數據集,而後實現下Callback接口:

private class DiffCallBack extends DiffUtil.Callback {

        @Override
        public int getOldListSize() {
            return data.size();
        }

        @Override
        public int getNewListSize() {
            return newData.size();
        }

        @Override
        public boolean areItemsTheSame(int oldItemPosition, int newItemPosition) {
            return data.get(oldItemPosition).getType() == newData.get(newItemPosition).getType();
        }

        @Override
        public boolean areContentsTheSame(int oldItemPosition, int newItemPosition) {
            String oldStr = (String) DiffUtilDemoActivity.this.data.get(oldItemPosition).getData();
            String newStr = (String) DiffUtilDemoActivity.this.newData.get(newItemPosition).getData();
            return oldStr.equals(newStr);
        }
    }

實現的方法比較容易看懂,diffutil之因此能判斷兩個數據集的差距就是經過調用上述方法實現,areItemsTheSame表示的就是兩個數據集對應position上的itemtype是否同樣,areContentsTheSame就是比較在itemtype一致的狀況下item中內容是否相同,能夠理解成是否須要對item進行局部刷新。實現完callback以後接下來就是如何調用了。

DiffUtil.DiffResult diffResult = DiffUtil.calculateDiff(new DiffCallBack(), true);
diffResult.dispatchUpdatesTo(adapter);
adapter.setData(newData);

上述就是diffutil一個簡單的代碼範例,其實最開始的時候本身想將diffutil封裝到adapter庫,但實際在使用後發現了幾個自認爲的弊端,因此放棄使用該工具類,這也多是本身沒有徹底掌握diffutil精髓所致使的吧,這裏就直接說下我對diffutil使用的見解。

弊端一:

看示例代碼應該也能察覺到,要想使用diffutil必須準備兩個數據集,這就是一個比較蛋疼的事情.

原先咱們只須要維護一個數據集就能夠,如今就須要咱們同時維護兩個數據集,兩個數據集都須要有一份本身的數據.

若是隻是簡單將數據從一個集合copy到另外一個集合是可能會致使問題的,會涉及到對象的深拷貝和淺拷貝問題,你必須保證兩份數據集都有各自獨立的內存,不然當你修改其中一個數據集可能會形成另外一個數據集同時被修改掉的狀況。

弊端二:

爲了實現callback接口必須實現四個方法,其中areContentsTheSame是最難實現的一個方法,由於這裏涉及到對比同type的item內容是否一致,這就須要將該item對應的數據bean進行比較,怎麼比較效率會高點,目前能想到的方法就是將bean轉換成string經過調用equals方法進行比較,若是item的數據bean對應的成員變量不多如示例所示那倒還好,這也是網上不少推薦diffutil文章避開的問題。

可是若是bean對應的成員不少,或者成員變量含有list,裏面又包含各類對象元素,想一想就知道areContentsTheSame很難去實現,爲了引入一個diffutil額外增長這麼多的邏輯判斷有點得不償失。

弊端三:

diffutil看起來讓人捉摸不透的item動畫行爲,以上面代碼爲例

newData.add(new MultiTypeItem(R.layout.testlayout1, "hello1"));
newData.add(new MultiTypeItem(R.layout.testlayout1, "hello2"));
//        newData.add(new MultiTypeItem(R.layout.testlayout1, "hello3"));
newData.add(new MultiTypeItem(R.layout.testlayout1, "hello4"));
newData.add(new MultiTypeItem(R.layout.testlayout1, "hello5"));
newData.add(new MultiTypeItem(R.layout.testlayout1, "hello6"));
newData.add(new MultiTypeItem(R.layout.testlayout1, "hello7"));

新的數據集和原有數據集惟一的不一樣點就在於中間刪除了一條數據,按照原先咱們對於rv的理解,執行的表現形式應該是hello3被刪除掉,而後hello3下面的全部item總體上移纔對,但在使用diffutil後你會發現並非這樣的,它的表現比較怪異會移除第一條數據,這種怪異的行爲應該和diffutil內部複雜的算法有關。

基於上述幾個弊端因此最終本身並無在adapter庫去使用diffutil,比較有意思的是以前在看關於diffutil文章的時候特地留言問過其中一個做者在實際開發中是否有使用過diffutil,獲得的答案是並無在實際項目使用過,因此對於一些工具類是否真的好用還須要實際項目來檢驗,固然上面所說的都只是個人理解,不排除有人能透徹理解diffutil活用它的開發者,只是我沒有在網上找到這種文章。

setHasFixedSize

又是一個google提供給咱們的方法,主要做用就是設置固定高度的rv,避免rv重複measure調用。

這個方法能夠配合rv的wrap_content屬性來使用,好比一個垂直滾動的rv,它的height屬性設置爲wrap_content,最初的時候數據集data只有3條數據,所有展現出來也不能使rv撐滿整個屏幕,若是這時咱們經過調用notifyItemRangeInserted增長一條數據,在設置setHasFixedSize和沒有設置setHasFixedSize你會發現rv的高度是不同的,設置過setHasFixedSize屬性的rv高度不會改變,而沒有設置過則rv會從新measure它的高度,這是setHasFixedSize表現出來的外在形式,咱們能夠從代碼層來找到其中的緣由。

notifiy的一系列方法除了notifyDataSetChanged這種萬金油的方式,還有一系列進行局部刷新的方法可供調用,而這些方法最終都會執行到一個方法

void triggerUpdateProcessor() {
    if (POST_UPDATES_ON_ANIMATION && mHasFixedSize && mIsAttached) {
        ViewCompat.postOnAnimation(RecyclerView.this, mUpdateChildViewsRunnable);
    } else {
        mAdapterUpdateDuringMeasure = true;
        requestLayout();
    }
}

區別就在於當設置過setHasFixedSize會走if分支,而沒有設置則進入到else分支,else分支直接會調用到requestLayout方法.

該方法會致使視圖樹進行從新繪製,onmeasure,onlayout最終都會被執行到,結合這點再來看爲何rv的高度屬性爲wrap_content時會受到setHasFixedSize影響就很清楚了,根據上述源碼能夠獲得一個優化的地方在於,當item嵌套了rv而且rv沒有設置wrap_content屬性時,咱們能夠對該rv設置setHasFixedSize,這麼作的一個最大的好處就是嵌套的rv不會觸發requestLayout,從而不會致使外層的rv進行重繪,關於這個優化應該不少人都不知道,網上一些介紹setHasFixedSize的文章也並無提到這點。

上面介紹的這些方法都是本身在研究rv優化時本身總結的一些心得,文章到這裏其實應該能夠結束,但在看源碼的過程當中還發現了幾個比較有意思的方法,如今分享出來.

swapadapter

rv的setadapter你們都會使用,沒什麼好說的,但關於swapadapter可能就有些人不太知道了,這兩個方法最大的不一樣之處就在於setadapter會直接清空rv上的全部緩存,而swapadapter會將rv上的holder保存到pool中,google提供swapadapter方法考慮到的一個應用場景應該是兩個數據源有很大的類似部分的狀況下,直接使用setadapter重置的話會致使本來能夠被複用的holder所有被清空,而使用swapadapter來代替setadapter能夠充分利用rv的緩存機制,能夠說是一種更爲明智的選擇。

getAdapterPosition和getLayoutPosition

大部分狀況下調用這兩個方法獲得的結果是一致的,都是爲了得到holder對應的position位置,但getAdapterPosition獲取位置更爲及時,而getLayoutPosition會滯後到下一幀才能獲得正確的position,若是你想及時獲得holder對應的position信息建議使用前者。

舉個最簡單的例子就是當調用完notifyItemRangeInserted在rv頭部插入一個item後當即調用這兩個方法獲取下原先處於第一個位置的position就能當即看出區別,其實跟蹤下

getAdapterPosition的源碼很快能發現緣由

public int applyPendingUpdatesToPosition(int position) {
    final int size = mPendingUpdates.size();
    for (int i = 0; i < size; i++) {
        UpdateOp op = mPendingUpdates.get(i);
        switch (op.cmd) {
            case UpdateOp.ADD:
                if (op.positionStart <= position) {
                    position += op.itemCount;
                }
                break;
            case UpdateOp.REMOVE:
                if (op.positionStart <= position) {
                    final int end = op.positionStart + op.itemCount;
                    if (end > position) {
                        return RecyclerView.NO_POSITION;
                    }
                    position -= op.itemCount;
                }
                break;
            case UpdateOp.MOVE:
                if (op.positionStart == position) {
                    position = op.itemCount; //position end
                } else {
                    if (op.positionStart < position) {
                        position -= 1;
                    }
                    if (op.itemCount <= position) {
                        position += 1;
                    }
                }
                break;
        }
    }
    return position;
}

最終getAdapterPosition會進入到上述方法,在這個方法就能很清楚看出爲何getAdapterPosition老是能及時反應出position的正確位置。可是有一點須要注意的就是getAdapterPosition可能會返回-1

if (viewHolder.hasAnyOfTheFlags(ViewHolder.FLAG_INVALID
        | ViewHolder.FLAG_REMOVED | ViewHolder.FLAG_ADAPTER_POSITION_UNKNOWN)
        || !viewHolder.isBound()) {
    return RecyclerView.NO_POSITION;
}

這點須要特別留意,作好預防處理。

removeview和detachview

這兩個方法在rv進行排布item的時候會遇到,removeview就是你們很常見的操做,可是detachview就不太常見了,其實removeview是一個更爲完全的移除view操做,內部是會調用到detachview的,而且會調用到咱們很熟悉的ondetachfromwindow方法,而detachview是一個輕量級的操做,內部操做就是簡單的將該view從父view中移除掉,rv內部調用detachview的場景就是對應被移除的view可能在近期還會被使用到因此採用輕量級的移除操做,removeview通常都預示着這個holder已經完全從屏幕消失不可見了。

總結

總算寫完了,費了好大力氣,寫一篇技術文章真的很費時間,這樣一直堅持了一年時間,每篇文章都是本身用心去寫的,也是對本身以前研究過技術的一個總結,其實年前就已經想寫這篇文章,但老是被各類事情耽擱,如今咬咬牙把它寫完了。

rv確實是一個比較複雜的控件,看源碼最好的方式就是基於簡單的應用場景切入,而後在此基礎上嘗試rv的各類方法,帶着這些問題去分析源碼每每會比干看更有動力。

更多資料分享歡迎Android工程師朋友們加入安卓開發技術進階互助:856328774免費提供安卓開發架構的資料(包括Fultter、高級UI、性能優化、架構師課程、 NDK、Kotlin、混合式開發(ReactNative+Weex)和一線互聯網公司關於Android面試的題目彙總。
相關文章
相關標籤/搜索