안드로이드/정리(Android)

(Android) RecyclerView의 성능 개선 정리

김염인 2022. 1. 26. 23:07

먼저 RecyclerView와 ListView의 차이는 ?!

둘다 동일한 형식의 리스트들을 구현할 때 사용하지만 ListView와 RecyclerView에는 비슷한거 같지만 세밀한 차이가 존재합니다.

  RecyclerView ListView
ViewHolder ViewHolder 패턴을 이용한다. ViewHolder 패턴 이용하지 않는다.
Item Layout 가로, 세로, Grid 형식 모두 지원 세로 방향만 지원
Item Animation  아이템 애니메이션 처리 클래스 존재 아이템 추가/제거시에 적용가능한 애니메이션 없다.
Decoration RecyclerView.ItemDecoration 객체를 사용하여 구분선을 설정해야한다. Android:divider 속성을 이용하여 리스트에 있는 아이템을 쉽게 구분할 수 있다.
Click Detection 개별 터치 이벤트를 관리하지만 클릭 처리 기능이 내장되어 있지 않다. 목록의 개별 항목에 대한 클릭 이벤트에 바인딩하기 위한 AdapterView.OnItemClickListener 인터페이스가 있다.

이때 ListView보다 RecyclerView를 사용하는 가장 큰 이유는 재사용성 이 훨씬 좋기 때문입니다.

ListView는 ViewHolder 패턴을 사용하시 않고 getView()를 이용하여 View에 접근하는데 , 이처럼 getView()로 동작하게 되면 리스크 갯수만큼 getView()가 호출 되므로 매우 비효율적입니다. 하지만 RecyclerView는 ViewHolder를 통해 만든 객체를 재 사용하기 때문에 ListView보다 RecyclerView가 재 사용성 부분에서 더 효율적입니다.

 

RecyclerView 주요 클래스 

View Holder 

각각의 뷰를 보관하는 Holder 객체 입니다.

Item 뷰들을 재활용하기 위해 각 요소를 저장해두고 사용합니다. 즉, 아이템 생성시 뷰 바인딩은 한 번만하며, 그 바인딩 된 객체를 가져다 사용하여 성능 부분에서 효율적입니다.

 

LayoutManager

아이템의 배치를 담당합니다. 

LinearLayoutManager - 가로 / 세로

GridLayoutManager - 그리드 형식

StaggeredGridLayoutManager - 지그재그형의 그리드 형식

 

Adapter

ListView와 동일한 Adapter의 개념으로 데이터와 아이템에 관한 View를 생성하는 기능을 담당합니다.

 

ItemAnimation

Item 추가 / 삭제시에 애니메이션을 적용할 때 사용합니다.

 

ItemDecoration

RecyclerView의 아이템을 꾸미는 역할을 합니다.

주로 Divider를 설정할 때 유용하게 사용됩니다.

 

Click Detection 

Click Listener가 ListView 처럼 내장되어 있지 않으므로, onClickListner를 통해 직접 구현해주어야 합니다.


 

ㅁ 어뎁터란 ?

보여지는 뷰와 그 뷰에 올릴 데이터를 연결하는 일종의 다리 역할을 하는 객체이다.

어뎁터는 List와 DB등 데이터 소스와 ListView와 같은 우리눈에 보여지는 뷰들 사이에 존재하게 되고 데이터 소스를 어댑터를 통해 어댑터 뷰로 변경해주는 역할을 해준다.

 

RecyclerView의 Adapter는 RecyclerView에서 다음과 같은 역할을 한다.

데이터 리스트를 관리하여 포지션에 맞게 ViewHolder의 View와 연결하여 표시하는 중간자

 

기존 RecyclerView.Adapter를 사용할 경우 위 역할 중 데이터 리스트를 포지션에 맞게 표시하는 부분에서 비효율적인 방식이 나타난다.

 

기본적으로 새로운 데이터가 추가,삭제,변경 되었을 때 notifyDataSetChanged()를 통해 리스트 전체를 업데이트 한다.

private fun updateList(list: List<String>) {
   adapter.dataList = list
   adapter.notifyDataSetChanged() // 리스트 변경을 adapter에 알림
}

 

그러나 만약, 1000개의 데이터 중 단 한 개의 데이터만 바뀌었을 때에도 notifyDataSetChanged()를 사용하면 효율적일까?

아니다. 단 하나의 데이터를 바꾸기 위해 1000개의 데이터를 가진 리스트를 변경하는 작업을 수행하므로 효율적이지 못하다.

 

이 때 변경된 데이터의 position을 인자로 넘겨주어 해당 데이터만 변경하는 notifyItemChanged 메소드를 사용할 수 있다.

