1. 程式人生 > >最全面的RecyclerView原始碼解析

最全面的RecyclerView原始碼解析

相信很多人用RecyclerView已經很久了,但還是不得不感嘆 RecyclerView的強大,效能、擴充套件性等方面都很強大。網上看了很多原始碼方面對RecyclerView,覺得還不夠全面,而且自己不走一遍原始碼總感覺會很容易忘記。
開啟RecyclerView類,發現有11090行程式碼,千萬不要震驚,慢慢看。
整體看了下RecyclerView的架構,你會驚奇於這個優雅的設計,高度解耦,靈活性很強,給開發者一種插拔式的體驗,使用者只要通過設定不同的LayoutManager, ItemDecoration, ItemAnimator就可以實現各種各樣的效果了。

Recycler

開啟RecyclerView的結構,複雜,我們就從上往下說好了。首先來分析Recycler這個內部類,Recycler是整個RecyclerView的精髓所在,那麼Recycler到底是什麼呢:
Recycler的職責是管理那些已經廢棄了的或者從RecyclerView中分離的item view用於複用。
廢棄的View是指那些仍然依附於RecyclerView但是已經被標記為可以被移除或者複用的View。
Recycler典型的用法就是當LayoutManager去獲取Adapter中的某一項View的時候,如果這個View失效了,則需要重新繫結View,當複用的View是有效的話,View就會被直接被複用。有效的View如果不主動呼叫requestLayout,那麼該View不需要重新測量就可以被複用。
說了一大堆,還是邊看程式碼邊解釋更加清晰一點,首先來看Recycler幾個成員變數:

final ArrayList<ViewHolder> mAttachedScrap = new ArrayList<>();
private ArrayList<ViewHolder> mChangedScrap = null;
final ArrayList<ViewHolder> mCachedViews = new  ArrayList<ViewHolder>();
private final List<ViewHolder>
                mUnmodifiableAttachedScrap = Collections.unmodifiableList(mAttachedScrap);

private
int mViewCacheMax = DEFAULT_CACHE_SIZE; private RecycledViewPool mRecyclerPool; private ViewCacheExtension mViewCacheExtension;

先來看RecycledViewPool和ViewCacheExtension這兩個類:
RecycledViewPool讓開發者可以在多個RecyclerView之間共享View。
如果你想要垮RecyclerView複用View,建立一個RecycledViewPool例項,然後呼叫setRecycledViewPool(RecycledViewPool)方法就可以了。
RecyclerView會自動建立一個RecycledViewPool的例項。
有了RecycledViewPool的存在,就能很大程度上減少View的建立,提高效能。
先看下面這兩個方法:

private SparseArray<ArrayList<ViewHolder>> mScrap =
                new SparseArray<ArrayList<ViewHolder>>();
        private SparseIntArray mMaxScrap = new SparseIntArray();

//此處省略部分程式碼 
//……
public void setMaxRecycledViews(int viewType, int max) {
            mMaxScrap.put(viewType, max);
            final ArrayList<ViewHolder> scrapHeap = mScrap.get(viewType);
            if (scrapHeap != null) {
                while (scrapHeap.size() > max) {
                    scrapHeap.remove(scrapHeap.size() - 1);
                }
            }
        }

public ViewHolder getRecycledView(int viewType) {
            final ArrayList<ViewHolder> scrapHeap = mScrap.get(viewType);
            if (scrapHeap != null && !scrapHeap.isEmpty()) {
                final int index = scrapHeap.size() - 1;
                final ViewHolder scrap = scrapHeap.get(index);
                scrapHeap.remove(index);
                return scrap;
            }
            return null;
        }

可以看到,mScrap是一個《viewType, List>的對映,mMaxScrap是一個《viewType, maxNum>的對映,我們可以呼叫setMaxRecycledViews方法來設定每種viewType的view容量。從原始碼可以看出,如果viewType型別的list的size大於制定的最大數字的話,會先從列表的末尾開始丟棄超出的部分。呼叫getRecycledView(int viewType)方法呢,可以將scrapHeap中的最後一項移除並返回viewType對應的List的末尾項。這裡需要注意的是,因為是跨RecyclerView進行操作,所以要特別注意對於同一個RecycledViewPool,對ViewType的定義要統一,因為這裡是根據viewType來取ViewHolder的。

