English | 简体中文 | 繁體中文 | Русский язык | Français | Español | Português | Deutsch | 日本語 | 한국어 | Italiano | بالعربية

Android7.0 도구 클래스: DiffUtil 설명

1. 개요

DiffUtil은 support-v7:24.2.0에서 새로운 도구 클래스가 추가되었으며, 이는 두 데이터 셋을 비교하여 기존 데이터 셋을 찾습니다-》새 데이터 셋의 최소 변화량.

데이터 셋에 대해 이야기하면, 많은 분들이 어떤 것이 관련된지 알고 계실 것입니다. 바로 제가 가장 좋아하는 RecyclerView입니다.

제가 사용한这几天간에 보면, 가장 큰 도움이 되는 것은 RecyclerView를 갱신할 때 더 이상 무의식적으로 mAdapter.notifyDataSetChanged()를 사용하지 않는 것입니다.

과거에 사용했던 mAdapter.notifyDataSetChanged()는 두 가지 단점이 있었습니다:

1. RecyclerView의 애니메이션(삭제, 추가, 이동, 변경 애니메이션)을 유발하지 않습니다.

2. 성능이 낮습니다. 왜냐하면 RecyclerView 전체를 무의식적으로 갱신했기 때문입니다. 극단적인 경우, 새로운 데이터 집합과 오래된 데이터 집합이 완전히 일치하면 효율성이 가장 낮습니다.

DiffUtil을 사용하면 다음과 같은 코드로 변경됩니다:

DiffUtil.DiffResult diffResult = DiffUtil.calculateDiff(new DiffCallBack(mDatas, newDatas), true);
diffResult.dispatchUpdatesTo(mAdapter);

그것은 자동으로 새로운 데이터 집합과 오래된 데이터 집합의 차이를 계산하고, 차이에 따라 다음 네 가지 메서드를 자동으로 호출합니다.

adapter.notifyItemRangeInserted(position, count);
adapter.notifyItemRangeRemoved(position, count);
adapter.notifyItemMoved(fromPosition, toPosition);
adapter.notifyItemRangeChanged(position, count, payload);

물론, 이 네 가지 메서드는 모두 RecyclerView의 애니메이션과 함께 실행되며, 모두 지정된 갱신 메서드이며, 갱신 효율성이 크게 향상됩니다.
구속 규칙에 따라, 먼저 그림을 올립니다.

그림 한 번은 notifyAdapterDataSetChanged()를 사용한 효과 그림입니다. 갱신 인터랙션은 매우 거칠고, Item이 갑자기 특정 위치에 나타나는 것을 볼 수 있습니다:

그림 두 번은 DiffUtils의 효과 그림입니다. 가장 명확한 것은 Item을 추가하고 이동하는 애니메이션입니다:

GIF는 약간渣이지만, 문서의 마지막 부분에서 Demo를 다운로드하여 실행하면 더 나은 효과를 볼 수 있습니다.

이 문서는 다음과 같은 내용을 포함하며, 그 이상도 포함합니다:

1 먼저 DiffUtil의 간단한 사용법을 소개하고, 갱신 시의 '증가적 갱신' 효과를 구현합니다. ('증가적 갱신'은 제가 지은 이름입니다).
2 DiffUtil의 고급 사용법은, 특정 Item의 내용(data)만 변경되고 위치(position)이 변경되지 않았을 때, 일부 갱신(공식적으로는 Partial bind, 일부 바인딩)을 완료하는 것입니다.
3 RecyclerView.Adapter에 대한 public void onBindViewHolder(VH holder, int position, List<Object> payloads) 메서드를 이해하고, 그것을 익혀야 합니다.
4 DiffResult를 서브스레드에서 계산하고, RecyclerView를 갱신하는 작업을 메인 스레드에서 수행합니다.
5 일부 사람들이 좋아하지 않는 notifyItemChanged()가 Item이 백라이트를 켜지는 애니메이션을 제거하는 방법.
6 DiffUtil 부분 클래스, 메서드 공식 설명의 한글 번역

2. DiffUtil의 간단한 사용법

이전에도 언급했듯이, DiffUtil은 RecyclerView를 갱신할 때 새로운 데이터 집합과 오래된 데이터 집합의 차이를 계산하여 RecyclerView.Adapter의 갱신 메서드를 자동으로 호출하여 효율적으로 갱신하고 Item 애니메이션 효과를 동반하는 도구입니다.

그러면 이를 공부하기 전에 일부러 준비를 해야 합니다. 먼저 일반 청년 버전을 작성하고, notifyDataSetChanged()를 사용하여 무의식적으로 갱신하는 Demo를 작성해야 합니다.

1 일반적인 JavaBean이지만 clone 메서드를 구현했으며, Demo를 통해 데이터를 갱신하는 데 사용되며, 실제 프로젝트에서는 필요하지 않습니다. 갱신할 때마다 데이터는 모두 네트워크에서 가져옵니다.