하지만 데이터가 변경 될 때마다 해당 position을 찾아 넘겨주며 하나하나 값을 변경하는 것은 번거롭다.

 

(1) DiffUtil

 

이러한 문제를 해결하기 위해 DiffUtil이라는 것이 존재한다.

DiffUtil은 oldItem, newItem의 두 데이터셋을 비교하여 값이 변경된 부분만을 RecyclerView에게 알려줄 수 있다.

 

object FlowerDiffUtil : DiffUtil.ItemCallback<Flower>() {

    override fun areItemsTheSame(oldItem: Flower, newItem: Flower): Boolean {
        return oldItem.id == newItem.id
    }

    override fun areContentsTheSame(oldItem: Flower, newItem: Flower): Boolean {
        return oldItem == newItem
    }
}

 

DiffUtil.ItemCallback을 통해 DiffUtil의 객체를 생성 할 수 있다. 

 

areContentsTheSame 메소드에서는 두 값이 동일한 아이템인지를 확인한다. return 값이 false라면 DiffUtil은 해당 데이터의 변경이 필요하다고 판단하고 RecyclerView에 반영 할 수 있도록 한다. return 값이 true라면 아이템이 같더라도 아이템 내의 데이터가 바뀌었는지 확인하기 위해 areItemsTheSame 메소드를 수행한다.

 

areItemsTheSame 메소드에서는 두 아이템의 내부 데이터가 동일한지 확인한다. 만약 return 값이 false라면 같은 아이템이더라도 데이터가 바뀐 것이므로 RecyclerView에 변경을 반영 할 수 있도록 한다. return 값이 true라면 아이템과 데이터 모두 변경이 없는 것이므로 값의 변경을 반영하지 않는다.

 

DiffUtil의 경우, 리스트의 아이템이 많으면 하나하나 모두 비교 연산을 수행하므로 작업 시간이 길어질 수 있다.

그래서 안드로이드 공식문서에서는 DiffUtil의 비교 연산의 경우 백그라운드 스레드에서 처리하기를 권고하고 있다.

 

'목록이 크면 이 작업에 상당한 시간이 걸릴 수 있으므로 백그라운드 스레드에서 실행하라'

 

 

출처 : https://hungseong.tistory.com/24#---%--DiffUtil

 

package com.example.aop_part3_chaptor05

import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.widget.ListAdapter
import android.widget.TextView
import androidx.recyclerview.widget.DiffUtil
import androidx.recyclerview.widget.RecyclerView
import com.example.aop_part3_chaptor05.model.CardItem

class CardItemAdapter :
    androidx.recyclerview.widget.ListAdapter<CardItem, CardItemAdapter.ViewHolder>(diffUtil) {
    inner class ViewHolder(private val view: View) : RecyclerView.ViewHolder(view) {
        fun bind(cardItem: CardItem) {
            view.findViewById<TextView>(R.id.nameTextView).text = cardItem.name
        }
    }

    override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder {
        val inflater = LayoutInflater.from(parent.context)
        return ViewHolder(inflater.inflate(R.layout.item_card, parent, false))
    }

    override fun onBindViewHolder(holder: ViewHolder, position: Int) {
        holder.bind(currentList[position])
    }

    companion object {
        val diffUtil = object : DiffUtil.ItemCallback<CardItem>() {
            override fun areItemsTheSame(oldItem: CardItem, newItem: CardItem): Boolean {
                return oldItem.userId == newItem.userId
            }

            override fun areContentsTheSame(oldItem: CardItem, newItem: CardItem): Boolean {
                return oldItem == newItem
            }

        }
    }
}

어뎁터 직접구현, ListAdapter와 diffutil을 이용하여 중복된 cardItem을 없애주는 설정을 하였다. 

여기서 LayoutInflater는 XML에 정의된 Resource를 View 객체로 반환해주는 역할을 한다.

우리가 매번 사용하는 onCreate() 메서드에 있는 setContentView(R.layout.activity_main) 또한 Inflater 역할을 한다.

(이 함수의 내부에서 layout inflater가 실행되어 view들을 객체화한다.)

 

LayoutInflater inflater = LayoutInflater.from(context);
View view = inflater.inflate(R.layout.my_layout, parent, false);

LayoutInflater에는 LayoutInflater를 쉽게 생성할 수 있도록 static factory method를 가지고 있다.

코드를 자세히 보면 내부적으로 getSystemService()를 호출하는 것을 볼 수 있다.

public static LayoutInflater from(Context ctx) {
	LayoutInflater inflater = (LayoutInflater) ctx.getSystemService(Context.LAYOUT_INFLATER_SERVICE);
	if (inflater == null) {
		throw new AssertionError("LayoutInflater not found.");
	}
	return inflater;
}



출처: https://suri78.tistory.com/62 [공부노트]