(七)RecycleView 性能提高、卡頓優化(絕對乾貨!!)

目錄java

前言緩存

1、RecycleView 性能提高性能優化

(1)卡頓緣由:網絡

(2)優化提案:ide

2、佈局、繪製優化函數

 3、視圖綁定與數據處理分離工具

4、notifyxxx()局部刷新佈局

(1)經常使用的5個列表刷新性能

(2)處理刷新閃爍問題大數據

5、改變mCachedViews的緩存

6、共享RecycledViewPool

(1)嵌套RecycleView卡頓緣由

(2)解決嵌套RecycleView卡頓

7、慣性滑動延遲加載

(1)快速滑動RecycleView卡頓緣由:

(2)解決快速滑動形成的卡頓

(3)檢測慣性滑動

(4) 判斷是否已加載


 

前言

RecycleView 是一個可回收複用的列表控件,有着極高的靈活性,能實現ListView、GridView的全部功能。在平常開發中,RecycleView承擔着重要的做用,像是淘寶、京東等電商APP都會有商品列表的展現,能夠說與用戶體驗是緊密相連,其重要程度不言而喻。若是RecycleView使用不當將會影響到應用的總體性能拉低,所以它是性能優化系列中「卡頓優化」的重點。

Android性能優化(一)閃退治理、卡頓優化、耗電優化、APK瘦身

本篇,單獨瞭解一下若是對RecycleView 進行性能提高、卡頓優化。

推薦閱讀 

(一)RecycleView 初探回收複用,onCreateView和onBindView調用關係

(二)RecycleView 實現吸附小標題的Demo(附源碼)

(三)RecycleView 自定義下拉刷新,上拉加載監聽

(四)RecycleView 滑動到置頂、Adapter局部刷新

(五)RecycleView 動態設置改變列表顯示的高度

(六)RecycleView 回收複用機制總結

(七)RecycleView 性能提高、卡頓優化


1、RecycleView 性能提高

RecyclerView自身有一套完整的緩存機制,很是優秀,對於簡單的數據列通常不會有任何問題。但仍然存在不足之處。好比,不能根據滑動狀態自行調節數據綁定。遇到開發一些相似商城的應用,當展現大量的商品圖片的時候,快速滑動商品列表頁面,或頻繁增刪數據的時候,都頗有可能形成列表的卡頓。那麼,形成卡頓的緣由到底是什麼呢?

 

(1)卡頓緣由:

  • 界面設計不合理,佈局層級嵌套太多,過分繪製。
  • bindViewHolder中業務邏輯複雜,數據計算及類型轉換等耗時。
  • 界面數據改變,一味的全局刷新,致使閃屏卡頓。
  • 快速滑動列表,數據加載緩慢。

 

(2)優化提案:

  • 佈局、繪製優化。
  • 視圖綁定與數據處理分離。
  • notifyxxx()局部刷新。
  • 加大RecyclerView.mCachedViews的緩存。
  • 共享RecycledViewPool 。
  • 慣性滑動延遲加載。

 


2、佈局、繪製優化

老生常談的優化方案。就不過多贅述哦~

由於View的測量、佈局和繪製是經過遍從來進行操做的,若是佈局層級太多極易形成卡頓(官方建議不超過10層)。

能夠考慮自定義ViewGroup、<ViewStub>延遲View加載、<merge>標籤等方式減小層級;

多層次重疊的 UI 結構中移除底層背景減小過分繪製;

從而提升UI渲染的效率。

 


 3、視圖綁定與數據處理分離

onBindViewHolder()就是RecyclerView對item視圖進行數據綁定的方法。

由於,RecyclerView的onBindViewHolder()方法是在UI線程運行的,而該方法作了耗時操做就會影響滑動的流暢性。好比,下載文件操做、網絡鏈接操做、類型轉換操做(日期轉換、音頻格式轉換等)、文件操做、較大數據的初始化、sleep函數等。 

例如,我要在item裏面根據日期顯示背景顏色和年月日文字:

class ItemBean{
    Date dateDue;
    String title;
    String description;
}
static Date today = new Date();
static SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd", Locale.CHINA);