class TestBean implements Cloneable {
 private String name;
 private String desc;
 ....//get set方法省略
 //仅写DEMO 用 实现克隆方法
 @Override
 public TestBean clone() throws CloneNotSupportedException {
  TestBean bean = null;
  try {
   bean = (TestBean) super.clone();
  }
   e.printStackTrace();
  }
  return bean;
 }

2 实现一个普普通通的RecyclerView.Adapter。

public class DiffAdapter extends RecyclerView.Adapter<DiffAdapter.DiffVH> {
 private final static String TAG = "zxt";
 private List<TestBean> mDatas;
 private Context mContext;
 private LayoutInflater mInflater;
 public DiffAdapter(Context mContext, List<TestBean> mDatas) {
  this.mContext = mContext;
  this.mDatas = mDatas;
  mInflater = LayoutInflater.from(mContext);
 }
 public void setDatas(List<TestBean> mDatas) {
  this.mDatas = mDatas;
 }
 @Override
 public DiffVH onCreateViewHolder(ViewGroup parent, int viewType) {
  return new DiffVH(mInflater.inflate(R.layout.item_diff, parent, false));
 }
 @Override
 public void onBindViewHolder(final DiffVH holder, final int position) {
  TestBean bean = mDatas.get(position);
  holder.tv1.setText(bean.getName());
  holder.tv2.setText(bean.getDesc());
  holder.iv.setImageResource(bean.getPic());
 }
 @Override
 public int getItemCount() {
  return mDatas != null63; mDatas.size() : 0;
 }
 class DiffVH extends RecyclerView.ViewHolder {
  TextView tv1, tv2;
  ImageView iv;
  public DiffVH(View itemView) {
   super(itemView);
   tv1 = (TextView) itemView.findViewById(R.id.tv1);
   tv2 = (TextView) itemView.findViewById(R.id.tv2);
   iv = (ImageView) itemView.findViewById(R.id.iv);
  }
 }
}

3 액티비티 코드:

public class MainActivity extends AppCompatActivity {
 private List<TestBean> mDatas;
 private RecyclerView mRv;
 private DiffAdapter mAdapter;
 @Override
 protected void onCreate(Bundle savedInstanceState) {
  super.onCreate(savedInstanceState);
  setContentView(R.layout.activity_main);
  initData();
  mRv = (RecyclerView) findViewById(R.id.rv);
  mRv.setLayoutManager(new LinearLayoutManager(this));
  mAdapter = new DiffAdapter(this, mDatas);
  mRv.setAdapter(mAdapter);
 }
 private void initData() {
  mDatas = new ArrayList<>();
  mDatas.add(new TestBean("張旭童"1"안드로이드", R.drawable.pic1));
  mDatas.add(new TestBean("張旭童"2"자바", R.drawable.pic2));
  mDatas.add(new TestBean("張旭童"3"배고", R.drawable.pic3));
  mDatas.add(new TestBean("張旭童"4"수시 제품", R.drawable.pic4));
  mDatas.add(new TestBean("張旭童"5"수시 테스트", R.drawable.pic5));
 }
 /**
  * 시뮬레이션 리프레시 작업
  *
  * @param view
  */
 public void onRefresh(View view) {
  try {
   List<TestBean> newDatas = new ArrayList<>();
   for (TestBean bean : mDatas) {
    newDatas.add(bean.clone());//좀머리 데이터를 복사하여 새로운 데이터 소스를 모의합니다
   }
   newDatas.add(new TestBean("赵子龙", "帅", R.drawable.pic6));//데이터 추가 모의
   newDatas.get(0).setDesc("Android+);
   newDatas.get(0).setPic(R.drawable.pic7);//데이터 수정 모의
   TestBean testBean = newDatas.get(1);//데이터 이동 모의
   newDatas.remove(testBean);
   newDatas.add(testBean);
   //Adapter에 새 데이터를 제공하지 마세요.
   mDatas = newDatas;
   mAdapter.setDatas(mDatas);
   mAdapter.notifyDataSetChanged();//이전에는 대부분 이렇게 했습니다
  }
   e.printStackTrace();
  }
 }
}

매우 간단합니다. 단, 새로운 데이터 소스 newDatas를 구성할 때마다 기존 데이터 소스 mDatas를 순회하며 각 data의 clone() 메서드를 호출하여, 두 데이터 소스가 데이터가 일치하지만 메모리 주소(포인터)가 다를 것을 보장합니다. 이렇게 하면 나중에 newDatas에서 값을 변경할 때 mDatas의 값을 함께 변경하지 않습니다.

4 activity_main.xml 일부 코드를 제거했습니다. 단순히 RecyclerView와 Button을 사용하여 새로운 데이터 소스를 모의합니다.:

<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
>
 <android.support.v7.widget.RecyclerView
  android:id="@"+id/rv" />
 <Button
  android:id="@"+id/btnRefresh"
  android:layout_alignParentRight="true"
  android:onClick="onRefresh"
  android:text="캐시 레퍼시스" />
</RelativeLayout>

위는 일반 청년이 쉽게 작성할 수 있는, notifyDataSetChanged()를 사용하지 않는 demo입니다. 실행 결과는 첫 번째 장의 그림 1과 같습니다.
하지만 우리는 모두 문학적 청년이 되고 싶어 합니다, 그래서

아래에서는 DiffUtil의 간단한 사용법에 대해 이야기하겠습니다. 추가로 클래스를 작성해야 합니다.

문학적 청년이 되고 싶다면, DiffUtil.Callback를 상속받은 클래스를 구현해야 합니다. 그의 네 가지 abstract 메서드를 구현해야 합니다.
이 클래스는 Callback이라고 불리지만, 이를 다음과 같이 이해하는 것이 더 적합합니다: 새로운 Item과 기존 Item이 같은지를 비교하는 계약(Contract)과 규칙(Rule)을 정의하는 클래스입니다.

DiffUtil.Callback 추상 클래스는 다음과 같습니다:

 public abstract static class Callback {
  public abstract int getOldListSize();//旧行数据集의 크기
  public abstract int getNewListSize();//새 데이터 집합의 크기
  public abstract boolean areItemsTheSame(int oldItemPosition, int newItemPosition);//새로운 데이터 집합과 기존 데이터 집합이 같은 위치의 Item이 객체인가요? (내용이 다를 수 있으며, 여기서 true를 반환하면 아래의 메서드가 호출됩니다)
  public abstract boolean areContentsTheSame(int oldItemPosition, int newItemPosition);//이 메서드는 위의 메서드가 true를 반환할 때만 호출됩니다. 제 이해에 따르면 notifyItemRangeChanged() 만이 호출됩니다. Item의 내용이 변경되었는지�断합니다
  //이 메서드는 DiffUtil 고급 사용법에서 사용되며, 여기서는 언급하지 않을 것입니다
  @Nullable
  public Object getChangePayload(int oldItemPosition, int newItemPosition) {
   return null;
  }
 }

  이 Demo에서는 DiffUtil.Callback를 구현하였으며, 핵심 메서드에는 중국어와 영어 양쪽의 설명이 포함되어 있습니다(즉, 공식 영어 설명을 번역하여, 모두가 더 잘 이해할 수 있도록 했습니다).

/**
 * 소개: 핵심 클래스로서 새로운 Item과 기존 Item이 같은지�断하기 위해 사용됨
 * 저자: zhangxutong
 * 이메일: [email protected]
 * 시간: 2016/9/12.
 */
public class DiffCallBack extends DiffUtil.Callback {
 private List<TestBean> mOldDatas, mNewDatas;//이름을 보세요
 public DiffCallBack(List<TestBean> mOldDatas, List<TestBean> mNewDatas) {
  this.mOldDatas = mOldDatas;
  this.mNewDatas = mNewDatas;
 }
 //旧行数据集의 크기
 @Override
 public int getOldListSize() {
  return mOldDatas != null ? mOldDatas.size() : 0;
 }
 //새 데이터 집합의 크기
 @Override
 public int getNewListSize() {
  return mNewDatas != null ? mNewDatas.size() : 0;
 }
 /**
  * DiffUtil이 두 객체가 동일한 Item을 대표하는지 결정하기 위해 호출됩니다.
  * DiffUtil이 호출되어 두 객체가 동일한 Item인지�断합니다.
  * 예를 들어,如果你的items가 유일한 ids를 가지고 있다면, 이 메서드는 그들의 id 일치성을 확인해야 합니다.
  * 예를 들어,如果你的Item가 유일한 id 필드를 가지고 있다면, 이 메서드는 id가 일치하는지�断합니다.
  * 이 예제는 name 필드가 일치하는지�断합니다
  *
  * @param oldItemPosition 기존 목록에서 item의 위치
  * @param newItemPosition 새로운 목록에서 item의 위치
  * @return 두 항목이 동일한 객체를 대표하면 참을 반환하고, 다르다면 거짓을 반환합니다.
  */
 @Override
 public boolean areItemsTheSame(int oldItemPosition, int newItemPosition) {
  return mOldDatas.get(oldItemPosition).getName().equals(mNewDatas.get(newItemPosition).getName());
 }
 /**
  * DiffUtil이 두 항목이 동일한 데이터인지 확인하고자 할 때 호출됩니다.
  * DiffUtil이 호출하여 두 item이 동일한 데이터를 가지고 있는지 확인하는 데 사용됩니다.
  * DiffUtil은 이 정보를 사용하여 item의 내용이 변경되었는지 검사합니다.
  * DiffUtil은 반환된 정보(true false)를 사용하여 현재 item의 내용이 변경되었는지 검사합니다.
  * DiffUtil은 이 메서드를 {@link Object#equals(Object)} 대신 사용하여 동등성을 확인합니다.
  * DiffUtil은 이 메서드를 equals 메서드 대신 사용하여 동등성을 확인합니다.
  * 그래서 UI에 따라 행동을 변경할 수 있습니다.
  * 따라서 UI에 따라 반환 값을 변경할 수 있습니다.
  * 예를 들어, DiffUtil을 RecyclerView.Adapter와 함께 사용할 때,
  * {@link android.support.v7.widget.RecyclerView.Adapter RecyclerView.Adapter}, you should
  * items의 시각적 표현이 같은지 반환합니다.
  * 예를 들어, RecyclerView.Adapter와 DiffUtil을 함께 사용할 때, Item의 시각적 표현이 같은지 반환해야 합니다.
  * 이 메서드는 {@link #areItemsTheSame(int, int)}가 반환할 때만 호출됩니다.
  * {@code true} for these items.
  * 이 메서드는 areItemsTheSame()가 true를 반환할 때만 호출됩니다.
  * @param oldItemPosition 기존 목록에서 item의 위치
  * @param newItemPosition The position of the item in the new list which replaces the
  *      oldItem
  * @return True if the contents of the items are the same or false if they are different.
  */
 @Override
 public boolean areContentsTheSame(int oldItemPosition, int newItemPosition) {}}
  TestBean beanOld = mOldDatas.get(oldItemPosition);
  TestBean beanNew = mNewDatas.get(newItemPosition);
  if (!beanOld.getDesc().equals(beanNew.getDesc())) {
   return false;//내용이 다를 경우 false를 반환합니다
  }
  if (beanOld.getPic() != beanNew.getPic()) {
   return false;//내용이 다를 경우 false를 반환합니다
  }
  return true; //기본적으로 두 개의 data 내용은 동일합니다
 }

