ListAdapter
,DiffUtil
를 사용했습니다RecyclerView.Adapter
의 notifyDataSetChanged
는 모든 데이터를 변경하라는 의미이기 때문에 변경되지 않은 데이터도 변경시켜버리니 성능에 안좋다! 라는 것만 알고 습관적으로 ListAdapter
와 DiffUtil
을 사용했습니다🤣RecyclerView
의 관점에서notifyDataSetChanged
의 정확한 문제점이 무엇인지 공부해보겠습니다notifyDataSetChanged
는 RecyclerView.Adapter가 가진 public 메서드입니다. 주석을 읽어보면 등록된 관찰자에게 데이터 집합이 변경되었음을 알리는 메서드 라고 되어있습니다.notifyChanged
메서드를 보니 반복문을 통해 관찰자로부터 모든 item에 변경 이벤트가 생겼음을 알리고 있습니다notifyDataSetChanged
는 데이터 집합이 변경된 내용을 지정하지 않으므로 관찰자는 모든 기존 항목과 구조가 더 이상 유효하지 않다고 가정합니다.(데이터 전부 바꼈고 아이템 삭제나 추가됐을테니 레이아웃 구조도 변경됐겠구나!라고 판단) LayoutManager는 표시되는 모든 보기를 완전히 다시 바인딩하고 ViewHolder를 생성해야 합니다.이쯤에서 notifyDataSetChanged
를 사용했을 때 RecyclerView의 내부 동작 과정에 어떤 변화가 있는지 궁금해졌습니다.
로그를 보시면 RecyclerView의 item 수보다 살짝 많게 ViewHolder가 생성 된 후 onViewRecycled가 호출되며 ViewHolder가 재활용 되고 있음을 알립니다 이때 상단 검은 버튼을 클릭해서 notifyDataSetChanged
를 호출했더니 onViewRecycled의 position이 -1이 13번 찍히다가 다시 ViewHolder를 생성하는 콜백인 onCreateViewHolder가 호출됩니다. 뷰홀더를 다시 만들고 있습니다!
이때 position이 -1로 나타나는 이유는 무엇일까요? onViewRecycled의 position은 재활용하기 위해 가져온 itemView의 포지션을 나타냅니다. notifyDataSetChanged()에 의해 리사이클러뷰가 아이템뷰를 갱신하는 과정에서, 뷰홀더가 참조하는 itemView가 삭제되면 재활용할 itemView가 없고=position을 나타낼 수 없어서 NO_POSITION의 값인 -1을 리턴하게 되는 것입니다. -1이 13번 호출된 이유는 맨 처음 RecyclerView가 window에 attached되었을 때 ViewHolder를 create한 횟수가 13번 이었고, notifyDatasetChange가 호출된 후 모든 ViewHolder가 갱신되어야 하므로 처음처럼 13번 호출된 것 같습니다(제가 잘못 이해하고 있는 거라면 댓글 남겨주시면 감사하겠습니다🙇♀️)
이러한 현상이 나타나는 이유는 notifyDataSetChanged
가 호출되면 모든 뷰홀더는 더이상 유효하지 않은 뷰홀더라고 판단되어, 모두 pool로 들어가게 되는데 이때 pool의 공간이 충분하지 않으면(기본으로 pool의 사이즈는 5개 입니다) 몇몇 뷰홀더는 버려져서 GC에 의해 수거되고, 그 결과 새로운 뷰홀더를 생성하기 때문입니다.
실제로 맨 처음 onCreateViewHolder를 호출하는 횟수를 카운팅해보았을 때 총 13번 호출되었으니 뷰홀더 인스턴스는 13개가 생성된 상태입니다. 이때 notifyDataSetChanged
을 호출하면 총 13개의 뷰홀더가 다시 pool에 들어가려 하지만, 이미 pool은 꽉찬 상태입니다. 나머지 8개의 뷰홀더는 버려져서 GC에 의해 수거되고 새로운 뷰홀더 8개를 생성하기 위해 onCreateViewHolder가 8번 호출됩니다.
binding.rv.recycledViewPool.setMaxRecycledViews(0,8) 를 통해 pool의 사이즈를 8개로 늘리고 다시 테스트 해보면 notifyDataSetChanged
가 호출된 후 총 5번의 onCreateViewHolder가 호출됨을 알 수 있습니다
또한 모든 뷰홀더는 cache가 아닌 pool에 있던 dirty view이면서 새롭게 생성된 뷰홀더들이기 때문에 데이터를 다시 bind해줘야 하는 불필요한 작업까지 추가됩니다
결과적으로 notifyDataSetChanged
는 몇개의 항목이나 구조가 변경되었든 상관없이 초기의 뷰홀더 개수만큼 뷰홀더를 "다시" 생성하고 bind하니, 아이템의 수가 많은 경우 모든 아이템을 한번에 변경하느라 깜빡이는 현상까지 나타났던 것입니다.
이런 이유로 공식문서에서 notifyDataSetChanged()
를 "마지막 수단"으로 사용하라고 합니다!