再來看ViewCacheExtension這個類:

public abstract static class ViewCacheExtension {
        abstract public View getViewForPositionAndType(Recycler recycler, int position, int type);
    }

ViewCacheExtension是一個幫助類,給開發者提供了一個可以由他們自己控制View快取的這麼一個類。當呼叫Recycler的getViewForPosition(int position)方法等時候,Recycler會先去檢查attached scrap和一級快取來這個View,如果都找不到匹配的View的話,Recycler會呼叫ViewCacheExtension的getViewForPositionAndType方法檢查,如果還是沒有,再去檢查RecycledViewPool。
需要注意的是:Recycler不會在這個類中做任何快取View的操作,是否需要快取View由開發者自己控制。
然後回到Recycler,Recycler最終目的還是要去獲取到來看這個方法:

        View getViewForPosition(int position, boolean dryRun) {
            if (position < 0 || position >= mState.getItemCount()) {
                throw new IndexOutOfBoundsException("Invalid item position " + position
                        + "(" + position + "). Item count:" + mState.getItemCount());
            }
            boolean fromScrap = false;
            ViewHolder holder = null;
            // 0) If there is a changed scrap, try to find from there
            if (mState.isPreLayout()) {
                holder = getChangedScrapViewForPosition(position);
                fromScrap = holder != null;
            }
            // 1) Find from scrap by position
            if (holder == null) {
                holder = getScrapViewForPosition(position, INVALID_TYPE, dryRun);
                if (holder != null) {
                    if (!validateViewHolderForOffsetPosition(holder)) {
                        // recycle this scrap
                        if (!dryRun) {
                            // we would like to recycle this but need to make sure it is not used by
                            // animation logic etc.
                            holder.addFlags(ViewHolder.FLAG_INVALID);
                            if (holder.isScrap()) {
                                removeDetachedView(holder.itemView, false);
                                holder.unScrap();
                            } else if (holder.wasReturnedFromScrap()) {
                                holder.clearReturnedFromScrapFlag();
                            }
                            recycleViewHolderInternal(holder);
                        }
                        holder = null;
                    } else {
                        fromScrap = true;
                    }
                }
            }
            if (holder == null) {
                final int offsetPosition = mAdapterHelper.findPositionOffset(position);
                if (offsetPosition < 0 || offsetPosition >= mAdapter.getItemCount()) {
                    throw new IndexOutOfBoundsException("Inconsistency detected. Invalid item "
                            + "position " + position + "(offset:" + offsetPosition + ")."
                            + "state:" + mState.getItemCount());
                }

                final int type = mAdapter.getItemViewType(offsetPosition);
                // 2) Find from scrap via stable ids, if exists
                if (mAdapter.hasStableIds()) {
                    holder = getScrapViewForId(mAdapter.getItemId(offsetPosition), type, dryRun);
                    if (holder != null) {
                        // update position
                        holder.mPosition = offsetPosition;
                        fromScrap = true;
                    }
                }
                if (holder == null && mViewCacheExtension != null) {
                    // We are NOT sending the offsetPosition because LayoutManager does not
                    // know it.
                    final View view = mViewCacheExtension
                            .getViewForPositionAndType(this, position, type);
                    if (view != null) {
                        holder = getChildViewHolder(view);
                        if (holder == null) {
                            throw new IllegalArgumentException("getViewForPositionAndType returned"
                                    + " a view which does not have a ViewHolder");
                        } else if (holder.shouldIgnore()) {
                            throw new IllegalArgumentException("getViewForPositionAndType returned"
                                    + " a view that is ignored. You must call stopIgnoring before"
                                    + " returning this view.");
                        }
                    }
                }
                if (holder == null) { // fallback to recycler
                    // try recycler.
                    // Head to the shared pool.
                    if (DEBUG) {
                        Log.d(TAG, "getViewForPosition(" + position + ") fetching from shared "
                                + "pool");
                    }
                    holder = getRecycledViewPool().getRecycledView(type);
                    if (holder != null) {
                        holder.resetInternal();
                        if (FORCE_INVALIDATE_DISPLAY_LIST) {
                            invalidateDisplayListInt(holder);
                        }
                    }
                }
                if (holder == null) {
                    holder = mAdapter.createViewHolder(RecyclerView.this, type);
                    if (DEBUG) {
                        Log.d(TAG, "getViewForPosition created new ViewHolder");
                    }
                }
            }

            // This is very ugly but the only place we can grab this information
            // before the View is rebound and returned to the LayoutManager for post layout ops.
            // We don't need this in pre-layout since the VH is not updated by the LM.
            if (fromScrap && !mState.isPreLayout() && holder
                    .hasAnyOfTheFlags(ViewHolder.FLAG_BOUNCED_FROM_HIDDEN_LIST)) {
                holder.setFlags(0, ViewHolder.FLAG_BOUNCED_FROM_HIDDEN_LIST);
                if (mState.mRunSimpleAnimations) {
                    int changeFlags = ItemAnimator
                            .buildAdapterChangeFlagsForAnimations(holder);
                    changeFlags |= ItemAnimator.FLAG_APPEARED_IN_PRE_LAYOUT;
                    final ItemHolderInfo info = mItemAnimator.recordPreLayoutInformation(mState,
                            holder, changeFlags, holder.getUnmodifiedPayloads());
                    recordAnimationInfoIfBouncedHiddenView(holder, info);
                }
            }

            boolean bound = false;
            if (mState.isPreLayout() && holder.isBound()) {
                // do not update unless we absolutely have to.
                holder.mPreLayoutPosition = position;
            } else if (!holder.isBound() || holder.needsUpdate() || holder.isInvalid()) {
                if (DEBUG && holder.isRemoved()) {
                    throw new IllegalStateException("Removed holder should be bound and it should"
                            + " come here only in pre-layout. Holder: " + holder);
                }
                final int offsetPosition = mAdapterHelper.findPositionOffset(position);
                holder.mOwnerRecyclerView = RecyclerView.this;
                mAdapter.bindViewHolder(holder, offsetPosition);
                attachAccessibilityDelegate(holder.itemView);
                bound = true;
                if (mState.isPreLayout()) {
                    holder.mPreLayoutPosition = position;
                }
            }

            final ViewGroup.LayoutParams lp = holder.itemView.getLayoutParams();
            final LayoutParams rvLayoutParams;
            if (lp == null) {
                rvLayoutParams = (LayoutParams) generateDefaultLayoutParams();
                holder.itemView.setLayoutParams(rvLayoutParams);
            } else if (!checkLayoutParams(lp)) {
                rvLayoutParams = (LayoutParams) generateLayoutParams(lp);
                holder.itemView.setLayoutParams(rvLayoutParams);
            } else {
                rvLayoutParams = (LayoutParams) lp;
            }
            rvLayoutParams.mViewHolder = holder;
            rvLayoutParams.mPendingInvalidate = fromScrap && bound;
            return holder.itemView;
        }