이렇게 상세한 주석을 달았습니다+간단한 코드이며, 한 눈에 이해할 수 있습니다.

사용할 때는 이전에 작성한 notifyDatasetChanged() 메서드를 주석 처리하고 다음 코드로 대체하세요:

//문학적 청년의 신선한 선택
//DiffUtil.calculateDiff() 메서드를 사용하여 규칙 DiffUtil.Callback 객체를传入하고, Item 이동을 검사할지 여부를 boolean 변수로传入하여 DiffUtil.DiffResult 객체를 얻습니다
DiffUtil.DiffResult diffResult = DiffUtil.calculateDiff(new DiffCallBack(mDatas, newDatas), true);
//DiffUtil.DiffResult 객체의 dispatchUpdatesTo() 메서드를 사용하여 RecyclerView의 Adapter를传入하면 쉽게 문학적 청년이 될 수 있습니다
diffResult.dispatchUpdatesTo(mAdapter);
//Adapter에 새 데이터를 제공하지 마세요.
mDatas = newDatas;
mAdapter.setDatas(mDatas);

설명:

단계 1

newDatas를 Adapter에 설정하기 전에 먼저 DiffUtil.calculateDiff() 메서드를 호출합니다:새로운 데이터 집합과 오래된 데이터 집합을 변환한 최소 업데이트 집합은 다음과 같습니다:

DiffUtil.DiffResult 객체.
DiffUtil.calculateDiff() 메서드는 다음과 같이 정의되어 있습니다:
첫 번째 매개변수는 DiffUtil.Callback 객체입니다.
두 번째 매개변수는 Item의 이동을 검사할지 여부를 나타냅니다. false로 설정하면 알고리즘의 효율성이 높아지며, 필요에 따라 설정합니다. 여기서는 true입니다.

public static DiffResult calculateDiff(Callback cb, boolean detectMoves)

계단 두 번째

그런 다음 DiffUtil.DiffResult 객체의 dispatchUpdatesTo() 메서드를 사용하여 RecyclerView의 Adapter를 전달하여 일반 젊은이들이 사용하는 mAdapter.notifyDataSetChanged() 메서드를 대체합니다.

소스 코드를 확인하면 이 메서드는 내부적으로 adapter의 네 가지 지정된 업데이트 메서드를 호출하는 것을 알 수 있습니다.

 public void dispatchUpdatesTo(final RecyclerView.Adapter adapter) {
   dispatchUpdatesTo(new ListUpdateCallback() {
    @Override
    public void onInserted(int position, int count) {
     adapter.notifyItemRangeInserted(position, count);
    }
    @Override
    public void onRemoved(int position, int count) {
     adapter.notifyItemRangeRemoved(position, count);
    }
    @Override
    public void onMoved(int fromPosition, int toPosition) {
     adapter.notifyItemMoved(fromPosition, toPosition);
    }
    @Override
    public void onChanged(int position, int count, Object payload) {
     adapter.notifyItemRangeChanged(position, count, payload);
    }
   });
  }

결론:

따라서 DiffUtil은 RecyclerView와 함께만 사용할 수 없습니다. ListUpdateCallback 인터페이스의 네 가지 메서드를 직접 구현하여 일을 할 수 있습니다. (제가 임의로 생각해보았을 때, 나의 프로젝트에 있는 나인격 컨트롤과 함께 사용할 수 있을까요? 또는 제가 작성한 NestFullListView를 최적화할 수 있을까요? 추천, ListView, RecyclerView, ScrollView에서 ListView를 내장하는 상대적으로 아름다운 해결책을 확인하세요: http://blog.csdn.net/zxt0601/article/details/52494665)

이제 우리는 문예 청년이 되었습니다. 실행 결과는 첫 번째 절의 그림 두 번째와 거의 일치합니다.

단 하나의 차이는 이때 adapter.notifyItemRangeChanged()는 Item이 빛나는 업데이트 애니메이션을 가질 것입니다. (이 문서의 Demo의 position이 0인 item). 이 Item이 빛나는 애니메이션은 누군가에게는 좋고, 누군가에게는 싫을 수 있지만, 모두 중요하지 않습니다.

우리가 제3절의 DiffUtil의 기본 사용법을 배웠다면, 이 ItemChange 애니메이션을 좋아해도, 싫어해도, 그것은 모두 없어질 것입니다. (오히려 공식 버그일 수도 있습니다)
효과는 첫 번째 절의 그림 두 번째와 같습니다. 우리의 item0은 이미지와 텍스트 모두가 변경되었지만, 이 변경은 어떤 애니메이션도 따르지 않았습니다.

문예 청년 속의 문예 청년으로 나아가겠습니다.

3. DiffUtil의 고급 사용법

이론:

고급 사용법은 두 가지 메서드만이涉及到합니다,
DiffUtil.Callback의
public Object getChangePayload(int oldItemPosition, int newItemPosition) 메서드
반환하는 Object는 Item이 어떤 내용을 변경했는지를 나타냅니다.

RecyclerView.Adapter의
public void onBindViewHolder(VH holder, int position, List<Object> payloads) 메서드
정향적 갱신을 완료합니다。(문예 청년 속의 문예 청년, 문예 청년 청년。)
노트를 쳐두세요, 이는 새로운 메서드입니다. 앞 두 개의 매개변수는 잘 알고 있지만, 세 번째 매개변수는 getChangePayload()에서 반환한 Object를 포함하고 있습니다.

그럼 이 메서드가 누구인지 먼저 보겠습니다:

v7-24.2.0의 소스 코드에서는 이렇게 보입니다:

 /**
   * 리사이클러뷰가 지정된 위치에 데이터를 표시하기 위해 호출됩니다. 이 메서드
   * should update the contents of the {@link ViewHolder#itemView} to reflect the item at
   * given position.
   * <p>
   * Note that unlike {@link android.widget.ListView}, RecyclerView will not call this method
   * 이미지가 변경되지 않는 한) 이 위치에서만
   * 무효화되었거나 새 위치를 결정할 수 없습니다. 이 이유로, 데이터 셋에서 아이템의 위치가 변경되면 다시만
   * use the <code>position</코드> 매개변수를 얻는 동안 관련 데이터 아이템 내부
   * 이 메서드를 저장하지 마시고 나중에 아이템의 위치가 필요하다면
   * 예를 들어 클릭 리스너에서) 사용할 때, {@link ViewHolder#getAdapterPosition()}을 사용하여
   * 업데이트된 Adapter 위치를 가지고 있습니다.
   * <p>
   * 부분 바인딩 대비 전체 바인딩:
   * <p>
   * payloads 매개변수는 {@link #notifyItemChanged(int, Object)} 또는
   * {@link #notifyItemRangeChanged(int, int, Object)}. 만약 payloads 목록이 비어 있지 않으면,
   * 현재 ViewHolder는 오래된 데이터에 바인딩되어 있으며 Adapter가 효율적인 부분 실행을 할 수 있습니다.
   * update using the payload info. If the payload is empty, Adapter must run a full bind.
   * 데이터를 업데이트하는 데 페이로드 정보를 사용하여야 합니다. 페이로드가 비어 있으면, Adapter는 전체 바인드를 실행해야 합니다.
   * onBindViewHolder(). 예를 들어, 뷰가 화면에 연결되지 않았을 때, Adapter는 notify 메서드에 전달된 페이로드가 받아지기를 기대할 수 없습니다.
   * notifyItemChange()에서 페이로드로 전달된 페이로드가 단순히 버려질 것입니다.
   *
   * @param holder 업데이트되어야 하며, 해당 내용을 나타내는 ViewHolder입니다.
   *    데이터 셋에서 주어진 위치에 있는 아이템.
   * @param position 아이템이 어댑터의 데이터 셋 내에서의 위치입니다.
   * @param payloads 비어 있지 않은-합쳐진 페이로드의 목록이 null입니다. 전체 바인드가 필요하면 비어 있는 목록일 수 있습니다.
   *     업데이트.
   */
  public void onBindViewHolder(VH holder, int position, List<Object> payloads) {
   onBindViewHolder(holder, position);
  }

그렇다면 이 메서드 내부에서는 단지 두 개의 인자를 가진 onBindViewHolder(holder, position)를 호출하는 것으로 밝혀졌습니다. (간단한 말로, 아이고야, 제 NestFullListView의 Adapter도 이런 쓰기 방식과 유사합니다. 나는 Google大神에게 더 가까워졌네요.)

이렇게 해서 이해했는데, 실제로 onBind의 진입점은 이 메서드이고, 이 메서드가 onCreateViewHolder와 일치하는 메서드입니다.
소스 코드를 몇 줄 아래로 내려가면 public final void bindViewHolder(VH holder, int position)라는 메서드가 있고, 이 메서드는 세 개의 인자를 가진 onBindViewHolder를 내부에서 호출한다는 것을 알 수 있습니다.
RecyclerView.Adapter에 대해도 단순히 몇 마디로 설명할 수는 없습니다。(실제로는 여기까지만 이해했습니다)
이제 주제에서 벗어나, 우리의 세 파라미터의 onBindViewHolder(VH holder, int position, List<Object> payloads)에 돌아갑니다. 이 메서드의 헤더에는 많은 영어 주석이 있으며, 저는 이 주석을 읽어 이 메서드를 이해하는 데 매우 유용하다고 생각했습니다. 그래서 번역을 해보았습니다,

번역:

RecyclerView가 호출하여 지정된 위치에 데이터를 표시하는 데 사용됩니다。
이 메서드는 ViewHolder 내의 ItemView의 내용을 업데이트하여 주어진 위치의 아이템의 변화를 반영해야 합니다。
ListView와는 달리, 주어진 위치의 아이템의 데이터 셋이 변경되면 RecyclerView는 이 메서드를 다시 호출하지 않습니다. 아이템 자체가 만료되었거나 (invalidated) 새 위치를 결정할 수 없는 경우를 제외합니다。
이 이유로, 이 메서드에서는 position 파라미터만 사용하여 관련 데이터 아이템을 가져오고, 이 데이터 아이템의 사본을 유지해서는 안 됩니다。
어떤 시점에서 이 아이템의 position이 필요하다면 예를 들어 clickListener를 설정할 때 ViewHolder.getAdapterPosition()을 사용해야 합니다. 이는 업데이트된 위치를 제공합니다。
(한 글자씩 쓰는 저는 여기서 이것이 두 파라미터의 onBindViewHolder 메서드를 설명하는 것임을 발견했습니다)
아래는 이 세 파라미터 메서드의 독특한 부분입니다:()
**비교: 부분(partial) 바인딩**비교: 완전(full) 바인딩
payloads 파라미터는 notifyItemChanged(int, Object) 또는 notifyItemRangeChanged(int, int, Object)에서 얻은 병합된 리스트입니다。
payloads 리스트가 비어 있지 않다면, 현재 오래된 데이터를 바인딩한 ViewHolder와 Adapter는 payload의 데이터를 사용하여 효율적인 부분적 업데이트를 수행할 수 있습니다。
payload가 비어 있다면, Adapter는 완전한 바인딩을 한 번 수행해야 합니다(두 파라미터 메서드 호출)。
Adapter는 notifyxxxx 알림 메서드를 통해 전달된 payload가 항상 onBindViewHolder() 메서드에서 받을 것이라고 가정해서는 안 됩니다。(이 문장을 잘 번역하지 못했습니다 QAQ 예시를 참고하세요)
예를 들어, View가 스크린에 attached되지 않았을 때, notifyItemChange()에서 온 payload은 간단히 버리면 됩니다.
payloads 객체는 null이 아니지만, 비어 있을 수 있습니다(비어 있음), 이 경우 전체 바인딩이 필요합니다(따라서 메서드 내에서 isEmpty을 판단하면 됩니다. 중복으로 비어 있는지 판단할 필요는 없습니다).
저자의 말: 이 메서드는 효율적인 메서드입니다. 저는 효율적인 번역자가 아니라, 저는40+분. 그렇게 해서 중요한 부분이 강조되어 보이는 것을 깨달았습니다.

실전:

많은 말을 하지만, 실제 사용은 매우 간단합니다:
getChangePayload() 메서드를 사용하는 방법을 알아보겠습니다. 중국어와 영어 양쪽에 설명이 포함되어 있습니다.

  

 /**
  * 두 항목에 대해 {@link #areItemsTheSame(int, int)}이 true를 반환하면, 이 두 항목에 대해
  * 이 두 항목에 대해 {@link #areContentsTheSame(int, int)}이 false를 반환하면, DiffUtil
  * 변경에 대한 payload을 얻기 위해 이 메서드를 호출합니다.
  * 
  * 当{@link #areItemsTheSame(int, int)}이 true를 반환하며, {@link #areContentsTheSame(int, int)}이 false를 반환할 때, DiffUtils는 이 메서드를 콜백합니다.
  * 이 Item이 변경된 payload을 얻기 위해 사용합니다.
  * 
  * 예를 들어, DiffUtil과 {@link RecyclerView}를 사용하면, 해당 Item의 변경된 payload을 반환할 수 있습니다.
  * Item에서 변경된 특정 필드와 당신의
  * {@link android.support.v7.widget.RecyclerView.ItemAnimator ItemAnimator}을 사용하여 해당 정보를 사용할 수 있습니다.
  * 정확한 애니메이션을 실행하기 위한 정보.
  * 
  * 예를 들어, RecyclerView와 DiffUtils를 함께 사용하면, 이 Item이 변경된 필드를 반환할 수 있습니다.
  * {@link android.support.v7.widget.RecyclerView.ItemAnimator ItemAnimator}을 사용하여 올바른 애니메이션을 실행할 수 있는 정보는 무엇인가요?
  * 
  * 기본 구현은 {@code null}을 반환합니다.  * 기본 구현은 null을 반환합니다
  *
  * @param oldItemPosition 기존 목록에서 item의 위치
  * @param newItemPosition 새로운 목록에서 item의 위치
  * @return 두 item 간의 변경 사항을 나타내는 payload 객체.
  * 새로운 item과 기존 item의 변경된 내용을 나타내는 payload 객체를 반환합니다.
  */
 @Nullable
 @Override
 public Object getChangePayload(int oldItemPosition, int newItemPosition) {
  //이 메서드를 구현하면 문학적 청년 중의 문학적 청년이 될 수 있습니다
  // 지정된 갱신 중의 일부 갱신
  // 효율성이 가장 높습니다
  //아니면 ItemChange의 백라이트 애니메이션이 사라졌습니다. (물론 제게는 중요하지 않습니다)
  TestBean oldBean = mOldDatas.get(oldItemPosition);
  TestBean newBean = mNewDatas.get(newItemPosition);
  //이곳에서는 핵심 필드를 비교하지 않습니다, 반드시 일치합니다
  Bundle payload = new Bundle();
  if (!oldBean.getDesc().equals(newBean.getDesc())) {
   payload.putString("KEY_DESC", newBean.getDesc());
  }
  if (oldBean.getPic() != newBean.getPic()) {
   payload.putInt("KEY_PIC", newBean.getPic());
  }
  if (payload.size() == 0)//변화가 없으면 공백을 전달합니다
   return null;
  return payload;//
 }

간단히 말해서, 이 메서드는 Object 타입의 payload를 반환하며, 특정 item의 변경된 내용이 포함되어 있습니다.
여기서는 이 변화를 저장하기 위해 Bundle를 사용합니다.

Adapter에서는 다음과 같이 세 파라미터의 onBindViewHolder를 재정의합니다:}}

 @Override
 public void onBindViewHolder(DiffVH holder, int position, List<Object> payloads) {
  if (payloads.isEmpty()) {
   onBindViewHolder(holder, position);
  } else {
   //문학적 청년들 중에서 문학적 청년
   Bundle payload = (Bundle) payloads.get(0);
   TestBean bean = mDatas.get(position);
   for (String key : payload.keySet()) {
    switch (key) {
     case "KEY_DESC":
      //여기서 payload의 데이터를 사용할 수 있지만, data도 새로워서 사용할 수도 있습니다.
      holder.tv2.setText(bean.getDesc());
      break;
     case "KEY_PIC":
      holder.iv.setImageResource(payload.getInt(key));
      break;
     default:
      break;
    }
   }
  }
 }

