記一次列表載入超一萬條資料優化

guosen0612發表於2018-12-19

Android中列表是每個應用都會有的UI效果,而與使用者的互動無非就是使用者上下滑動、左右滑動、點選item 等等,本文就從小編遇到一次載入大量資料而影響體驗優化之旅。

專案的列表採用RecycleView + BaseMultiItemQuickAdapter 分組效果,資料量10000~20000以上

資料拉取、快取

首先是資料的獲取方式,分頁?還是全部獲取? 這得考慮到後端的查詢效率,資料庫可以採用建立索引,提高效率,前端可以採用分頁獲取,並且為了提高體驗,當然是儲存在本地資料庫了(NoSql),這裡就有一個問題分頁獲取分頁 再儲存,資料庫儲存會導致阻塞執行緒,

果然出現了:Message Blocked in Main Thread XXXX ms,這是anr日誌,說明,執行了耗時任務,UI 無響應。。。。。。。。。。 所以就想到了 這裡採用了執行緒控制儲存(建議採用執行緒池的方式來實現,不然物極必反,過多建立執行緒,浪費了資源。。。)

public CustomerReposity(){
      mThreadPool = new ThreadPoolExecutor(3, 5,
              10, TimeUnit.SECONDS, new LinkedBlockingDeque<Runnable>(128));
  }
複製程式碼
 mThreadPool.execute(new Runnable() {
                          @Override
                          public void run() {
                              Log.d("customer","thread save to db ::"+data.list.size());
                              saveCustomerToLocalDB(data.list, pageNumer == 1);
                          }
                      });

複製程式碼

從後端拉取到解析-再到資料庫的時間

這邊其實考慮到序列化,序列化有兩種 Serializable\Parcelable,Serializable 採用反射機制,會導致大量物件產生,所以,考慮到效能的話,採用Parcelable

  @Override
  public void writeToParcel(Parcel out, int i) {
      out.writeString(cid);
      pingYin = in.readString();

  }

  public static final Parcelable.Creator<Customer> CREATOR = new Parcelable.Creator<Customer>() {
      @Override
      public Customer createFromParcel(Parcel source) {
          return new Customer(source);
      }

      @Override
      public Customer[] newArray(int size) {
          return new Customer[size];
      }
  };

複製程式碼

總不能全部載入完畢才顯示給使用者看吧?

剛開始採用了資料監聽(LiveData的方式,專案架構本身採用MVVM的方式)


 mReposity.getCustomerDefaultTeam().observe(this, new Observer<List<Customer>>() {
          @Override
          public void onChanged(@Nullable List<Customer> customers) {
              mAdapter.setNewData(customers);
          }
      });

複製程式碼

又會有問題:資料邊載入邊重新整理??? 列表一直notifyDataChanged.........這邊採用preFetch思想,先給使用者展示部分資料

RecycleView 優化

佈局優化

也就是item 佈局,減少層級,這樣就避免多次繪製UI,如果 Item 高度是固定的話,可以使用 RecyclerView.setHasFixedSize(true); 來避免 requestLayout 浪費資源,如果不要求動畫,可以通過 ((SimpleItemAnimator) rv.getItemAnimator()).setSupportsChangeAnimations(false); 把預設動畫關閉來提神效率,通過 getExtraLayoutSpace 來增加 RecyclerView 預留的額外空間(顯示範圍之外,應該額外快取的空間),如下所示

mViewDataBinding.rvCustomers.setLayoutManager(new LinearLayoutManager(this){
         @Override
         protected int getExtraLayoutSpace(RecyclerView.State state) {
             return 100;
         }
     });
     mViewDataBinding.rvCustomers.setAdapter(mAdapter);
     mViewDataBinding.rvCustomers.setHasFixedSize(true);
     mViewDataBinding.rvCustomers.setItemViewCacheSize(20);
     ((SimpleItemAnimator) mViewDataBinding.rvCustomers.getItemAnimator()).setSupportsChangeAnima  mAdapter.expandAll();
     mCustomerReposity = CustomerReposity.getmInstances();

複製程式碼

可參考資料來源:blankj.com/2018/09/29/…

List Remove效率

在頁面點選展開分組、收縮分組的時候發現,展開的時候很順利,收縮的時候即卡頓個幾秒,,看下日誌ANR。。。。。,檢視  BaseQuickAdapter  原始碼
複製程式碼
public int collapse(@IntRange(from = 0) int position, boolean animate, boolean notify) {
       position -= getHeaderLayoutCount();

       IExpandable expandable = getExpandableItem(position);
       if (expandable == null) {
           return 0;
       }
       int subItemCount = recursiveCollapse(position);
       expandable.setExpanded(false);
       int parentPos = position + getHeaderLayoutCount();
       if (notify) {
           if (animate) {
               notifyItemChanged(parentPos);
               notifyItemRangeRemoved(parentPos + 1, subItemCount);
           } else {
               notifyDataSetChanged();
           }
       }
       return subItemCount;
   }
複製程式碼

因此分析 原因估計是出在recursiveCollapse(position)這個方法 ,繼續往下看


    @SuppressWarnings("unchecked")
    private int recursiveCollapse(@IntRange(from = 0) int position) {
        T item = getItem(position);
        if (!isExpandable(item)) {
            return 0;
        }
        IExpandable expandable = (IExpandable) item;
        int subItemCount = 0;
        if (expandable.isExpanded()) {
            List<T> subItems = expandable.getSubItems();
            if (null == subItems) return 0;

            for (int i = subItems.size() - 1; i >= 0; i--) {
                T subItem = subItems.get(i);
                int pos = getItemPosition(subItem);
                if (pos < 0) {
                    continue;
                }
                if (subItem instanceof IExpandable) {
                    subItemCount += recursiveCollapse(pos);
                }
                mData.remove(pos);
                subItemCount++;
            }
        }
        return subItemCount;
    }

複製程式碼

看了下程式碼好像沒做什麼,就是刪除分組下的item,就想到了,我一個分組有幾十W+的item,它這樣remove,是不是時間花太久了??????》〉》〉 果然 remove 方法執行耗時了。。。。。。開始改造



    /**
     * 改造原始碼****###
     * @param position
     * @return
     */
    private int recursiveCollapse(@IntRange(from = 0) int position) {
        MultiItemEntity item = getItem(position);
        if (!isExpandable(item)) {
            return 0;
        }
        IExpandable expandable = (IExpandable) item;
        if (expandable.isExpanded()) {
            List<MultiItemEntity> subItems = expandable.getSubItems();
            mData = removeAllChildList(mData,subItems);
        }
        return expandable.getSubItems().size();
    }

    public int collapsew(@IntRange(from = 0) int position, boolean animate) {
        return collapses(position, animate, true);
    }


    /**
     * 替代List.removeAll()的方法提高效率
     * @param source
     * @param destination
     * @return
     */
    public List<MultiItemEntity> removeAllChildList(List<MultiItemEntity> source, List<MultiItemEntity> destination) {
        List<MultiItemEntity> afterRemove = new LinkedList<MultiItemEntity>();
        Set<MultiItemEntity> destinaToRemove = new HashSet<MultiItemEntity>(destination);
        for (MultiItemEntity t : source) {
            if (!destinaToRemove.contains(t)) {
                afterRemove.add(t);
            }
        }
        return afterRemove;
    }
```java

# 部落格:http://guosen.github.io/

複製程式碼

相關文章