일 | 월 | 화 | 수 | 목 | 금 | 토 |
---|---|---|---|---|---|---|
1 | 2 | 3 | 4 | |||
5 | 6 | 7 | 8 | 9 | 10 | 11 |
12 | 13 | 14 | 15 | 16 | 17 | 18 |
19 | 20 | 21 | 22 | 23 | 24 | 25 |
26 | 27 | 28 | 29 | 30 | 31 |
- 안드로이드 테스트코드
- 안드로이드 Mockito
- 안드로이드 앱 아키텍처 가이드라인 설명
- 안드로이드 최적화
- 안드로이드 아키텍처 컴포넌트
- 안드로이드 mvvm예제
- Hilt
- android clean architecture
- 스타트업 코딩테스트
- 리싸이클러뷰 최적화
- MVVM
- Koin
- 안드로이드 앱 아키텍처 가이드라인
- Android MVVM
- sharedFlow
- 안드로이드 리싸이클러뷰
- Android App Architecture Guideline
- 안드로이드 의존성주입
- 코루틴
- 안드로이드 앱 아키텍처 가이드라인 사용법
- 안드로이드 앱 아키텍처 가이드라인 예시
- android memory leak
- 안드로이드 mvvm
- 안드로이드 클린 아키텍처
- 안드로이드 hilt
- android DI
- 안드로이드 JUnit
- coroutine
- RxJava
- 안드로이드 Espresso
- Today
- Total
안드로이드 연구소
[리싸이클러뷰 박사 교육과정] 리싸이클러뷰 최적화(1) - onBindViewHolder 최적화 본문
지난 포스트에서 리싸이클러뷰의 탄생배경, 작동원리와 문제점을 알아보았고
왜 우리 개발자들이 리싸이클러뷰를 최적화를 해주어야하는지를 알 수 있었습니다.
왜 최적화가 필요한지 모르시는 분들이 있다면 이전 포스트를 참고 해주세요!
이번에는 이어서 리싸이클러뷰를 최적화하는 여러가지 방법들을 보여드리겠습니다!
이번 포스트는 chatGPT보다 더 다양하고 꼼꼼하게 적어놓으신 선배 블로그님의 내용을 바탕으로 포스팅해보겠습니다.
우선 그전에 우리 프로그램의 Recyclerview 성능이 좋은지 안 좋은지를 알 수 있을까요?
Q1. 안드로이드 성능 측정 방법
GPU 렌더링 속도 및 오버드로 검사 | Android 개발자 | Android Developers
앱에서 문제가 발생할 수 있는 위치를 시각화하는 데 도움이 되는 온디바이스 개발자 옵션을 알아보세요.
developer.android.com
2.Systrace
시스템 추적 개요 | Android 개발자 | Android Developers
시스템 추적 개요 컬렉션을 사용해 정리하기 내 환경설정을 기준으로 콘텐츠를 저장하고 분류하세요. 단기간 동안의 기기 활동을 기록하는 것을 시스템 추적이라고 합니다. 시스템 추적을 실행
developer.android.com
3. FrameMetricsAggregator 수집과 Firebase Performance Monitoring 분석
https://firebase.google.com/docs/perf-mon/get-started-android?hl=ko#pdc
Android에서 Performance Monitoring 시작하기 | Firebase Performance Monitoring
5월 10일, Google I/O에서 Firebase가 돌아옵니다. 세션 확인하기 의견 보내기 Android에서 Performance Monitoring 시작하기 컬렉션을 사용해 정리하기 내 환경설정을 기준으로 콘텐츠를 저장하고 분류하세요.
firebase.google.com
위 세가지 방법 중 하나로 여러분의 리싸이클러뷰의 성능을 확인해시보고
최적화 후 얼마나 개선되었는지 확인해보시길 바랍니다.
출처: https://developer.android.com/topic/performance/vitals/render#fixing_jank
Q2. 출처 블로그! 베이스
https://gift123.tistory.com/67
안드로이드 개발 (34) RecyclerView 성능 향상
Android 개발을 하다보면 불가피 하게 RecyclerView를 사용해야 하는 순간이 대부분 옵니다. 요새 Compose열풍이 휩쓸고 있지만 아직은 회사에서 사용하는 목록형 UI는 RecyclerView를 통해 만들었을 겁니다
gift123.tistory.com
올해 제가 본 최고의 블로그가 아닐까 싶은데요.
코드를 하나하나 살펴보며 이해하는데 하루 1시간 x 3일이 걸렸는데
해당 블로그를 토대로 "저의 최적화된 리싸이클러뷰" 파일을 만드는데 집중을 해보려합니다.
Q3. 리싸이클러뷰를 최적화하는 방법에는 무엇이 있니?
첫번째 방법, onBindViewHolder 최적화하기
두번째 방법, onCreateViewHolder 최적화하기!
세번째 방법, 뷰레이아웃 최적화 & Image 최적화
그러면 오늘 포스트에서는 첫번째 방법부터!
Q4. OnBindViewHolder를 최적화하기.
앞 전 포스트에서 설명했듯이 onBindViewHolder는 생성된 뷰홀더에 데이터를 바인딩 해주는 역할을 합니다.
리싸이클러뷰가 1000개의 데이터가 있어도 대략 10개의 뷰 홀더 객체들만 만들어 사용할 뿐입니다.
하지만 객체안에 채울 데이터를 바인딩하는 작업은 데이터의 갯수인 1000번 해야하는거겠죠?
당연히 횟수가 반복될 수 록 성능은 저하될 수 밖에 없기에 뷰바인딩이 최소한으로 불러올 수 있는 방법들을 확인해보겠습니다.
방법1. setHasStableIds(true)와 getItemId()사용
아래처럼 두 구문을 작성해준다면 안정적인 ID를 활성화시킵니다.
이말은 이미 바인딩된 뷰중에 똑같은 뷰가 있을 때
새로 바인딩하지 않고 이전 뷰를 대신해서 보여줍니다.
init {
setHasStableIds(true) // Enable stable ids
}
override fun getItemId(position: Int) = items[position].id
한눈에 이해할 수 있도록 전체 예시입니다.
class MyAdapter(private val items: List<MyItem>) : RecyclerView.Adapter<MyViewHolder>() {
init {
setHasStableIds(true) // Enable stable ids
}
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): MyViewHolder {
val view = LayoutInflater.from(parent.context).inflate(R.layout.item_layout, parent, false)
return MyViewHolder(view)
}
override fun onBindViewHolder(holder: MyViewHolder, position: Int) {
holder.bind(items[position])
}
override fun getItemCount() = items.size
override fun getItemId(position: Int) = items[position].id // Return stable id for the item
}
방법2. 상황에 맞는 notifyItem함수(데이터 변경, 추가, 삭제, 이동) 사용하기
혹시 여러분들도 아래 예제처럼 리싸이클러뷰를 만들 때 LayoutManager를 만들고 리싸이클러뷰와 연결 후에
맹목적으로 notifyDataSetChange() 메서드로 리싸이클러뷰를 출력하지 않으셨는가요?
val layoutManager = LinearLayoutManager(this, LinearLayoutManager.VERTICAL, false)
recyclerview.layoutManager = layoutManager
adapter.notifyDataSetChanged()
이 메서드는 모든 데이터를 전체 변경을 하기 때문에 한 두개의 데이터를 바꿔야할 때는
렌더링 리소스 관점에서 매우 손해이고,
사용자는 전체 데이터가 바뀌는 과정에 깜빡거려 버벅거리는 현상이 보이는 것으로 인식합니다.
데이터를 몇개만 변경하거나 추가, 삭제, 이동할 경우에는 아래 메서드를 사용하시면 됩니다.
데이터 변경: notifyItemChanged, notifyItemRangeChanged
데이터 추가: notifyItemInserted, notifyItemRangeInserted
데이터 삭제: notifyItemRemoved, notifyItemRangeRemoved
데이터 이동: notifyItemMoved
위 예제들의 예시를 보고 싶으시면 아래 "더보기"를 확인해보시면 됩니다.
아래 예제는 제가 많이 사용하는 notifyItemChanged(position)인데요.
리싸이클러뷰의 특정 포지션의 아이템을 클릭하면 해당 포지션의 데이터가 바뀌는 로직입니다.
notifyItemInserted, notifyItemRemoved, notifyItemMoved 메서드 모두 아래와 같은 형식으로 구현됩니다.
// Initialize the RecyclerView and layout manager
recyclerView = findViewById(R.id.recyclerView)
layoutManager = LinearLayoutManager(this)
recyclerView.layoutManager = layoutManager
// Initialize the items list and adapter
items = mutableListOf("Item 1", "Item 2", "Item 3")
val adapter = MyAdapter(items)
recyclerView.adapter = adapter
// Set an OnClickListener on the RecyclerView
recyclerView.setOnClickListener { view ->
val clickedPosition = layoutManager.getPosition(view)
items[clickedPosition] = "New Data"
adapter.notifyItemChanged(clickedPosition)
}
notifyItemRemoved(position) 예시입니다.
// Initialize the RecyclerView and layout manager
recyclerView = findViewById(R.id.recyclerView)
layoutManager = LinearLayoutManager(this)
recyclerView.layoutManager = layoutManager
// Initialize the items list and adapter
items = mutableListOf("Item 1", "Item 2", "Item 3")
val adapter = MyAdapter(items)
recyclerView.adapter = adapter
// Set an OnClickListener on the RecyclerView
recyclerView.setOnClickListener { view ->
val clickedPosition = layoutManager.getPosition(view)
items.removeAt(clickedPosition)
adapter.notifyItemRemoved(clickedPosition)
}
여러개의 아이템을 바꾸는 notifyItemRangeChanged 메서드는 아래와 같습니다.
// Initialize the RecyclerView and layout manager
recyclerView = findViewById(R.id.recyclerView)
layoutManager = LinearLayoutManager(this)
recyclerView.layoutManager = layoutManager
// Initialize the items list and adapter
items = mutableListOf("Item 1", "Item 2", "Item 3", "Item 4", "Item 5")
val adapter = MyAdapter(items)
recyclerView.adapter = adapter
// Set an OnClickListener on the RecyclerView
recyclerView.setOnClickListener { view ->
val clickedPosition = layoutManager.getPosition(view)
for (i in clickedPosition until items.size) {
items[i] = "Updated Item ${i + 1}"
}
adapter.notifyItemRangeChanged(clickedPosition, items.size - clickedPosition)
}
방법3. DiffUtil 사용하기
위의 notifyItem 메서드에는 문제점이 있습니다.
전체 변경, 한개 변경, 일부 변경, 추가, 여러개 추가, 삭제, 여러개 삭제 등 리싸이클러뷰를 만들 때 마다
목적에 맞게 호출해야하기 때문에 작성 후에 매우 헷갈리고 코드가 난잡해보입니다. (경험담)
이를 해결하는 DiffUtil 클래스
DiffUtil은 두 목록 간의 데이터 차이를 확인하고
이전 목록을 새로운 목록으로 변환할 때 차이가 나는 부분만 추려내
데이터를 변경하는 유틸리티 클래스입니다.
예제는 아래와 같다.
첫번째로 어뎁터에서 MyDiffCallback 클래스를 아래와 같은 양식으로 생성해준다.
두번째로 ListAdapter<MyItem, MyViewHolder>(MyDiffCallback())를 상속시켜준다.
class MyAdapter : ListAdapter<MyItem, MyViewHolder>(MyDiffCallback()) {
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): MyViewHolder {
val view = LayoutInflater.from(parent.context).inflate(R.layout.my_item_layout, parent, false)
return MyViewHolder(view)
}
override fun onBindViewHolder(holder: MyViewHolder, position: Int) {
holder.bind(getItem(position))
}
class MyViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) {
fun bind(item: MyItem) {
// Bind the data to the view
}
}
class MyDiffCallback : DiffUtil.ItemCallback<MyItem>() {
// 두 아이템간의 고유id값이 같은지 확인
override fun areItemsTheSame(oldItem: MyItem, newItem: MyItem): Boolean {
return oldItem.id == newItem.id
}
// 두 아이템간의 내용이 같은지 확인
override fun areContentsTheSame(oldItem: MyItem, newItem: MyItem): Boolean {
return oldItem == newItem
}
}
}
세번째 adapter.submitList(items)로 리싸이클러뷰 출력.
끝! 너무 쉬운데?
recyclerView.layoutManager = LinearLayoutManager(this)
val adapter = MyAdapter()
recyclerView.adapter = adapter
val items = listOf(MyItem(1, "Item 1"), MyItem(2, "Item 2"), MyItem(3, "Item 3"))
adapter.submitList(items)
DiffUtil을 사용할 경우 setHasStabledId(true)와 getItemId() 그리고 당연히 notifyItem함수들을 모두 사용할 필요가 없다고 한다.
이미 데이터 간의 차이를 계산하였기에 1번, 2번 대안들 말고 DiffUtil을 사용해보는 것은 어떨까요?
방법4. SetHasFixedSize(true)
만약 여러분이 사용하는 데이터 UI 레이아웃의 크기가 모두 동일하다면 아래 메서드를 사용하자.
SetHasFixedSize(true)경우 크기를 고정값으로 계산하여 레이아웃 계산을 하지 않아 렌더링 시간을 단축시킨다.
recyclerView.setHasFixedSize(true) // setHasFixedSize to true
val layoutManager = LinearLayoutManager(this)
recyclerView.layoutManager = layoutManager
val items = mutableListOf("Item 1", "Item 2", "Item 3", "Item 4", "Item 5")
val adapter = MyAdapter(items)
recyclerView.adapter = adapter
방법5. setItemViewCacheSize
스크롤을 통해 보이지 않는 뷰홀더는
view pool이라는 곳에 저장되어
다음 뷰홀더가 재사용될 때 꺼내오는 방식입니다.
하지만 이때 방금 막 스크롤에서 사라진 뷰홀더는
사실 바로 view pool로 가지 않고 view cache라는 단기저장소에 머물게 됩니다.
만약 다시 스크롤이 원위치로 복귀하여 앞 전 item을 보여준다면
뷰홀더를 재활용하지 않고 view cache에서 꺼내옵니다.
view cache는 기본적으로 2개의 뷰홀더를 저장하고 있습니다.
아래와 같은 코드를 통해 view cache의 크기를 늘릴 수 있습니다.
아래 예제는 2개에서 10개로 늘린 영역입니다.
recyclerView.setItemViewCacheSize(10)
view cache를 늘려 바인딩되는 뷰홀더 호출 수를 낮춰 렌더링에 좋은 효과를 나타낼 수 있습니다.
하지만 캐시 크기를 높은 값으로 설정하면 앱의 메모리 사용량이 증가할 수 있으므로
뷰 크기와 복잡성에 따라 적절한 크기를 선택해야 합니다.
우선 기본 2개로 시작하여 한개씩 "느린 렌더링 성능의 개선 정도"와 "메모리 사용량"을 측정하여
적절한 균형을 찾는다.
방법6. 애니메이션 제거
끝이 없는 스크롤이나 많은 페이지를 보유한 리스트에서
아래 메서드를 제거하여 animation을 제거하여 성능을 높일 수 있다.
recyclerView.setItemAnimator(null)
방법7. 상위 리싸이클러뷰와 하위 리싸이클러뷰의 View Pool 공유하기
리싸이클러뷰 뷰홀더 안에 리싸이클러뷰를 사용하는 "중첩된 리싸이클러뷰"에서
상위 리싸이클러뷰의 view pool과 하위 리싸이클러뷰 view pool공유하기
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder {
val innerLm = LinearLayoutManager(parent.context, LinearLayoutManager.HORIZONTAL, false)
val innerRv = RecyclerView(parent.context).apply {
layoutManager = innerLm
setRecycledViewPool(sharedPool)
}
return ViewHolder(innerRv)
}
방법8. onBindViewHolder에서 반복문, 복잡한 데이터 계산 사용금지
onBindViewHolder에서 for문과 같은 10줄짜리 반복문들을 사용하게 되면
15개의 데이터 리스트를 불러올 경우 15x10짜리 구문을 불러오게됩니다.
이렇게 되면 하나의 뷰홀더 생성하는데 시간이 오래걸리고 느린 렌더링으로 스크롤 버벅거림을 만들어낼 수 밖에 없습니다.
override fun onBindViewHolder(holder: ViewHolder, position: Int) {
val item = items[position]
holder.bind(item)
for (i in 1..position) {
// Do something with the item or holder at each position
}
}
반복문을 사용하지 않고 ViewHolder클래스 init{} 안 에서 계산하거나
엑티비티에서 필요한 데이터를 계산한 후에 어뎁터에 파라미터로 가져오는 방식이 바람직합니다.
override fun onBindViewHolder(holder: ViewHolder, position: Int) {
val item = dataList[position]
holder.bind(item)
}
override fun getItemCount(): Int {
return dataList.size
}
inner class ViewHolder(private val binding: MyItemLayoutBinding) : RecyclerView.ViewHolder(binding.root), View.OnClickListener {
init {
for (i in myData) {
// Do something with the item or holder at each position
}
}
}
방법9. onBindViewHolder에서 클릭 리스너 사용금지
뷰가 바인딩될 때마다 새 리스너를 생성하는 경우
onBindViewHolder에서 setOnClickListener를 사용하면
렌더링 속도가 느려질 뿐만 아니라 메모리 누수 및 성능 문제가 발생할 수 있습니다.
override fun onBindViewHolder(holder: ViewHolder, position: Int) {
val item = dataList[position]
holder.bind(item)
// 클릭 이벤트
holder.binding.root.setOnClickListener {
// show clicked event
}
}
override fun getItemCount(): Int {
return dataList.size
}
inner class ViewHolder(val binding: MyItemLayoutBinding) : RecyclerView.ViewHolder(binding.root) {
fun bind(item: String) {
binding.textView.text = item
}
}
onBindViewHolder에서는 말 그대로 데이터만 연결하고
viewHolder클래스에서 init함수안에 클릭리스너를 사용하여
viewHolder바인딩할 때마다가 아니라
한정된 갯수의 viewHolder생성 시 리스너가 생성되면, ViewHolder하나에 하나의 클릭리스너가 매칭되어 효율적입니다.
override fun onBindViewHolder(holder: ViewHolder, position: Int) {
val item = dataList[position]
holder.bind(item)
}
override fun getItemCount(): Int {
return dataList.size
}
inner class ViewHolder(private val binding: MyItemLayoutBinding) : RecyclerView.ViewHolder(binding.root), View.OnClickListener {
init {
binding.root.setOnClickListener(this)
}
fun bind(item: String) {
binding.textView.text = item
}
override fun onClick(view: View?) {
val item = dataList[adapterPosition]
}
}
방법10. onBindViewHolder에서 HtmlCompat.formHtml()사용 금지
HtmlCompat.formHtml()은 텍스트뷰에 출력할 내용들을
<B>태그에 감싸 굵게 표시해주거나
<font color='blue'>로 일부 텍스트의 색상을 바꿀 때처럼
텍스트 서식을 주기위해서 사용한다.
이 방법은 HTML 구문 분석 및 스타일이 지정된 텍스트 생성이 포함되며
이는 복잡한 프로세스를 가져 계산 비용이 많이 들 수 있습니다.
그래서 onBindViewHolder에서 사용을 제한한다.
inner class ViewHolder(private val binding: MyItemLayoutBinding) : RecyclerView.ViewHolder(binding.root) {
fun bind(item: String) {
binding.textView.text = HtmlCompat.fromHtml(item, HtmlCompat.FROM_HTML_MODE_LEGACY)
}
}
HtmlCompat.fromHtml 대신 아래처럼 SpannableStringBuilder()를 사용하여
렌더링 성능을 유지하면서 텍스트 서식을 더 잘 제어할 수 있다.
override fun onBindViewHolder(holder: MyViewHolder, position: Int) {
val item = dataList[position]
val formattedText = SpannableStringBuilder()
.append("Title: ")
.bold { append(item.title) }
.append("\nDescription: ")
.italic { append(item.description) }
holder.textView.text = formattedText
}
onBindeViewHolder를 최적화하는 방법 외에도
뷰홀더의 아이템 xml을 구현하는 과정에서도 최적화하지 못한 설계를 한다면
느린 렌더링이 발생할 수 있습니다.
'안드로이드 연구소' 카테고리의 다른 글
[리싸이클러뷰 박사 교육과정] 리싸이클러뷰 최적화(3) - 뷰 레이아웃 최적화 & 이미지 최적화 (0) | 2023.05.08 |
---|---|
[리싸이클러뷰 박사 교육과정] 리싸이클러뷰 최적화(2) - onCreateViewHolder 최적화 (0) | 2023.05.08 |
[리싸이클러뷰 박사 교육과정] 리싸이클러뷰 최적화(0) - Recyclerview 탄생배경과 작동원리와 문제점 (2) | 2023.05.06 |
[코딩테스트 제출 시 필독] 주석(@어노테이션)으로 코드 검사 개선 (0) | 2023.05.05 |
[코딩테스트 제출 시 필독] Deprecated메서드 제거 (0) | 2023.05.05 |