여기서 전달된 payloads는 List입니다. 주석을 통해 알 수 있듯이 null이 아니므로, empty인지 여부를 판단합니다.
empty이면, 두 파라미터의 함수를 호출하여一次 Full Bind를 수행합니다.
empty가 아니면, partial bind를 수행합니다.
getChangePayload 메서드에서 반환한 payload을 인덱스 0에서 꺼내고, payload의 키를 순회하며 키를 검색하면, payload에 해당하는 변경 사항이 포함되어 있으면 해당 변경 사항을 꺼내고 ItemView에 업데이트합니다.
(여기서 mDatas를 통해 얻는 것도 최신 데이터 소스의 데이터이므로, payload의 데이터나 새로운 데이터의 데이터를 사용하여 업데이트할 수 있습니다.)

이제 우리는 RecyclerView를 새로 고칠 수 있는 방법을 완전히 이해했습니다. 문학적 청년들 중에서 가장 문학적이고 예술적인 방법입니다.

4. 서브 스레드에서 DiffUtil 사용

DiffUtil 소스 코드의 상단 주석에서 DiffUtil에 대한 정보가 설명되어 있습니다.
DiffUtil이 Eugene W. Myers의 difference 알고리즘을 사용하지만, 이 알고리즘은 이동하는 item을 감지할 수 없기 때문에, Google은 이를 기반으로 개선하여 이동하는 프로젝트를 감지하는 것을 지원하지만, 이동하는 프로젝트를 감지하는 것은 성능이 더 많이 소모됩니다.
在有1000개 데이터 항목이 있습니다。200개 변경 사항에서, 이 알고리즘의 소요 시간은:
이동 감지를 시작했을 때: 평균 값:27.07ms, 중위수:26.92ms.
이동 감지를 종료했을 때: 평균 값:13.54ms, 중위수:13.36ms.
의미 있는 것은 소스 코드의 상단에 있는 주석을 읽어보는 것입니다.
만약 우리의 리스트가 너무 크다면, DiffResult를 계산하는 시간이 꽤 오래 걸립니다. 따라서 DiffResult를 가져오는 과정을 서브스레드에 두고, RecyclerView를 메인스레드에서 업데이트해야 합니다.

