目錄java
前言緩存
(1)卡頓緣由:網絡
(2)優化提案:ide
(2)處理刷新閃爍問題大數據
前言
RecycleView 是一個可回收複用的列表控件,有着極高的靈活性,能實現ListView、GridView的全部功能。在平常開發中,RecycleView承擔着重要的做用,像是淘寶、京東等電商APP都會有商品列表的展現,能夠說與用戶體驗是緊密相連,其重要程度不言而喻。若是RecycleView使用不當將會影響到應用的總體性能拉低,所以它是性能優化系列中「卡頓優化」的重點。
Android性能優化(一)閃退治理、卡頓優化、耗電優化、APK瘦身
本篇,單獨瞭解一下若是對RecycleView 進行性能提高、卡頓優化。
推薦閱讀
(一)RecycleView 初探回收複用,onCreateView和onBindView調用關係
(二)RecycleView 實現吸附小標題的Demo(附源碼)
(四)RecycleView 滑動到置頂、Adapter局部刷新
1、RecycleView 性能提高
RecyclerView自身有一套完整的緩存機制,很是優秀,對於簡單的數據列通常不會有任何問題。但仍然存在不足之處。好比,不能根據滑動狀態自行調節數據綁定。遇到開發一些相似商城的應用,當展現大量的商品圖片的時候,快速滑動商品列表頁面,或頻繁增刪數據的時候,都頗有可能形成列表的卡頓。那麼,形成卡頓的緣由到底是什麼呢?
(1)卡頓緣由:
- 界面設計不合理,佈局層級嵌套太多,過分繪製。
- bindViewHolder中業務邏輯複雜,數據計算及類型轉換等耗時。
- 界面數據改變,一味的全局刷新,致使閃屏卡頓。
- 快速滑動列表,數據加載緩慢。
(2)優化提案:
- 佈局、繪製優化。
- 視圖綁定與數據處理分離。
- notifyxxx()局部刷新。
- 加大RecyclerView.mCachedViews的緩存。
- 共享RecycledViewPool 。
- 慣性滑動延遲加載。
2、佈局、繪製優化
老生常談的優化方案。就不過多贅述哦~
由於View的測量、佈局和繪製是經過遍從來進行操做的,若是佈局層級太多極易形成卡頓(官方建議不超過10層)。
能夠考慮自定義ViewGroup、<ViewStub>延遲View加載、<merge>標籤等方式減小層級;
多層次重疊的 UI 結構中移除底層背景減小過分繪製;
從而提升UI渲染的效率。
3、視圖綁定與數據處理分離
onBindViewHolder()就是RecyclerView對item視圖進行數據綁定的方法。
由於,RecyclerView的onB
indViewHolder()
方法是在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視圖建立或數據綁定的方法會隨着滑動被屢次執行,容易形成卡頓。
(2)解決快速滑動形成的卡頓
通常都採用滑動關閉數據加載優化:主要是設置
RecyclerView.addOnScrollListener();經過自定義一個滑動監聽類繼承onScrollListener抽象類,實現
滑動狀態改變的方法
onScrollStateChanged(recycleview,state),從而實現
在滑動過程當中不加載,當滾動靜止時,刷新界面,實現加載。
缺點:
列表只要一滾動就不加載數據;
列表只要一中止滾動,就刷新數據一次;
無論用戶滾動了多少,都會刷新數據。
優化:
只有慣性滾動時纔不加載數據;
頂部/底部不刷新數據;
提升列表滑動速度。
技術難點:
如何檢測到列表是快速滾動。
如何判斷佈局是否未加載,若是已加載的就不用重複加載。
列表滑動速度如何改變。(由於是私有的成員變量 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,容易形成重複刷新數據。
終於寫完了。。。。。一看時間,半夜了。。。洗洗睡咯~