歡迎光臨
每天分享高質量文章

看完感覺我RecyclerView白學了!

作者:mandypig

連結:https://www.jianshu.com/p/1d2213f303fc

非常棒的文章,第4節一定要看!

1、前言

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

其實RecyclerView做為android開發一個非常常用的控制元件,可以這麼說,一般普通的ui頁面都可以透過RecyclerView去實現,個人覺得RecyclerView可以完全去替換掉scrollview,這裡說的普通的ui頁面特指那些沒有酷炫互動方式的頁面。

深入理解RecyclerView最佳化方面的技術對於發揮RecyclerView的效能是非常有幫助的。

寫這篇文章的緣由還是之前專案在使用同事封裝的adapter庫時,bugly上報崩潰,在解決問題的過程中有機會深入理解RecyclerView的部分原始碼,結合網上一些文章,自己總結出來的心得體會,有興趣的可以去看看我原先的那篇文章bugly關於RecyclerView崩潰問題研究

借用一句現在流行的網路用語就是,RecyclerView不止眼前的setadapter和notify,還有詩和遠方。閑話扯到這,接下來就來看一下RecyclerView最佳化方面的東西。

關於RecyclerView的最佳化,自己會將它們分為兩大類,一類是RecyclerView自帶的系統最佳化,另一類就是我們透過程式碼實現的手動最佳化,先來介紹下RecyclerView自帶的系統最佳化。系統最佳化我們不能做太多的幹預,但是透過理解RecyclerView的系統最佳化能夠讓我們更好的理解RecyclerView的工作機制。

2、預取功能(Prefetch)

這個功能是rv在版本25之後自帶的,也就是說只要你使用了25或者之後版本的rv,那麼就自帶該功能,並且預設就是處理開啟的狀態,透過LinearLayoutManager的setInitialItemPrefetchCount()我們可以手動控制該功能的開啟關閉,但是一般情況下沒必要也不推薦關閉該功能,預取功能的原理比較好理解,如圖所示

我們都知道android是透過每16ms掃清一次頁面來保證ui的流暢程度,現在android系統中掃清ui會透過cpu產生資料,然後交給gpu渲染的形式來完成,從上圖可以看出當cpu完成資料處理交給gpu後就一直處於空閑狀態,需要等待下一幀才會進行資料處理.

而這空閑時間就被白白浪費了,如何才能壓榨cpu的效能,讓它一直處於忙碌狀態,這就是rv的預取功能(Prefetch)要做的事情,rv會預取接下來可能要顯示的item,在下一幀到來之前提前處理完資料,然後將得到的itemholder快取起來,等到真正要使用的時候直接從快取取出來即可。

預取程式碼理解

雖說預取是預設開啟不需要我們開發者操心的事情,但是明白原理還是能加深該功能的理解。下麵就說下自己在看預取原始碼時的一點理解。實現預取功能的一個關鍵類就是gapworker,可以直接在rv原始碼中找到該類

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 }

來進行判斷,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控制元件一統天下的感覺。

3、四級快取

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

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

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

可以先來看下recycler中關於這四級快取的程式碼部分:

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

        final ArrayList mCachedViews = new ArrayList();

        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崩潰問題的時候會比較抓狂,根本原因還是在於沒能好好研究過一些相關原始碼。

4、我們可以做的

上面都在說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 findNestedRecyclerView(View rootView) {
    List 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         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>> 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         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 -= 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的各種方法,帶著這些問題去分析原始碼往往會比干看更有動力。

贊(0)

分享創造快樂