이곳에서 Handler와 DiffUtil을 함께 사용합니다:

코드는 다음과 같습니다:

 private static final int H_CODE_UPDATE = 1;
 private List<TestBean> mNewDatas;//newList를 임시로 저장할 변수 추가
 private Handler mHandler = new Handler() {
  @Override
  public void handleMessage(Message msg) {
   switch (msg.what) {
    case H_CODE_UPDATE:
     //Result 추출
     DiffUtil.DiffResult diffResult = (DiffUtil.DiffResult) msg.obj;
     diffResult.dispatchUpdatesTo(mAdapter);
     //Adapter에 새 데이터를 제공하지 마세요.
     mDatas = mNewDatas;
     mAdapter.setDatas(mDatas);
     break;
   }
  }
 });
   new Thread(new Runnable() {
    @Override
    public void run() {
     //DiffResult 계산을 서브스레드에서 수행합니다
     DiffUtil.DiffResult diffResult = DiffUtil.calculateDiff(new DiffCallBack(mDatas, mNewDatas), true);
     Message message = mHandler.obtainMessage(H_CODE_UPDATE);
     message.obj = diffResult;//obj에 DiffResult 저장
     message.sendToTarget();
    }
   }).start();

Handler의 사용법은 간단합니다. 더 이상 설명하지 않겠습니다.