從程式碼中可以看出,要獲取到某個position下的view,有這麼一個過程:
首先Recycler會先去檢查mChangedScrap,如果匹配成功則返回相應的viewHolder。mChangedScrap就是所謂的detachedView,與RecyclerView分離的viewHolder列表。
然後是mAttachedScrap,mViewCacheExtension,mRecyclerPool,如果都沒有,則去建立,呼叫Adapter.createViewHolder()。
其實相當於RecyclerView也做到了二級快取的概念,mCachedViews是一層,預設大小是DEFAULT_CACHE_SIZE = 2, 還有一層就是RecycledViewPool。我們可以呼叫這個方法來控制第一層的快取數量:

public void setViewCacheSize(int viewCount) {
            mViewCacheMax = viewCount;
            // first, try the views that can be recycled
            for (int i = mCachedViews.size() - 1; i >= 0 && mCachedViews.size() > viewCount; i--) {
                recycleCachedViewAt(i);
            }
        }

這就是Recycler去根據position獲取相應的itemView的過程,既然有獲取,那麼就肯定有放入,來看這個方法:

void recycleViewHolderInternal(ViewHolder holder) {
            //省略一部分程式碼。。。
            if (forceRecycle || holder.isRecyclable()) {
                if (!holder.hasAnyOfTheFlags(ViewHolder.FLAG_INVALID | ViewHolder.FLAG_REMOVED
                        | ViewHolder.FLAG_UPDATE)) {
                    // Retire oldest cached view
                    final int cachedViewSize = mCachedViews.size();
                    if (cachedViewSize == mViewCacheMax && cachedViewSize > 0) {
                        recycleCachedViewAt(0);
                    }
                    if (cachedViewSize < mViewCacheMax) {
                        mCachedViews.add(holder);
                        cached = true;
                    }
                }
                if (!cached) {
                    addViewHolderToRecycledViewPool(holder);
                    recycled = true;
                }
            } else if (DEBUG) {
                Log.d(TAG, "trying to recycle a non-recycleable holder. Hopefully, it will "
                        + "re-visit here. We are still removing it from animation lists");
            }
            mViewInfoStore.removeViewHolder(holder);
            if (!cached && !recycled && transientStatePreventsRecycling) {
                holder.mOwnerRecyclerView = null;
            }
        }