class MyRecyclerView.Adapter extends RecyclerView.Adapter {
    public onBindViewHolder(RecyclerView.ViewHolder tv, int position) {
        ItemBean bean = getItem(position);
        //日期的比較
        if (today.compareTo(bean.dateDue) > 0) {                    
            tv.backgroundView.setColor(Color.GREEN);
        } else {
            tv.backgroundView.setColor(Color.RED);
        }
        //日期的轉換
        String mDateSdf = sdf.format(bean.dateDue);
        tv.dateTextView.setDate(mDateSdf);
    }
}

案例中,在onBindViewHolder方法中進行了日期的比較和日期的格式化是很耗時的。然而,onBindViewHolder方法中應該只是將數據顯示到視圖中,而不該進行業務的處理。正確的作法是: 將日期的比較和日期的轉換在和RecycleView數據綁定以前提早計算完畢。大體表達的意思,以下:

class ItemBean{
    int backColor;
    String mDateSdf
}
class MyRecyclerView.Adapter extends RecyclerView.Adapter {
    public onBindViewHolder(RecyclerView.ViewHolder tv, int position) {
        ItemBean bean = getItem(position);
        tv.backgroundView.setColor(bean.backColor);
        tv.dateTextView.setDate(mDateSdf);
    }
}

 


4、notifyxxx()局部刷新

關於局部刷新,我在第四章裏講解了一點。下面來看看RecycleView的通知子項發生改變的幾種方法及處理刷新閃爍。

(1)經常使用的5個列表刷新

  • notifyDataSetChanged():所有刷新,(可能會閃)
  • notifyItemChanged (int) :指定一個刷新,(必定會閃)
  • notifyItemRangeChanged(int, int):指定刷新起始個數(必定會閃)
  • notifyItemInserted(int) :插入一個並刷新
  • notifyItemRemoved(int) :移除一個並刷新

對於新增、刪除、修改數據,能夠進行局部數據刷新,而不是一味的全局刷新數據,從而減小數據的綁定,下降卡頓。另外,可參考「DiffUtil」,它是support包下新增的一個工具類,判斷新數據和舊數據的差異進行局部刷新。

(2)處理刷新閃爍問題

一、爲何會出現閃爍呢?

  • 對於指定刷新:會走crateViewHolder和bindViewHolder從新建立和綁定。

  • 對於notifyDataSetChanged:會告知adapter,把全部的數據都從新加載了一遍,有緩存的直接獲取,沒緩存的從新建立。天然包括從新加載網絡圖片。

解決辦法:

notifyDataSetChanged + setHasStableIds(true) + 複寫getItemId() 方法 。(並不是是萬能的,注意場景,下面會講。)

mRecyclerViewAdapter.setHasStableIds(true);  

@Override
public long getItemId(int position) {//position對應數據源集合的索引
        return position;
}

二、解決的原理詳解:

複用緩存中獲取ViewHolder調用鏈的方法入口,源碼以下:

ViewHolder tryGetViewHolderForPositionByDeadline(int position, ...) {    
//...省略
    if (holder == null) {
        final int type = mAdapter.getItemViewType(offsetPosition);    
        if (mAdapter.hasStableIds()) {
         // 經過type 和 ItemId從 mAttachedScrap 和 mCachedViews 尋找
        holder = getScrapOrCachedViewForId(mAdapter.getItemId(offsetPosition), type, dryRun);
    }        
    if (holder == null) {
        // 沒有,那隻好 create 一個新的咯
        holder = mAdapter.createViewHolder(RecyclerView.this, type);
    }
}    
RecyclerView.ViewHolder getScrapOrCachedViewForId(long id, int type, boolean dryRun) {    
            int count = this.mAttachedScrap.size(); //先從mAttachScrap 尋找 
            for(int i = count - 1; i >= 0; --i) {
                RecyclerView.ViewHolder holder = this.mAttachedScrap.get(i);
                if (holder.getItemId() == id && !holder.wasReturnedFromScrap()) {
                    if (type == holder.getItemViewType()) {    
                        //..
                        return holder;//拿到了!
                    }    
                }
            }    
            count = this.mCachedViews.size();  //再從mCachedViews 尋找
            for(int ix = count - 1; ix >= 0; --ix) {
                RecyclerView.ViewHolder holderx = (RecyclerView.ViewHolder)this.mCachedViews.get(ix);
                if (holderx.getItemId() == id) {
                    if (type == holderx.getItemViewType()) { 
                        return holderx;//拿到了!
                    }    
                }
            }    
            return null;//沒找到,返回null
        }

源碼中,當hasStableIds()爲true,進入getScrapOrCachedViewForId(..itemId),再判斷itemId拿到緩存實例。至關於用itemId作了一個綁定,就不用從新建立和加載數據,這樣就避免了圖片閃爍。

 三、存在一個大大的坑:

由於getItemId()方法返回值是索引下標值position,當使用數據源集合裏的position的話做爲返回值的時候,由於業務邏輯集合增刪後,數據源的位置就發生了變化,這樣進入判斷itemId時不能對號入座,再通知子項刷新notifyDataSetChanged()的時候就會仍然出現閃爍。  

 


5、改變mCachedViews的緩存

由於mCachedViews默認緩存容量是 2 個。存在這裏的ViewHolder綁定的數據信息也都在,能夠直接添加到 RecyclerView 中進行顯示,不須要再次從新 onBindViewHolder()。

所以,咱們能夠經過 setViewCacheSize(int)方法改變緩存的容量大小,減小視圖綁定數據的次數。

原理:

典型的是:用空間換時間的方法。

recyclerView.setItemViewCacheSize(20);

recyclerView.setDrawingCacheEnabled(true);//保存繪圖,提升速度
//*public static final int DRAWING_CACHE_QUALITY_HIGH = 1048576;
recyclerView.setDrawingCacheQuality(View.DRAWING_CACHE_QUALITY_HIGH);

 


6、共享RecycledViewPool

由於,RecycleViewPool用來存放 mCachedViews 移除的ViewHolder。按照 Type 類型,默認對每一個Type最多緩存 5 個。重點源碼中它是被 public static 修飾,表示能夠被其餘RecyclerView 共享。

(1)嵌套RecycleView卡頓緣由

當使用多層嵌套的RecyclerView極易出現卡頓。好比在一個垂直的RecyclerView中嵌套水平的RecyclerView。

在嵌套RecyclerView中,當用戶滾動一個橫向RecycleView的時候確定沒什麼問題,也算流暢,由於它自身一套完整回收複用機制的「神功護體」。

可是,當整個列表垂直滾動時,外層的RecycleView的子項須要建立或複用吧,那麼,每個子項中的RecyclerView是否是一樣也得處理各自的回收複用機制,內外層的子項數量越龐大,內存消耗就越大,從而形成卡頓甚至,更嚴重的問題。

(2)解決嵌套RecycleView卡頓

經過調用RecyclerView.setRecycledViewPool()方法,讓每個子項裏的RecycleView在同一個RecycledViewPool裏作回收複用策略。(固然,前提是子項RecycleView的Adapter是相同的。)

/**
 * 解決雙層嵌套,共用RecycleViewPool
 */
public class OutShopAdapter extends BaseAdapter<String, RecyclerView.ViewHolder> {
    RecyclerView.RecycledViewPool mSharedPool = new RecyclerView.RecycledViewPool();
    public OutShopAdapter(Context context, List mMessages) {
        super(context, mMessages);
    }
    @Override
    protected RecyclerView.ViewHolder createViewHolder(int viewType, ViewGroup parent) {
        RecyclerView childRecycleView = new RecyclerView(context);
        childRecycleView.setRecycledViewPool(mSharedPool);
        return null;
    }
    @Override
    protected void setOnBindViewHolder(RecyclerView.ViewHolder viewHolder, int position) {    
    }
}

 


7、慣性滑動延遲加載

(1)快速滑動RecycleView卡頓緣由:

由於,列表上下滑動的時候,RecycleView會在執行復用策略,onCreateViewHolder和onBindViewHolder會執行。item視圖建立或數據綁定的方法會隨着滑動被屢次執行,容易形成卡頓。

能夠查看我第一章:(一)RecycleView 初探回收複用,onCreateView和onBindView調用關係

(2)解決快速滑動形成的卡頓

通常都採用滑動關閉數據加載優化:主要是設置RecyclerView.addOnScrollListener();經過自定義一個滑動監聽類繼承onScrollListener抽象類,實現滑動狀態改變的方法onScrollStateChanged(recycleview,state),從而實現在滑動過程當中不加載,當滾動靜止時,刷新界面,實現加載

缺點:

  • 列表只要一滾動就不加載數據;

  • 列表只要一中止滾動,就刷新數據一次;

  • 無論用戶滾動了多少,都會刷新數據。

優化:

  • 只有慣性滾動時纔不加載數據;

  • 頂部/底部不刷新數據;

  • 提升列表滑動速度。

技術難點:

  1. 如何檢測到列表是快速滾動。

  2. 如何判斷佈局是否未加載,若是已加載的就不用重複加載。

  3. 列表滑動速度如何改變。(由於是私有的成員變量 private final int mMaxFlingVelocity;)

(3)檢測慣性滑動

若是列表滾動中計算一下滾動速度,當速度大於某個值,咱們就認爲用戶快速滾動列表。

首先,使用GestureDetector.OnGestureListener的監聽onFling()方法。(不推薦)

//建立手勢
GestureDetector detector = new GestureDetector(this, new GestureDetector.SimpleOnGestureListener() {
            @Override
            public boolean onFling(MotionEvent motionEvent, MotionEvent motionEvent1, float v, float v1) {
                if (Math.abs(v1) > 8000) {//慣性值
                    simpleAdapter.setScrolling(true);
                }
                return false;
            }
});
//監聽手勢
recyclerView.setOnTouchListener(new View.OnTouchListener() {
            @Override
            public boolean onTouch(View view, MotionEvent event) {
                detector.onTouchEvent(event);
                return false;
            }
});

 其實,RecycleView內部就有慣性滑動的監聽。(推薦)

public static void setMaxFlingVelocity(RecyclerView recyclerView, final BaseAdapter adapter, final int velocity) {
        try {
            Field field = recyclerView.getClass().getDeclaredField("mMaxFlingVelocity");
            field.setAccessible(true);
            field.set(recyclerView, velocity);
        } catch (Exception e) {
            e.printStackTrace();
        }

        recyclerView.setOnFlingListener(new RecyclerView.OnFlingListener() {
            @Override
            public boolean onFling(int xv, int yv) {//xv是x方向滑動速度,yv是y方向滑動速度。    
                if (yv >= velocity) {
                    adapter.setScrolling(true);
                }else{
                    adapter.setScrolling(false);
                }
                return false;
            }
        });
 }

系統默認慣性滑動最大值mMaxFlingVelocity是8000,這個值是能夠經過反射修改的。值越大,慣性滑動距離越遠,越絲滑。所以,作了前面一套比較完善的RecycleView性能優化處理以後,就應該自信點把慣性值加倍,讓用戶體驗翻倍!

(4) 判斷是否已加載

adapter:

protected boolean scroll;   
    public boolean getScrolling(){return scroll;}    
    public void setScrolling(boolean scroll){this.scroll = scroll;} 
    @Override
    protected void setOnBindViewHolder(RecyclerView.ViewHolder viewHolder, int position) {
         ShopBean item = mlist.get(position);
         if(scroll){//未加載圖片
                ((ViewHolde) viewHolder).imageView.setImageResource(0);
         }else {//加載圖片
                Glide.with(mContext).
                        load(item.getPictureUrl())
                        .centerCrop()
                        .into(((ViewHolde) viewHolder).imageView);
          }
    }

scrollListener:

private boolean scrolled;//是否已滾動
    private BaseAdapter mAdapter;
    @Override
    public void onScrolled(RecyclerView recyclerView, int dx, int dy) {
        super.onScrolled(recyclerView, dx, dy);
        //y軸值發生改變
        if (dy != 0) { scrolled = true;}
    }
    @Override
    public void onScrollStateChanged(RecyclerView recyclerView, int newState) {
        switch (newState) {
            case RecyclerView.SCROLL_STATE_IDLE: //(靜止) 
                // 未加載數據
                if (mAdapter.getScrolling() && scrolled) {
                    mAdapter.setScrolling(false);//正常加載數據
                    mAdapter.notifyDataSetChanged();
                }
                scrolled = false;
                break;
        }
        super.onScrollStateChanged(recyclerView, newState);
    }

首先,根據一個Boolean類型變量scroll來控制ImageView是否加載圖片。 true 表示滾動中,不加載;false,中止滾動,正常顯示。默認爲false。

而後,滑動靜止加入2個判斷。

一、scroll 爲true,表示剛剛發生了「快速」滾動,如今屏幕顯示的都是未加載數據的列表項,能夠進行加載了。

二、scrolled爲true,表示剛剛列表滾動了距離。由於滑到頂部和底部,y軸滾動值爲0,容易形成重複刷新數據。

 


終於寫完了。。。。。一看時間,半夜了。。。洗洗睡咯~

相關文章
相關標籤/搜索