결론과 다른

1 이 문서의 코드 양은 많지 않습니다. Demo를 다운로드하여 확인하세요. 총 네 개의 클래스입니다.
하지만 무의식적으로 이렇게 길게 썼습니다. 주로 소스 코드 주석의 번역을 포함하여, 더 나은 이해를 돕기 위해 작성되었습니다.

2 DiffUtil은下拉刷新 이런 시나리오에 적합합니다.
업데이트 효율성이 향상되었으며, 애니메이션도 있으며, 그리고~ 머리를 사용하지 않아도 됩니다.
하지만 단순히 삭제, 좋아요와 같은 작업을 수행하는 경우에는 DiffUtils를 사용하지 않아도 됩니다. 자신이 postion을 기억하고, postion이 스크린 안에 있는지 확인하여 몇 가지 지정된 새로운 데이터를 업데이트하는 메서드를 호출하면 됩니다.

3 실제로 DiffUtil은 RecyclerView.Adapter와 함께 사용할 수 있는 것이 아닙니다.
ListUpdateCallback 인터페이스를 직접 구현하여 DiffUtil을 사용하여 새로운 및 구 데이터 셋의 최소 차이 집합을 찾아 더 많은 일을 할 수 있습니다.

4 DEMO를 작성할 때, 비교하기 위한 새로운 및 구 데이터 셋이 ArrayList가 다를 뿐만 아니라, 각 data도 다르여야 합니다. 그렇지 않으면 changed가 트리거되지 않습니다.
실제 프로젝트에서는 찾을 수 없습니다. 새로운 데이터는 대부분 네트워크에서 옵니다.

5 오늘은 중추절의 마지막 날이었지만, 우리 회사는 출근하기 시작했습니다!!! 분노之余,나는 DiffUtil을 짜서, DiffUtil을 사용하지 않아도 회사와 다른 회사의 차이를 쉽게 비교할 수 있습니다. QAQ, 그리고 오늘 상태가 좋지 않았지만,8시간이 걸려 완성되었습니다. 이 문서가 짧은 글집에 들어갈 것이라 생각했지만, 예상치 못하게 길었습니다. 인내심이 없다면 DEMO를 다운로드하여 확인해 보세요. 코드 양은 많지 않지만 사용하기는 쉽습니다.

github 전송문:
https://github.com/mcxtzhang/DiffUtils

이上是 Android에 대한7.0 도구 클래스 DiffUtil의 자료 정리, 이후 추가 자료를 계속 보충할 것입니다. 감사합니다.

좋아하는 것