這個方法其實是先去判斷了下mCachedViews是否已經滿了,如果滿了,則移除一個到recycledViewPool中去,然後將新的itemView新增到mCachedViews,如果沒滿那最好了,直接新增。

LayoutManager

LayoutManager的主要職責就是去測量和擺放itemView和當itemView對使用者來說不再能看見的時候去複用它。LayoutManager牽扯到的東西其實也蠻多的,我特意去翻看了下LinearLayoutManager的原始碼,挺複雜的,就不再這篇講了,到時候另起一篇來講下這個東西。
可以看到,RecyclerView的onMeasure和onLayout方法都是交給了LayoutManager來處理。

@Override
    protected void onMeasure(int widthSpec, int heightSpec) {
        //省略一段程式碼
        if (mLayout.mAutoMeasure) {
            final int widthMode = MeasureSpec.getMode(widthSpec);
            final int heightMode = MeasureSpec.getMode(heightSpec);
            final boolean skipMeasure = widthMode == MeasureSpec.EXACTLY
                    && heightMode == MeasureSpec.EXACTLY;
            mLayout.onMeasure(mRecycler, mState, widthSpec, heightSpec);
            if (skipMeasure || mAdapter == null) {
                return;
            }
            if (mState.mLayoutStep == State.STEP_START) {
                dispatchLayoutStep1();
            }
            mLayout.setMeasureSpecs(widthSpec, heightSpec);
            //省略一段程式碼
        } else {
            if (mHasFixedSize) {
                mLayout.onMeasure(mRecycler, mState, widthSpec, heightSpec);
                return;
            }
            //省略一段程式碼。。。。。。
            if (mAdapter != null) {
                mState.mItemCount = mAdapter.getItemCount();
            } else {
                mState.mItemCount = 0;
            }
            eatRequestLayout();
            mLayout.onMeasure(mRecycler, mState, widthSpec, heightSpec);
            resumeRequestLayout(false);
            mState.mInPreLayout = false; 
        }
    }

滑動

RecyclerView的onTouchEvent方法比較長,這裡就不貼了。其實滑動的套路都差不多的,RecyclerView也一樣。那就講一下我看了原始碼後的理解吧,這裡以垂直方向的滑動為力。
在ACTION_MOVE中,首先計算出手指滑動的距離,先跟TouchSlop進行比較,如果大雨這個值,那麼就是確定為師滑動了,而不是點選,然後呼叫scrollByInternal()方法,scrollByInternal其實最後還是用了scrollBy方法,拖拽性質的滑動就解決了。當然我們也發現當我們拖拽RecyclerView後,當我們的手指離開螢幕以後,RecyclerView還會滑動一段時間,這個其實就是fling事件。RecyclerView在ACTION_UP事件的時候,VelocityTracker根據拖拽時候滑動的距離和時間計算出一個速度,然後呼叫fling方法就行了,其實fling裡面還是用Scrooler來實現的。這裡貼下fling方法的程式碼:

public void fling(int velocityX, int velocityY) {
    setScrollState(SCROLL_STATE_SETTLING);
    mLastFlingX = mLastFlingY = 0;
    mScroller.fling(0, 0, velocityX, velocityY,
            Integer.MIN_VALUE, Integer.MAX_VALUE, Integer.MIN_VALUE, Integer.MAX_VALUE);
    postOnAnimation();
}

原始碼這個東西真的是越看越多,尤其RecyclerView感覺更是。還是分篇寫吧打算,下一篇繼續分析下LayoutManager,ViewHolder,ItemDecoration這些。第一次分析原始碼,如果有錯誤的地方請及時指出

相關推薦

全面的RecyclerView原始碼解析

相信很多人用RecyclerView已經很久了,但還是不得不感嘆 RecyclerView的強大,效能、擴充套件性等方面都很強大。網上看了很多原始碼方面對RecyclerView,覺得還不夠全面,而且自己不走一遍原始碼總感覺會很容易忘記。 開啟Recycler

【進階】RecyclerView原始碼解析(二)——快取機制

引言 接著上一篇部落格分析完RecyclerView的繪製流程,其實對RecyclerView已經有了一個大體的瞭解,尤其是RecyclerView和LayoutManager和ItemDecoration的關係。 本篇文章將對RecyclerVie

【進階】RecyclerView原始碼解析(三)——深度解析快取機制

上一篇部落格從原始碼角度分析了RecyclerView讀取快取的步驟,讓我們對於RecyclerView的快取有了一個初步的理解,但對於RecyclerView的快取的原理還是不能理解。本篇部落格將從實際專案角度來理解RecyclerView的快取原理。

RecyclerView原始碼解析

一.基本介紹 在平時的開發中我們或多或少地接觸過ListView,所以我們對ListView並不陌生。不過ListView存在某些缺點,所以谷歌又在ListView的基礎上推出了RecyclerView。記得RecyclerView剛推出時,網上許多文章都介紹

【進階】RecyclerView原始碼解析(一)——繪製流程

引言 自從Google出了RecyclerView後,基本上列表的場景已經完全替代了原來的ListView和GridView,現在不僅僅是列表,多樣式(俗稱蓋樓),複雜頁面等,只要我們願意,RecyclerView幾乎可以代替實現80%的佈局,Git

有史以來小的編譯器原始碼解析

後續內容更新,請前往:個人部落格,歡迎一起交流。 前言 稍微接觸一點前端,我們都知道現在前端“ES6即正義”,然而瀏覽器的支援還處於進行階段,所以我們常常會用一個神奇的工具將 ES6 語法轉換為目前支援比較廣泛的 ES5 語法,這裡我們所說的神奇的工具就是編譯器。

[work*] 全面解析 Ubuntu 16.04 安裝nvidia驅動

想在Lab的臺機上跑機器學習程式碼, 可以nvidia-smi, but一直報 AssertionError: Found no NVIDIA driver on your system. Please check that you have an NVIDIA GPU and installe

某課全Java併發程式設計高階技術-高效能併發框架原始碼解析與實戰

第1章 課程介紹(Java併發程式設計進階課程) 什麼是Disruptor?它一個高效能的非同步處理框架,號稱“單執行緒每秒可處理600W個訂單”的神器,本課程目標:徹底精通一個如此優秀的開源框架,面試秒殺面試官。本章會帶領小夥伴們先了解課程大綱與重點,然後模擬千萬,億級

Tomcat全面解析和作用介紹

最近需要學Tomcat的相關知識,在網上搜尋了很多csdn的知識,但是介紹的都很不全面 在我看來: Tomcat的作用以及概念就是web容器的概念,web容器是一種服務程式,在伺服器一個埠就有一個提供相應服務的程式,而這個程式就是處理從客戶端發出的請求,如JAV

143行js頂部進度條小外掛-nanobar.js原始碼解析

網頁頂部進度條外掛的有四五種,基本原理就是動態地建立一個元素,然後通過設定它的width來實現動畫效果,width增長到達指定位置時,將其去掉。 來看看nanobar.js作者jacoborus是怎麼做到的吧! /* http://nanobar.micronu

新版azkaban-3.40.0原始碼解析

web上傳zip以及解析入庫 web服務上傳zip包 入口: azkaban.webapp.servlet.LoginAbstractAzkabanServlet.doPost(HttpServletRequest, HttpServletRes

Java併發程式設計高階技術-高效能併發框架原始碼解析與實戰(已完結)2018(全)

1.背景 匿名內部類有以下問題: 語法過於冗餘 匿名類中的this和變數名容易使人產生誤解 型別載入和例項建立語義不夠靈活 無法捕獲非final的區域性變數 無法對控制流進行抽象 lambda表示式提供了輕量級的語法。 2.語法 lambda表示式的語法由引數列表、箭頭符號->和函式體

史上全Universal-Image-Loader原始碼解析————核心程式碼篇

背景 接著上一篇的內容,我們接著看ImageLoader的核心程式碼篇,上一篇主要是看ImageLoader的記憶體優化,主要包括磁碟快取和記憶體快取,還有就是記憶體的快取策略,這一篇,我們重點來看一下ImageLoader是如何使用這些記憶體和進行圖片的

Android四大元件:Service史上全面解析

前言 Service作為Android四大元件之一,應用非常廣泛 本文將介紹對Service進行全面介紹(基礎認識、生命週期、使用和應用場景) 目錄 1. 基礎知識 定義:服務,屬於Android中的計算型元件 作用

史上全Universal-Image-Loader原始碼解析————快取篇

背景 在我寫下這篇部落格的時候,我還是一名二本學校計算機專業大四的應屆畢業生,自學Android開發快兩年了,這兩年的時間裡面,其實真的是感慨萬千,兩年的時間裡面,Android的很多事情也都見識過了,其實Android對於新手入門在就業方面是相當不友好的事情。都說第一個吃螃蟹的人最

JSON介紹及Android全面解析方法(Gson、AS自帶org.son、Jackson解析)

前言 今天,我們來介紹一下現今主流的資料交換格式-JSON! 目錄 定義 JavaScript Object Notation,JavaScript的物件表示法,是一種輕量級的文字資料交換格式。 作用 用於資料的標記、儲存

【智慧製造】「人.機.料.法.環」全面解析

人:指製造產品的人員;機:指製造產品所用的裝置;料:指製造產品所使用的原材料;法:指製造產品所使

外掛化系列開發之九--Android 全面外掛化 RePlugin 流程與原始碼解析

 RePlugin,360開源的全面外掛化框架,按照官網說的,其目的是“儘可能多的讓模組變成外掛”,並在很穩定的前提下,儘可能像開發普通App那樣靈活。那麼下面就讓我們一起深入♂瞭解它吧。 (ps :閱讀本文請多參考原始碼圖片 ( ̄^ ̄)ゞ ) 一、介紹   RePlugi

Android開發:JSON簡介及全面解析方法(Gson、AS自帶org.json、Jackson解析)

目錄 JSON簡介&解析方法介紹.png 定義 JavaScript Object Notation,JavaScript的物件表示法,是一種輕量級的文字資料交換格式。 作用 用於資料的標記、儲存和傳輸。 特點 輕量級的文字資料交換格式 獨立於語言和平臺 具有自我描述性 讀寫速度快,解析簡單 語法

RecyclerView 的常用方法;工作原理與ListView比較;原始碼解析

寫在前面 本文原創,轉載請以連結形式註明地址:http://kymjs.com/code/2016/07/10/01起深入淺出這名字的時候我是慎重又慎重的,生怕被人罵標題黨,寫的什麼破玩意還敢說深入淺出。所以還是請大家不要抱著太高的期望,因為沒有期望就沒有失望,就像陳潤說的