-
이어지는 글
-
RecyclerView.ViewHolder에서의 ViewTreeLifecycleOwner 사용
-
기존 ViewTreeLifecycleOwner 사용시
-
ViewTreeLifecycleOwner의 간단한 개념
-
RecyclerView.ViewHolder에서의 LifecycleOwner를 사용할 위치
-
RecyclerView에서 LifecycleOwner의 사용
-
Tips & Tricks
-
No Update == No OnBind
-
Payloads
-
OnCreate Means “create”
-
Positions : Adapter vs Layout
-
RecyclerView의 최적화
-
1. RecyclerView/Adapter의 함수를 효율적으로 응용
-
setHasStableIds와 getItemId
-
setHasFixedSize
-
중첩 RecyclerView간의 Pool 공유
-
2. View 및 onBindViewHolder 최적화
-
onBindViewHolder에서 복잡한 로직 피하기
-
ViewHolder 및 onCreateViewHolder에서 가능한 미리 값 처리
-
OnBindViewHolder에서 Html.fromHtml() 사용 지양
-
아이템 레이아웃 UI 계층 단순화
-
부모 계층 ViewGroup이 ScrollView or RecyclerView
-
item에 복잡한 drawable 사용 지양
-
ViewStub 사용
-
투명색은 가능하면 지양
-
3. 이외의 시도
-
LayoutManager custom을 통해 onCreateView 호출을 여러번 미리 가져오기 (PreCache)
-
LayoutManager.setItemPrefetchEnabled(true, false) 사용
-
init 시에만 AsycLayoutInflate 사용, UI 스케줄러 제작(안드로이드 개발자 Phil Olson의 방식 )
-
AsyncLayoutInflater 를 사용해서 infalte, bind(Dmitrii Kachan 의 방식)
-
ViewHolder paramter로 넘어가는 View를 inflate 해서 생성하지 않고 class로 만들기
-
Compose LazyComposable 사용
이어지는 글
[Android] RecyclerView Deep Dive - 1. RecyclerView 정의와 동작원리 및 생명주기
개요 이전에 RecyclerView에 대한 글을 정리한 적이 있었다. 당시에는, RecyclerView의 등장 의의와 RecyclerView를 사용하는 경우에 구현해야 하는 구현부에 대한 내용을 중심으로 작성했었다. 2022.12.22 - [I
hodie.tistory.com
RecyclerView.ViewHolder에서의 ViewTreeLifecycleOwner 사용
참고: https://pluu.github.io/blog/android/2021/09/20/lifecycleowner/
기존 ViewTreeLifecycleOwner 사용시

- 기존
onBindViewHolder
에서 위와 같이null
이 반환되는 것을 볼 수 있다.
ViewTreeLifecycleOwner의 간단한 개념
Android
에는 생명주기 개념을 가지는 Component로Activity
/Fragment
가 있다.
그리고, 생명주기를 다룬다는 정의를 LifecycleOwner 인터페이스를 통해서 선언하고 있다.
public interface *LifecycleOwner* {
@NonNullLifecycle getLifecycle();
}
// 출처: https://github.com/androidx/androidx/blob/androidx-main/lifecycle/lifecycle-common/src/main/java/androidx/lifecycle/LifecycleOwner.java
AndroidX
에는ComponentActivity
와Fragment
에서LifecycleOwner interface
를 구현하고 있다.
그래서, 해당Component
에서는 직접getLifecycle()
함수를 통해서 직접 생명주기를 다룰 수 있다.
코드 분리
- 리팩토링 등으로
Activity
/Fragment
에 코드는 없지만,Activity
/Fragment
의 LifecycleOwner 인터페이스에 접근해야 할 경우가 있다.
→ 이때 유용한 API가 바로ViewTreeLifecycleOwner
- 그리고,
LifecycleOwner
은 ViewTreeLifecycleOwner.get 혹은 View.findViewTreeLifecycleOwner KTX를 통해서 가져올 수 있다.
public fun View.findViewTreeLifecycleOwner(): LifecycleOwner? = ViewTreeLifecycleOwner.get(this)
// 출처: https://github.com/androidx/androidx/blob/androidx-main/lifecycle/lifecycle-runtime-ktx/src/main/java/androidx/lifecycle/View.kt
RecyclerView.ViewHolder에서의 LifecycleOwner를 사용할 위치
Lifecycle
에 맞춘 핸들링은Activity
/Fragment
뿐만 아니라 경우에 따라서View
가 존재하는 어느 곳이더라도 필요할 수도 있다.- 또한,
View.findViewTreeLifecycleOwner
KTX를 사용하면 쉽게LifecycleOwner
를 가져올 수 있다는 것을 이미 알고 있다. onBindViewHolder
에서도ViewHolder
의itemView
를 사용하면 당연히LifecycleOwner
가 반환될 것이라고 기대하지만, 위에서 봤다시피 결과는null
을 반환한다.- 그 이유가 무엇일까?
1. ViewTreeLifecycleOwner
- 이를 위해 먼저 살펴볼 것은,
ViewTreeLifecycleOwner.get
이다.findViewTreeLifecycleOwner
KTX 또한ViewTreeLifecycleOwner.get
을 호출하고 있으므로LifecycleOwner
를 가져오는 핵심 코드라고 볼 수 있다.
public class ViewTreeLifecycleOwner { @Nullable public static LifecycleOwner get(@NonNull View view) { LifecycleOwner found = (LifecycleOwner) view.getTag(R.id.view_tree_lifecycle_owner); if (found != null) return found; ViewParent parent = view.getParent(); while (found == null && parent instanceof View) { // View#getParent가 유효한지 체크 final View parentView = (View) parent; found = (LifecycleOwner) parentView.getTag(R.id.view_tree_lifecycle_owner); parent = parentView.getParent(); } return found; } }
→ 기본 동작은View.getParent
를 반복적으로 호출하면서LifecycleOwner
가 존재하는지 체크하는 형태이다- 이어서
onBindViewHolder
에서View.getTag
와View.getParent
를 확인해 보면
→ 당연히 View.getTag를 통한 LifecycleOwner는 null이다.
→ 그러나,ViewHolder
에서View
에 해당하는itemView
의 parent는null
이라고 출력된다.ViewTreeLifecycleOwner#get
로직에서 내부적으로View.getParent
를 호출하면서LifecycleOwner
가 존재하는 객체를 찾지만, 여기에서는parent
가null
이므로 최종적으로null
로 반환되었던 것이다.
→null
이 반환되는 원인은ViewHolder
가 가리키는 View의부모 View가 null
이기 때문이었다.
2. RecyclerView 생명주기
- 위에서 살펴봤듯이,
RecyclerView
사용 시에onCreateViewHolder
를 통해서View
가 생성되고,onBindViewHolder
를 통해서 값을 반영하는 코드라는 것을 알고 있다. - 그런데,
onBindViewHolder
단계에서ViewHolder
의View
는RecyclerView
에 Add되었을까?ViewHolder
의itemView
는Attach
되었을까?
→ 정답은 No이다. 이전 RecyclerView Deep Dive - 1. RecyclerView 정의와 동작원리 및 생명주기 글에서의 RecyclerViewㅢ Birth의 요약을 보면, 최초ViewHolder
를 생성하는 경우, Bind한 뒤에, 마지막 단계에addView()
를 하고,onViewAttachedToWindow()
를 호출하는 것을 볼 수 있다.
→ 즉, ViewHolder
는 onBindViewHolder
시점이 되더라도 RecyclerView
에 add 되지 않은 상태이기에, . onBindViewHolder
에서 parent
가 null
로 반환됐던 것이며, findViewTreeLifecycleOwner
결과가 null
로 나온 것이다.
3. onViewAttachedToWindow
- 그렇다면,
onViewAttachedToWindow
이후엔 View가 add되었다 볼 수 있다. 그렇다면 결과가 달라질 수 있을까? 출처:https://pluu.github.io/blog/android/2021/09/20/lifecycleowner/
-
(RecyclerView의 child로 add 됨)
ViewHolder
의itemView
의parent
가RecyclerView
로 지정된다 parent
가 존재하므로findViewTreeLifecycleOwner
API 결과로 유효한LifecycleOwner
가 반환된다
-
→ 따라서 LifecycleOwner를 사용하기 위해선 View가 Attach된 이후에 사용해야 한다
RecyclerView에서 LifecycleOwner의 사용
RecyclerView.ViewHolder
를 통한LifecycleOwner
탐색 시 2가지 사실을 알 수 있었다.ViewTreeLifecycleOwner.get
에서parent
를 반복적으로 호출하면서LifecycleOwner
를 소유하는 객체를 찾는다onBindViewHolder
단계에서는RecyclerView
의 child로 추가되지 않는다
LifecycleOwner
를 얻는 시점을onViewAttachedToWindow
에서 다룰 수도 있지만,
매번Adapter
에서ViewHolder
에LifecycleOwner
를 주입하는 코드는 놓치기 쉬운 코드며 모든ViewHolder
에서는 필요한 경우는 드물다.- 대신
ViewHolder
의 초기화 단계(init)에서doOnAttach
KTX 함수를 사용하면ViewHolder
에서 코드를 작성할 수 있다.
→ 결과로 간단하게View
가Attach
된 이후에ViewTreeLifecycleOwner.get
API를 사용할 수 있다.
class ViewHolder(
private val binding: ItemRecyclerViewSampleBinding
) : RecyclerView.ViewHolder(binding.root) {
private var lifecycleOwner: LifecycleOwner? = null
init {
itemView.doOnAttach {
lifecycleOwner = itemView.findViewTreeLifecycleOwner()
}
itemView.doOnDetach {
lifecycleOwner = null
}
}
}
그 결과

Tips & Tricks
No Update == No OnBind
- 아이템을 업데이트하지 않는다면 → 그 아이템에 대한
onBind
는 호출되지 않는다.
즉, 아이템이 새로운 아이템으로 업데이트 되지 않는다면onBind
는 재호출되지 않는다.
❌ Don’t this (잘못된 코드)
mAdapter.notifyItemMoved(1, 5);
// onBind 호출 X
// onBind가 일어나지 않으므로 invalidate도 없다.
public void onBindViewHolder(final ViewHolder holder, final int position) {
holder.itemView.setOnClickListener(
new View.OnClickLister() {
@Override
public void onClick(View v) {
removeAtPosition(**position**);
}
}
}
}
// 아이템이 이동되었을 때 position을 가지고 뭔가 추가하거나 제거하려는 동작을 구현하였으나
// 아래 코드 상황에서 position이 올바르게 동작하지 않는 버그가 발생한다.
onBindViewHolder(holder, 5);
notifyItemMoved(5, 15);
holder.itemView.callOnClick();
// 버그 발생!
✅ Do this
public void onBindViewHolder(final ViewHolder holder, final int position) {
holder.itemView.setOnClickListener(
new View.OnClickLister() {
@Override
public void onClick(View v) {
removeAtPosition(**holder.getAdapterPosition**);
}
}
}
}
// ViewHolder는 현재 어댑터의 position을 알고 있기에 getAdapterPosition을
// 사용하면 아이템이 이동되는 이벤트에도 정확한 position을 구할 수 있다.
Payloads
- 아이템의 변경에
Payload
를 사용할 수 있다. - 예를 들어, 유저의 좋아요 이벤트 같은 아이템의 변경에 페이로드를 사용해볼 수 있는데 이는
onBind
를 더 효율적으로 만들며, 더 나은 애니메이션을 제공할 수 있다.
mAdapter.notifyItemChanged(position, LIKE_UPDATE);
// "좋아요" 이벤트가 발생하여 아이템을 업데이트
onBind
에서payloads
가 비었는지를 체크해볼 수 있다.payloads
가 존재한다면 전체 아이템을 전부 다시Bind
할 필요가 없어진다.payloads
를 통해 오직 변경점에 대한 부분만 다시Bind
할 수 있게 되는 것
public void onBindViewHolder(MyViewHolder holder, int position, List payloads) {
if (payloads.isEmpty()) {
// 전체 Re-Bind
onBindViewHolder(holder, position);
} else if (payloads.contains(LIKE_UPDATE)) {
// 부분적인 Re-Bind
Item item = mItems.get(position);
holder.likeButton.setEnabled(!item.isLiked());
}
}
OnCreate Means “create”
- 자주하는 실수는 헤더 뷰 같은 것을 구현할 때 발생한다.
아래 코드에서 기존의 헤더ViewHolder
가 존재한다면 재사용해주도록 Return하고 있다.
❌ Don’t this
ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
if (viewType == HeaderViewHolder.TYPE) {
if (mHeaderViewHolder == null) {
mHeaderViewHolder = new HeaderViewHolder(parent);
}
return mHeaderViewHolder;
}
return new MyViewHolder(parent);
}
- 하지만 위 코드는 버그를 발생시킬 수 있는데, 이에 대해
onCreateViewHolder
가 어떤 의미인지 생각해볼 필요가 있다.
말 그대로 ‘생성’을 의미하는 것이기 때문에 위 코드는 바람직하지 않다.
✅ Do this
ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
if (viewType == HeaderViewHolder.TYPE) {
return new HeaderViewHolder(parent);
}
return new MyViewHolder(parent);
}
Positions : Adapter vs Layout
adapterPosition
과layoutPosition
은 아이템 리스트의 변경이 생겼을 때 차이가 발생하게 된다.
왜냐하면다음 레이아웃 과정
에서 이 position을 계산하는 과정이 비동기적이기 때문이다.- 비로소
레이아웃 과정
을 마치고View
가refresh
된 이후에야adapterPosition
과layoutPosition
이 일치하게 되는 것이다.
→ 그러므로 데이터에 접근할 때
는 adapterPosition
을 사용하는게 좋을 것이고
유저가 무언가를 클릭 했을 때 위, 아래에 있는 아이템에 접근해야할 경우
에는 layoutPosition
을 사용하는게 좋다.
RecyclerView의 최적화
1. RecyclerView/Adapter의 함수를 효율적으로 응용
setHasStableIds와 getItemId
HasStableIds
사용을 통해, 데이터를 바인딩하는 경우,onBindViewHolder()
를 최적화되게 호출 할 수있다.- 참고로,
DiffUtil
을 사용하는 경우, setHasStableId(true) , getItemId()는 사용하지 않아도 된다.
차이 알고리즘에 의해 필요한 부분만 업데이트가 이미 되기 때문이다.
HasStableIds
사용으로 최적화 호출이 가능한 조건
- 둘 중 하나의 조건에만 해당한다면 성능을 크게 향상시킬 수 있다
- 똑같은 데이터가 반복적으로 나타나는 리스트
notifyDataSetChanged
를 자주 호출한다
최적화 호출 원리
setHasStableIds(true)
를 사용하면 각각 아이템position
에 지정된id
를 기준으로 상황에 따라onBindViewHolder()
호출을 제외시킨다.- 값이 변경된
id
만onBindViewHolder
를 호출하거나 호출된 아이템의id
가 이전position
아이템에 이미 존재할 시onBindViewHolder
함수를 호출 하지 않고 이전에 같은id
를 가진View
를 대신 보여준다. - 이러한 원리로 같은 데이터임을 알고, 데이터 바인드를 할 필요가 없기 때문에 호출되지 않으며 그만큼 성능이 향상될 수 있는 것이다.
사용 방법
HasStableIds
는Adapter.setHasStableIds(boolean)
을 통해 설정할 수 있고, 사용하는 경우Adapter
의getItemId(int)
를 반드시 구현해야 작동한다.getItemId(int)
를 통해 해당 아이템은 고정된 상태로 설정된다.
사용 예시 (https://blog.kmshack.kr/Stable-Id를-이용한-RecyclerView-성능-향상법/)
position | return |
---|---|
0 | 100 |
1 | 200 |
2 | 300 |
3 | 100 |
4 | 400 |
5 | 500 |
- 위와 같이 값이 반환되게 구현된 경우, onBindViewHolder(view, int)는 position 0, 1, 2, 4, 5만 호출된다.
- 3번은 0번째 포지션에서 같은 고정된 ID를 반환했기 때문에 같은 데이터로 인식하여 onBindViewHolder(view, int)가 호출되지 않는다
→ 같은 데이터임을 알고 데이터 바인드를 할 필요가 없기 때문에 호출되지 않으며 그만큼 성능은 향상된다.
position | return |
---|---|
0 | 100 |
1 | 200 |
2 | 600 |
3 | 300 |
4 | 100 |
5 | 400 |
6 | 500 |
- position 2번에 데이터를 추가하고 notifyDataSetChanged()를 호출하였다. 이때, onBindViewHolder(view, int) 는 현재 보이고 있는 포지션이 모두 호출되지만 StableId를 사용하게 된다면 이미 호출된 고정된 ID를 제외한 위치가 호출된다.
- notifyDataSetChanged()를 하였음에도 변경되는 ID만을 골라 해당 포지션만 onBindViewHolder(view, int)를 호출하게 됨으로 그만큼 성능은 향상된다.
setHasFixedSize
RecyclerView
는 데이터가 삽입, 삭제가 될 때 각각 아이템의 레이아웃을 다시 계산해야할지 정한다.hasFixedSize
가true
일 경우 고정값으로 인식하고,false
일 경우requestLayout()
호출이 되어 아이템의 레이아웃 계산이 다시 이루어진다.View
와 관련된 작업은 모두UI Thread
가 위임하기 때문에, 버벅거림이 없는60프레임
을 달성 하려면 최대한UI Thread
를 효율적으로 활용 해야한다.- 따라서 아이템 UI의 사이즈가 고정이라면
true
로 하는게 좋다.
중첩 RecyclerView간의 Pool 공유
- 중첩되어 구현된
RecyclerView
에서RecyclerView
간의pool
를 공유해 성능을 향상 시킬 수 있다.
→ 바깥쪽과 안쪽 pool를 공유하면 된다.
예시
override fun onCreateViewHolder(...): RecyclerView.ViewHolder {
val innerLm = LinearLayoutManager(...) innerRv.apply{
layoutManager = innerLm recyclerViewPool = sharedPool
} return OuterAdapter.ViewHolder(innerRv)
}
2. View 및 onBindViewHolder 최적화
onBindViewHolder에서 복잡한 로직 피하기
onBindViewHolder
는 가능하면 순수 data set만 하는 것이 가장 이상적이다.data
를 외부에서 미리 가공해서onBindViewHolder
에서set
하는 것이 가장 안정적이다.
구체적 예시
onBindViewHolder
에서 가능하면for
,while
과 같은 반복문은 피한다.onBindViewHolder
에서 특별한 경우를 제외하고 콜백,리스너 정의를 하지 않아야 한다.
(ex: onClickListener)
ovrride fun onBindViewHolder(
holder: RecyclerView.ViewHolder,
position: Int
) {
// 앱 성능 저하의 원인
for (...) {...}
// 불 필요한 코드
view.setOnClickListenr { ... }
//아래 처럼 set만 하는 코드만 존재하는 것이 가장 이상적
imageView.set...(data[position].url)
textView.text = data[position].text
}
ViewHolder 및 onCreateViewHolder에서 가능한 미리 값 처리
예시 코드
- 예시로
width
를 디바이스 기기의 3분의 1 크기로 고정값으로 사용해야하는 경우
class ViewHolder(v: View) :
RecyclerView.ViewHolder(v) {
init {
// 가능하면 미리 정할 수 있는 값은 ViewHolder나 onCreateView에서 처리하는 것이 좋다.
v.layoutParms = LayoutParms(....)
v.itemView.setOnClickListener { ... }
}
override fun onBindViewHolder(...) {
//별로 좋지않는 코드, 성능 저하의 원인 중 일부분이 될 수 있음
holder.layoutParms = LayoutParms(....)
holder.itemView.setOnClickListener { ... }
}
}
→ 위 처럼 가능하면 ViewHolder
에서 미리 처리할 수 있는 것은 set
을 해두는 것이 좋고 onBindViewHolder
에 최대한 부담을 없애야 한다.
OnBindViewHolder에서 Html.fromHtml() 사용 지양
html
를 사용하기 때문에 마찬가지로onBindViewHolder
에 적합하지 않다.- 성능저하의 원인 중 하나가 된다.
아이템 레이아웃 UI 계층 단순화
안드로이드 xml
은View
가 위치한depth
가 한단계 씩 깊어질 수록UI 연산
이 배로 증가한다
https://android-developers.googleblog.com/2017/08/understanding-performance-benefits-of.html
→ 성능 저하의 원인으로 이어질 수 있다- 따라서, 특별한 경우를 제외하고 가능하면 xml 에서
ConstraintLayout
를 사용해1 depth
로 View를 그리는 것이 좋다- 이상적코드
<android.support.constraint.ConstraintLayout>
<ImageView />
<ImageView />
<TextView />
<EditText />
<TextView />
<TextView />
<EditText />
<Button />
<Button />
<TextView />
</android.support.constraint.ConstraintLayout>
부모 계층 ViewGroup이 ScrollView or RecyclerView
recyclerView.setNestedScrollingEnabled(false)
- 상위 뷰에 따라 스크롤이 부드럽지 않는 경우도 있다.
- 그럴 때 위 코드를 적용하면 한층 더 좋아짐을 알 수가 있으며
XML
에서도 설정 가능하다
item에 복잡한 drawable 사용 지양
<layout-list>
로 이루어진 복잡한drawable
을item XML
에 가능하면 사용하지 말아야한다.
→ 저가용 기기에서 GPU 에 부하를 주기 때문에 가능하면 심플하게 사용해야 한다
ViewStub 사용
ViewStub
으로 지연 인플레이트를 사용하는 것이 좋다.visible
로 설정된 경우에inflate
하기 때문에 성능 컨트롤에 효율적이다.
투명색은 가능하면 지양
- 투명색 사용은 저가용 기기에서 상당한 부하가 될 수 있다.
- 이는 투명색 처리는 연산처리가 많기 때문에 가급적 피하는 것이 좋다.
3. 이외의 시도
LayoutManager custom을 통해 onCreateView 호출을 여러번 미리 가져오기 (PreCache)
최적화 원리
ViewHolder
생성 ~ 제거 Lifecycle에서onCreateViewHolder
는 최초로ViewHolder
생성한다.- 하지만 [생성]을 미리 해놓는 경우, 다음 LifeCycle에서
onCreateViewHolder
호출 횟수를 줄이는 방법이 존재한다
→ 바로RecyclerView
가 내부적으로 인식하는 화면 사이즈를 크게 잡는 것 RecyclerView
는사용자가 눈으로 볼 수 있는 화면
+보이지 않는 위아래 가상의 영역의 사이즈
가 있는데 이전체 사이즈
를 내부적으로 늘리면
→ 기본적으로 갖는 아이템의 양의 수보다 더 많은 아이템을onCreateViewHolder
로 생성 해놓을 수 있다.- 스크롤을 하게 되면 이미
onCreateViewHolder
로 생성한 아이템들로 인해onCreateViewHolder
함수의 호출이 적어진다.
class PreCacheLayoutManager(context: Context, private val extraLayoutSpace: Int) :
LinearLayoutManager(context) {
override fun getExtraLayoutSpace(state: RecyclerView.State?) = extraLayoutSpace
}
//Activity 에서..
recyclerView.adapter = PreCacheLayoutManager(context,600)
단점
- 그러나, 사이즈를 너무 크게 잡아버리면
init
시 호출되는onCreateViewHolder
가 많아지고 가상으로 인식하는 사이즈가 매우 넓기 때문에init
타이밍에 버벅거림이 존재할 수 있다. - 따라서, 이 방식은 적재적소하게 사용하는 것이 좋다.
LayoutManager.setItemPrefetchEnabled(true, false) 사용
RecyclerView 25.0.0
부터 기본적으로setItemPrefetchEnabled()
이 true로 설정되어 있다.
→ 따라서true
로 하고 싶다면 별도의 설정을 하지 않아도 된다
최적화 원리
// false 하고 싶을 때만 직접적으로 명시
layoutManager.itemPrefetchEnabled = false
onBindViewHolder
를 몇 프레임 앞서서 미리 호출하는 방식이다.
→ 유저가 빠른 스크롤을 할때 미리onBindViewHolder
를 미리 호출한 것을 보여주기 때문에 자연스러운 스크롤을 달성할 수 있다.- (1)
UI thread
에서View
의inflate
와bind
가 완료되면
(2) 순차적으로GPU Render Thread
에서 렌더링 작업을 하게 되는데,
(3) 이때UI Thread
는 유휴 상태가 된다.
→ 스크롤 할때 이 순서를 반복 - 하지만 스크롤할때 새로운
View
가 등장하기 위해UI Thread
는 다시infalte & bind
작업을 하게 되는데, 문제는 위 작업이Render Thread
에서 렌더링이 끝나고 UI가 사용자 눈에 표시 되려 하는 순간과 겹치게 된다.
→ 그래서UI Thread
의View
infate & bind
작업은 비용이 매우 크면서 동시에 렌더링 된 UI가 표시가 되기 때문에
→ 순간적으로 버벅거림이 생기게 된다. - 따라서
Prefetch 방식
을 통해 스크롤 할때inflate
가 필요한 경우,UI 스레드
가 유휴상태에 들어가는 동안 미리 추가적으로onBindViewHolder()
를 호출한다.
사용 조건
RecyclerView
버전이 업데이트 되면서setItemPrefetchEnabled()
가 기본적으로true
로 설정되어 있다. 그러나,ViewCache
메모리를 추가적으로 사용하게 된다.
→ 추가적인 비용 소요- 따라서
스크롤이 조금만 발생을 하도록 유도한 UX
,전체 아이템 갯수가 매우 적을 경우
→false
반대로많은 스크롤이 일어나는 일반적인 경우
→true
init 시에만 AsycLayoutInflate 사용, UI 스케줄러 제작(안드로이드 개발자 Phil Olson의 방식 )
Smooth RecyclerView scrolling in Android
Tips for using complex views without skipping frames
medium.com
RecyclerView
에adapter
를set
하면서 동시에RecyclerView Adapter
를 사용하는 경우가 아닌,Adapter
에 뒤늦게 아이템이 insert 되는 경우,AsycLayoutInflate
를 사용해 미리onCreateViewHolder
에서 사용할View
를 만들어 두어onCreateViewHolder
의 부담을 더는 방법
예시
class SmoothListAdapter(val context: Context) :
ListAdapter<ListItem, SmoothListAdapter.ListItemViewHolder>(ListItemViewHolder.MyDiffCallback()) {
data class ListItem(val id: String, val text: String)
class ListItemViewHolder(view: View) : RecyclerView.ViewHolder(view) {
private val asyncLayoutInflater = AsyncLayoutInflater(context)
private val cachedViews = Stack<View>()
init {
//Create some views asynchronously and add them to our stack
for (i in 0..NUM_CACHED_VIEWS) {
asyncLayoutInflater.inflate(
R.layout.list_item,
null
) { view, layoutRes, viewGroup -> cachedViews.push(view) }
}
}
override fun onCreateViewHolder(
parent: ViewGroup,
viewType: Int
): ListItemViewHolder {
//Use the cached views if possible, otherwise if we ran out of cached views inflate a new one
val view = if (cachedViews.isEmpty()) {
LayoutInflater.from(context).inflate(R.layout.list_item, parent, false)
} else {
cachedViews.pop().also {
it.layoutParams = LinearLayout.LayoutParams(MATCH_PARENT, WRAP_CONTENT)
}
}
return ListItemViewHolder(view)
}
fun populateFrom(listItem: ListItem) {
//TODO: populate your view
}
override fun onBindViewHolder(viewHolder: ListItemViewHolder, position: Int) =
viewHolder.populateFrom(getItem(position))
class MyDiffCallback : DiffUtil.ItemCallback<ListItem>() {
override fun areItemsTheSame(firstItem: ListItem, secondItem: ListItem) =
firstItem.id == secondItem.id override fun areContentsTheSame(
firstItem: ListItem,
secondItem: ListItem
) = firstItem == secondItem
}
companion object {
const val NUM_CACHED_VIEWS = 5
}
}
}
- 만약 위 코드 처럼
ListAdapter
를 사용한다면 미리adapter
를 초기화 해두고submitList(colleaction<T>)
를 뒤늦게 사용하게 된다면 미리 만들어둔view
를stack
에 담아놨던걸 그대로 사용하면 된다. - 또한
UI 스케줄러
를 따로 만들어 사용 가능하다.
object UIJobScheduler {
private const val MAX_JOB_TIME_MS: Float = 4f private
var elapsed = 0L private
val jobQueue = ArrayDeque<() -> Unit>() private
val isOverMaxTime get () = elapsed > MAX_JOB_TIME_MS * 1_000_000 private
val handler = Handler()
fun submitJob(job: () -> Unit) {
jobQueue.add(job) if (jobQueue.size == 1) {
handler.post { processJobs() }
}
}
private fun processJobs() {
while (!jobQueue.isEmpty() && !isOverMaxTime) {
val start =
System.nanoTime() jobQueue . poll ().invoke() elapsed += System . nanoTime () - start
} if (jobQueue.isEmpty()) {
elapsed = 0
} else if (isOverMaxTime) {
onNextFrame { elapsed = 0 processJobs () }
}
}
private fun onNextFrame(callback: () -> Unit) =
Choreographer.getInstance().postFrameCallback { callback() }
}
- 위 코드는 최대 4ms를 사용한다.
MAX_JOB_TIME_MS
상수를 바꿔서 원하는 최대 ms를 설정할 수 있다.
AsyncLayoutInflater 를 사용해서 infalte, bind(Dmitrii Kachan 의 방식)
Improve UI Performance with Async RecyclerView Layout Loading
RecyclerView is one of the most commonly used Android UI components. It can be very powerful but unfortunately it sometimes becomes very…
proandroiddev.com
AsyncLayoutInflater
를 사용해onCreateViewHolder
에서ViewHolder
를 생성하고,
생성 후onBindViewHolder
에서bind
까지 하는 방식
→ 기존에UI Thread
가 하던 일을background Thread
에 맡기는 형식
→background Thread
에서 레이아웃 계산부터 모두 비동기 방식으로 사용이 가능하다.
예시
open class AsyncCell(context: Context) : FrameLayout(context, null, 0, 0) {
init {
layoutParams = LayoutParams(MATCH_PARENT, WRAP_CONTENT)
}
open val layoutId = -1
// override with your layout Id
private var isInflated = false
private val bindingFunctions: MutableList<AsyncCell.() -> Unit> = mutableListOf()
fun inflate() {
AsyncLayoutInflater(context).inflate(layoutId, this) { view, _, _ ->
isInflated = true addView (createDataBindingView(view)) bindView ()
}
}
private fun bindView() {
with(bindingFunctions) {
forEach { it() }
clear()
}
}
fun bindWhenInflated(bindFunc: AsyncCell.() -> Unit) {
if (isInflated) {
bindFunc()
} else {
bindingFunctions.add(bindFunc)
}
}
// override for usage with dataBinding
open fun createDataBindingView(view: View): View? = view
}
- 위에
FrameLayout
를 더미로 만들고addView
를 해서 아래처럼 사용
class RecyclerViewAsyncAdapter(private val items: List<TestItem>) :
RecyclerView.Adapter<RecyclerView.ViewHolder>() {
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder =
SmallItemViewHolder(SmallItemCell(parent.context).apply { inflate() })
override fun getItemCount(): Int = items.size
override fun onBindViewHolder(
holder: RecyclerView.ViewHolder,
position: Int
) {
if (holder is SmallItemViewHolder) {
setUpSmallViewHolder(holder, position)
}
}
private fun setUpLargeViewHolder(holder: LargeItemViewHolder, position: Int) {
(holder.itemView as LargeItemCell).bindWhenInflated {
items[position].let { item ->
holder.itemView.binding?.item = item
}
}
}
private fun setUpSmallViewHolder(holder: SmallItemViewHolder, position: Int) {
(holder.itemView as SmallItemCell).bindWhenInflated {
items[position].let { item ->
holder.itemView.binding?.item = item
}
}
}
private inner class SmallItemViewHolder internal constructor(view: ViewGroup) : RecyclerView.ViewHolder(view)
private inner class SmallItemCell(context: Context) : AsyncCell(context) {
var binding: SmallItemCellBinding? = null override
val layoutId =
R.layout.small_item_cell override fun createDataBindingView(view: View): View? {
binding = SmallItemCellBinding.bind(view) return view.rootView
}
}
}
- 단점으로 복잡한
View
를 가진 아이템에서는 간혹 UI가 제대로 렌더링 못하는 경우가 있거나, 오히려 더 느려지는 문제점이 생기기도 한다.
→ 마찬가지로 상황에 맞게 사용해야 한다
ViewHolder paramter로 넘어가는 View를 inflate 해서 생성하지 않고 class로 만들기
onCreateViewHolder
에서 기본적으로XML
를LayoutInflate
로View
로inflate
해서 사용하는게 일반적인 경우- 대신
XML
를 사용하지 않고ItemView
자체를 class로 직접 만들어ViewHolder
paramter 로 넘기는 방법
→inflate
을 하지않고 바로View
를 넘기기 때문에ViewHolder
생성 비용을 아낄 수 있음
예시
class ProductCardView : MaterialCardView {
...
}
abstract class MyAdapter : RecyclerView.Adapter<MyAdapter.ViewHolder>() {
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder {
return RecyclerView.ViewHolder(
ProductCardView(parent.context)
)
}
...
}
단점
- 하지만
유지보수 측면
에서XML
에 비해 효율은 좋지 않음. - 성능을 중심으로 코딩 하는 상황이 아니라면 가급적 사용하지 않는 방법.
Compose LazyComposable 사용
- Compose를 통해 목록형 UI를 만드는 방법
늦은 초기화
를 통해 View가 사용자 눈에 보일 때 UI 렌더링 합니다.
→ 복잡한 Adpater 관리도 사라지며composable function
안에서 쉽게 List 관리가 가능하다.XML
과 혼용해 사용할 수 있기 때문에 List부분만LazyComposable
를 이용해 만드는 것도 하나의 방법
예시
LazyColumn {
// Add a single item
item { Text(text = "First item") }
// Add 5
items items (5) { index -> Text(text = "Item: $index") }
// Add another single
item item { Text(text = "Last item") }
}
'IT > Android' 카테고리의 다른 글
[Android/Refactoring] Memory Leak - 2. 자주 발생하는 안드로이드 메모리 누수 방지하기 (0) | 2023.07.25 |
---|---|
[Android/Refactoring] Memory Leak - 1. 안드로이드 앱에서의 메모리 누수 찾기 (1) | 2023.07.21 |
[Android] RecyclerView Deep Dive - 1. RecyclerView 정의와 동작원리 및 생명주기 (0) | 2023.07.05 |
[Android/FCM] (5) Android에서의 알림 수신 구현 (0) | 2023.03.11 |
[Android/FCM] (4) Android 설정 (0) | 2023.03.07 |
이어지는 글
[Android] RecyclerView Deep Dive - 1. RecyclerView 정의와 동작원리 및 생명주기
개요 이전에 RecyclerView에 대한 글을 정리한 적이 있었다. 당시에는, RecyclerView의 등장 의의와 RecyclerView를 사용하는 경우에 구현해야 하는 구현부에 대한 내용을 중심으로 작성했었다. 2022.12.22 - [I
hodie.tistory.com
RecyclerView.ViewHolder에서의 ViewTreeLifecycleOwner 사용
참고: https://pluu.github.io/blog/android/2021/09/20/lifecycleowner/
기존 ViewTreeLifecycleOwner 사용시

- 기존
onBindViewHolder
에서 위와 같이null
이 반환되는 것을 볼 수 있다.
ViewTreeLifecycleOwner의 간단한 개념
Android
에는 생명주기 개념을 가지는 Component로Activity
/Fragment
가 있다.
그리고, 생명주기를 다룬다는 정의를 LifecycleOwner 인터페이스를 통해서 선언하고 있다.
public interface *LifecycleOwner* { @NonNullLifecycle getLifecycle(); } // 출처: https://github.com/androidx/androidx/blob/androidx-main/lifecycle/lifecycle-common/src/main/java/androidx/lifecycle/LifecycleOwner.java
AndroidX
에는ComponentActivity
와Fragment
에서LifecycleOwner interface
를 구현하고 있다.
그래서, 해당Component
에서는 직접getLifecycle()
함수를 통해서 직접 생명주기를 다룰 수 있다.
코드 분리
- 리팩토링 등으로
Activity
/Fragment
에 코드는 없지만,Activity
/Fragment
의 LifecycleOwner 인터페이스에 접근해야 할 경우가 있다.
→ 이때 유용한 API가 바로ViewTreeLifecycleOwner
- 그리고,
LifecycleOwner
은 ViewTreeLifecycleOwner.get 혹은 View.findViewTreeLifecycleOwner KTX를 통해서 가져올 수 있다.
public fun View.findViewTreeLifecycleOwner(): LifecycleOwner? = ViewTreeLifecycleOwner.get(this) // 출처: https://github.com/androidx/androidx/blob/androidx-main/lifecycle/lifecycle-runtime-ktx/src/main/java/androidx/lifecycle/View.kt
RecyclerView.ViewHolder에서의 LifecycleOwner를 사용할 위치
Lifecycle
에 맞춘 핸들링은Activity
/Fragment
뿐만 아니라 경우에 따라서View
가 존재하는 어느 곳이더라도 필요할 수도 있다.- 또한,
View.findViewTreeLifecycleOwner
KTX를 사용하면 쉽게LifecycleOwner
를 가져올 수 있다는 것을 이미 알고 있다. onBindViewHolder
에서도ViewHolder
의itemView
를 사용하면 당연히LifecycleOwner
가 반환될 것이라고 기대하지만, 위에서 봤다시피 결과는null
을 반환한다.- 그 이유가 무엇일까?
1. ViewTreeLifecycleOwner
- 이를 위해 먼저 살펴볼 것은,
ViewTreeLifecycleOwner.get
이다.findViewTreeLifecycleOwner
KTX 또한ViewTreeLifecycleOwner.get
을 호출하고 있으므로LifecycleOwner
를 가져오는 핵심 코드라고 볼 수 있다.
public class ViewTreeLifecycleOwner { @Nullable public static LifecycleOwner get(@NonNull View view) { LifecycleOwner found = (LifecycleOwner) view.getTag(R.id.view_tree_lifecycle_owner); if (found != null) return found; ViewParent parent = view.getParent(); while (found == null && parent instanceof View) { // View#getParent가 유효한지 체크 final View parentView = (View) parent; found = (LifecycleOwner) parentView.getTag(R.id.view_tree_lifecycle_owner); parent = parentView.getParent(); } return found; } }
→ 기본 동작은View.getParent
를 반복적으로 호출하면서LifecycleOwner
가 존재하는지 체크하는 형태이다- 이어서
onBindViewHolder
에서View.getTag
와View.getParent
를 확인해 보면
→ 당연히 View.getTag를 통한 LifecycleOwner는 null이다.
→ 그러나,ViewHolder
에서View
에 해당하는itemView
의 parent는null
이라고 출력된다.ViewTreeLifecycleOwner#get
로직에서 내부적으로View.getParent
를 호출하면서LifecycleOwner
가 존재하는 객체를 찾지만, 여기에서는parent
가null
이므로 최종적으로null
로 반환되었던 것이다.
→null
이 반환되는 원인은ViewHolder
가 가리키는 View의부모 View가 null
이기 때문이었다.
2. RecyclerView 생명주기
- 위에서 살펴봤듯이,
RecyclerView
사용 시에onCreateViewHolder
를 통해서View
가 생성되고,onBindViewHolder
를 통해서 값을 반영하는 코드라는 것을 알고 있다. - 그런데,
onBindViewHolder
단계에서ViewHolder
의View
는RecyclerView
에 Add되었을까?ViewHolder
의itemView
는Attach
되었을까?
→ 정답은 No이다. 이전 RecyclerView Deep Dive - 1. RecyclerView 정의와 동작원리 및 생명주기 글에서의 RecyclerViewㅢ Birth의 요약을 보면, 최초ViewHolder
를 생성하는 경우, Bind한 뒤에, 마지막 단계에addView()
를 하고,onViewAttachedToWindow()
를 호출하는 것을 볼 수 있다.
→ 즉, ViewHolder
는 onBindViewHolder
시점이 되더라도 RecyclerView
에 add 되지 않은 상태이기에, . onBindViewHolder
에서 parent
가 null
로 반환됐던 것이며, findViewTreeLifecycleOwner
결과가 null
로 나온 것이다.
3. onViewAttachedToWindow
- 그렇다면,
onViewAttachedToWindow
이후엔 View가 add되었다 볼 수 있다. 그렇다면 결과가 달라질 수 있을까? 출처:https://pluu.github.io/blog/android/2021/09/20/lifecycleowner/
-
(RecyclerView의 child로 add 됨)
ViewHolder
의itemView
의parent
가RecyclerView
로 지정된다 parent
가 존재하므로findViewTreeLifecycleOwner
API 결과로 유효한LifecycleOwner
가 반환된다
-
→ 따라서 LifecycleOwner를 사용하기 위해선 View가 Attach된 이후에 사용해야 한다
RecyclerView에서 LifecycleOwner의 사용
RecyclerView.ViewHolder
를 통한LifecycleOwner
탐색 시 2가지 사실을 알 수 있었다.ViewTreeLifecycleOwner.get
에서parent
를 반복적으로 호출하면서LifecycleOwner
를 소유하는 객체를 찾는다onBindViewHolder
단계에서는RecyclerView
의 child로 추가되지 않는다
LifecycleOwner
를 얻는 시점을onViewAttachedToWindow
에서 다룰 수도 있지만,
매번Adapter
에서ViewHolder
에LifecycleOwner
를 주입하는 코드는 놓치기 쉬운 코드며 모든ViewHolder
에서는 필요한 경우는 드물다.- 대신
ViewHolder
의 초기화 단계(init)에서doOnAttach
KTX 함수를 사용하면ViewHolder
에서 코드를 작성할 수 있다.
→ 결과로 간단하게View
가Attach
된 이후에ViewTreeLifecycleOwner.get
API를 사용할 수 있다.
class ViewHolder( private val binding: ItemRecyclerViewSampleBinding ) : RecyclerView.ViewHolder(binding.root) { private var lifecycleOwner: LifecycleOwner? = null init { itemView.doOnAttach { lifecycleOwner = itemView.findViewTreeLifecycleOwner() } itemView.doOnDetach { lifecycleOwner = null } } }
그 결과

Tips & Tricks
No Update == No OnBind
- 아이템을 업데이트하지 않는다면 → 그 아이템에 대한
onBind
는 호출되지 않는다.
즉, 아이템이 새로운 아이템으로 업데이트 되지 않는다면onBind
는 재호출되지 않는다.
❌ Don’t this (잘못된 코드)
mAdapter.notifyItemMoved(1, 5); // onBind 호출 X // onBind가 일어나지 않으므로 invalidate도 없다. public void onBindViewHolder(final ViewHolder holder, final int position) { holder.itemView.setOnClickListener( new View.OnClickLister() { @Override public void onClick(View v) { removeAtPosition(**position**); } } } } // 아이템이 이동되었을 때 position을 가지고 뭔가 추가하거나 제거하려는 동작을 구현하였으나 // 아래 코드 상황에서 position이 올바르게 동작하지 않는 버그가 발생한다. onBindViewHolder(holder, 5); notifyItemMoved(5, 15); holder.itemView.callOnClick(); // 버그 발생!
✅ Do this
public void onBindViewHolder(final ViewHolder holder, final int position) { holder.itemView.setOnClickListener( new View.OnClickLister() { @Override public void onClick(View v) { removeAtPosition(**holder.getAdapterPosition**); } } } } // ViewHolder는 현재 어댑터의 position을 알고 있기에 getAdapterPosition을 // 사용하면 아이템이 이동되는 이벤트에도 정확한 position을 구할 수 있다.
Payloads
- 아이템의 변경에
Payload
를 사용할 수 있다. - 예를 들어, 유저의 좋아요 이벤트 같은 아이템의 변경에 페이로드를 사용해볼 수 있는데 이는
onBind
를 더 효율적으로 만들며, 더 나은 애니메이션을 제공할 수 있다.
mAdapter.notifyItemChanged(position, LIKE_UPDATE); // "좋아요" 이벤트가 발생하여 아이템을 업데이트
onBind
에서payloads
가 비었는지를 체크해볼 수 있다.payloads
가 존재한다면 전체 아이템을 전부 다시Bind
할 필요가 없어진다.payloads
를 통해 오직 변경점에 대한 부분만 다시Bind
할 수 있게 되는 것
public void onBindViewHolder(MyViewHolder holder, int position, List payloads) { if (payloads.isEmpty()) { // 전체 Re-Bind onBindViewHolder(holder, position); } else if (payloads.contains(LIKE_UPDATE)) { // 부분적인 Re-Bind Item item = mItems.get(position); holder.likeButton.setEnabled(!item.isLiked()); } }
OnCreate Means “create”
- 자주하는 실수는 헤더 뷰 같은 것을 구현할 때 발생한다.
아래 코드에서 기존의 헤더ViewHolder
가 존재한다면 재사용해주도록 Return하고 있다.
❌ Don’t this
ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) { if (viewType == HeaderViewHolder.TYPE) { if (mHeaderViewHolder == null) { mHeaderViewHolder = new HeaderViewHolder(parent); } return mHeaderViewHolder; } return new MyViewHolder(parent); }
- 하지만 위 코드는 버그를 발생시킬 수 있는데, 이에 대해
onCreateViewHolder
가 어떤 의미인지 생각해볼 필요가 있다.
말 그대로 ‘생성’을 의미하는 것이기 때문에 위 코드는 바람직하지 않다.
✅ Do this
ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) { if (viewType == HeaderViewHolder.TYPE) { return new HeaderViewHolder(parent); } return new MyViewHolder(parent); }
Positions : Adapter vs Layout
adapterPosition
과layoutPosition
은 아이템 리스트의 변경이 생겼을 때 차이가 발생하게 된다.
왜냐하면다음 레이아웃 과정
에서 이 position을 계산하는 과정이 비동기적이기 때문이다.- 비로소
레이아웃 과정
을 마치고View
가refresh
된 이후에야adapterPosition
과layoutPosition
이 일치하게 되는 것이다.
→ 그러므로 데이터에 접근할 때
는 adapterPosition
을 사용하는게 좋을 것이고
유저가 무언가를 클릭 했을 때 위, 아래에 있는 아이템에 접근해야할 경우
에는 layoutPosition
을 사용하는게 좋다.
RecyclerView의 최적화
1. RecyclerView/Adapter의 함수를 효율적으로 응용
setHasStableIds와 getItemId
HasStableIds
사용을 통해, 데이터를 바인딩하는 경우,onBindViewHolder()
를 최적화되게 호출 할 수있다.- 참고로,
DiffUtil
을 사용하는 경우, setHasStableId(true) , getItemId()는 사용하지 않아도 된다.
차이 알고리즘에 의해 필요한 부분만 업데이트가 이미 되기 때문이다.
HasStableIds
사용으로 최적화 호출이 가능한 조건
- 둘 중 하나의 조건에만 해당한다면 성능을 크게 향상시킬 수 있다
- 똑같은 데이터가 반복적으로 나타나는 리스트
notifyDataSetChanged
를 자주 호출한다
최적화 호출 원리
setHasStableIds(true)
를 사용하면 각각 아이템position
에 지정된id
를 기준으로 상황에 따라onBindViewHolder()
호출을 제외시킨다.- 값이 변경된
id
만onBindViewHolder
를 호출하거나 호출된 아이템의id
가 이전position
아이템에 이미 존재할 시onBindViewHolder
함수를 호출 하지 않고 이전에 같은id
를 가진View
를 대신 보여준다. - 이러한 원리로 같은 데이터임을 알고, 데이터 바인드를 할 필요가 없기 때문에 호출되지 않으며 그만큼 성능이 향상될 수 있는 것이다.
사용 방법
HasStableIds
는Adapter.setHasStableIds(boolean)
을 통해 설정할 수 있고, 사용하는 경우Adapter
의getItemId(int)
를 반드시 구현해야 작동한다.getItemId(int)
를 통해 해당 아이템은 고정된 상태로 설정된다.
사용 예시 (https://blog.kmshack.kr/Stable-Id를-이용한-RecyclerView-성능-향상법/)
position | return |
---|---|
0 | 100 |
1 | 200 |
2 | 300 |
3 | 100 |
4 | 400 |
5 | 500 |
- 위와 같이 값이 반환되게 구현된 경우, onBindViewHolder(view, int)는 position 0, 1, 2, 4, 5만 호출된다.
- 3번은 0번째 포지션에서 같은 고정된 ID를 반환했기 때문에 같은 데이터로 인식하여 onBindViewHolder(view, int)가 호출되지 않는다
→ 같은 데이터임을 알고 데이터 바인드를 할 필요가 없기 때문에 호출되지 않으며 그만큼 성능은 향상된다.
position | return |
---|---|
0 | 100 |
1 | 200 |
2 | 600 |
3 | 300 |
4 | 100 |
5 | 400 |
6 | 500 |
- position 2번에 데이터를 추가하고 notifyDataSetChanged()를 호출하였다. 이때, onBindViewHolder(view, int) 는 현재 보이고 있는 포지션이 모두 호출되지만 StableId를 사용하게 된다면 이미 호출된 고정된 ID를 제외한 위치가 호출된다.
- notifyDataSetChanged()를 하였음에도 변경되는 ID만을 골라 해당 포지션만 onBindViewHolder(view, int)를 호출하게 됨으로 그만큼 성능은 향상된다.
setHasFixedSize
RecyclerView
는 데이터가 삽입, 삭제가 될 때 각각 아이템의 레이아웃을 다시 계산해야할지 정한다.hasFixedSize
가true
일 경우 고정값으로 인식하고,false
일 경우requestLayout()
호출이 되어 아이템의 레이아웃 계산이 다시 이루어진다.View
와 관련된 작업은 모두UI Thread
가 위임하기 때문에, 버벅거림이 없는60프레임
을 달성 하려면 최대한UI Thread
를 효율적으로 활용 해야한다.- 따라서 아이템 UI의 사이즈가 고정이라면
true
로 하는게 좋다.
중첩 RecyclerView간의 Pool 공유
- 중첩되어 구현된
RecyclerView
에서RecyclerView
간의pool
를 공유해 성능을 향상 시킬 수 있다.
→ 바깥쪽과 안쪽 pool를 공유하면 된다.
예시
override fun onCreateViewHolder(...): RecyclerView.ViewHolder { val innerLm = LinearLayoutManager(...) innerRv.apply{ layoutManager = innerLm recyclerViewPool = sharedPool } return OuterAdapter.ViewHolder(innerRv) }
2. View 및 onBindViewHolder 최적화
onBindViewHolder에서 복잡한 로직 피하기
onBindViewHolder
는 가능하면 순수 data set만 하는 것이 가장 이상적이다.data
를 외부에서 미리 가공해서onBindViewHolder
에서set
하는 것이 가장 안정적이다.
구체적 예시
onBindViewHolder
에서 가능하면for
,while
과 같은 반복문은 피한다.onBindViewHolder
에서 특별한 경우를 제외하고 콜백,리스너 정의를 하지 않아야 한다.
(ex: onClickListener)
ovrride fun onBindViewHolder( holder: RecyclerView.ViewHolder, position: Int ) { // 앱 성능 저하의 원인 for (...) {...} // 불 필요한 코드 view.setOnClickListenr { ... } //아래 처럼 set만 하는 코드만 존재하는 것이 가장 이상적 imageView.set...(data[position].url) textView.text = data[position].text }
ViewHolder 및 onCreateViewHolder에서 가능한 미리 값 처리
예시 코드
- 예시로
width
를 디바이스 기기의 3분의 1 크기로 고정값으로 사용해야하는 경우
class ViewHolder(v: View) : RecyclerView.ViewHolder(v) { init { // 가능하면 미리 정할 수 있는 값은 ViewHolder나 onCreateView에서 처리하는 것이 좋다. v.layoutParms = LayoutParms(....) v.itemView.setOnClickListener { ... } } override fun onBindViewHolder(...) { //별로 좋지않는 코드, 성능 저하의 원인 중 일부분이 될 수 있음 holder.layoutParms = LayoutParms(....) holder.itemView.setOnClickListener { ... } } }
→ 위 처럼 가능하면 ViewHolder
에서 미리 처리할 수 있는 것은 set
을 해두는 것이 좋고 onBindViewHolder
에 최대한 부담을 없애야 한다.
OnBindViewHolder에서 Html.fromHtml() 사용 지양
html
를 사용하기 때문에 마찬가지로onBindViewHolder
에 적합하지 않다.- 성능저하의 원인 중 하나가 된다.
아이템 레이아웃 UI 계층 단순화
안드로이드 xml
은View
가 위치한depth
가 한단계 씩 깊어질 수록UI 연산
이 배로 증가한다
https://android-developers.googleblog.com/2017/08/understanding-performance-benefits-of.html
→ 성능 저하의 원인으로 이어질 수 있다- 따라서, 특별한 경우를 제외하고 가능하면 xml 에서
ConstraintLayout
를 사용해1 depth
로 View를 그리는 것이 좋다- 이상적코드
<android.support.constraint.ConstraintLayout> <ImageView /> <ImageView /> <TextView /> <EditText /> <TextView /> <TextView /> <EditText /> <Button /> <Button /> <TextView /> </android.support.constraint.ConstraintLayout>
부모 계층 ViewGroup이 ScrollView or RecyclerView
recyclerView.setNestedScrollingEnabled(false)
- 상위 뷰에 따라 스크롤이 부드럽지 않는 경우도 있다.
- 그럴 때 위 코드를 적용하면 한층 더 좋아짐을 알 수가 있으며
XML
에서도 설정 가능하다
item에 복잡한 drawable 사용 지양
<layout-list>
로 이루어진 복잡한drawable
을item XML
에 가능하면 사용하지 말아야한다.
→ 저가용 기기에서 GPU 에 부하를 주기 때문에 가능하면 심플하게 사용해야 한다
ViewStub 사용
ViewStub
으로 지연 인플레이트를 사용하는 것이 좋다.visible
로 설정된 경우에inflate
하기 때문에 성능 컨트롤에 효율적이다.
투명색은 가능하면 지양
- 투명색 사용은 저가용 기기에서 상당한 부하가 될 수 있다.
- 이는 투명색 처리는 연산처리가 많기 때문에 가급적 피하는 것이 좋다.
3. 이외의 시도
LayoutManager custom을 통해 onCreateView 호출을 여러번 미리 가져오기 (PreCache)
최적화 원리
ViewHolder
생성 ~ 제거 Lifecycle에서onCreateViewHolder
는 최초로ViewHolder
생성한다.- 하지만 [생성]을 미리 해놓는 경우, 다음 LifeCycle에서
onCreateViewHolder
호출 횟수를 줄이는 방법이 존재한다
→ 바로RecyclerView
가 내부적으로 인식하는 화면 사이즈를 크게 잡는 것 RecyclerView
는사용자가 눈으로 볼 수 있는 화면
+보이지 않는 위아래 가상의 영역의 사이즈
가 있는데 이전체 사이즈
를 내부적으로 늘리면
→ 기본적으로 갖는 아이템의 양의 수보다 더 많은 아이템을onCreateViewHolder
로 생성 해놓을 수 있다.- 스크롤을 하게 되면 이미
onCreateViewHolder
로 생성한 아이템들로 인해onCreateViewHolder
함수의 호출이 적어진다.
class PreCacheLayoutManager(context: Context, private val extraLayoutSpace: Int) : LinearLayoutManager(context) { override fun getExtraLayoutSpace(state: RecyclerView.State?) = extraLayoutSpace } //Activity 에서.. recyclerView.adapter = PreCacheLayoutManager(context,600)
단점
- 그러나, 사이즈를 너무 크게 잡아버리면
init
시 호출되는onCreateViewHolder
가 많아지고 가상으로 인식하는 사이즈가 매우 넓기 때문에init
타이밍에 버벅거림이 존재할 수 있다. - 따라서, 이 방식은 적재적소하게 사용하는 것이 좋다.
LayoutManager.setItemPrefetchEnabled(true, false) 사용
RecyclerView 25.0.0
부터 기본적으로setItemPrefetchEnabled()
이 true로 설정되어 있다.
→ 따라서true
로 하고 싶다면 별도의 설정을 하지 않아도 된다
최적화 원리
// false 하고 싶을 때만 직접적으로 명시 layoutManager.itemPrefetchEnabled = false
onBindViewHolder
를 몇 프레임 앞서서 미리 호출하는 방식이다.
→ 유저가 빠른 스크롤을 할때 미리onBindViewHolder
를 미리 호출한 것을 보여주기 때문에 자연스러운 스크롤을 달성할 수 있다.- (1)
UI thread
에서View
의inflate
와bind
가 완료되면
(2) 순차적으로GPU Render Thread
에서 렌더링 작업을 하게 되는데,
(3) 이때UI Thread
는 유휴 상태가 된다.
→ 스크롤 할때 이 순서를 반복 - 하지만 스크롤할때 새로운
View
가 등장하기 위해UI Thread
는 다시infalte & bind
작업을 하게 되는데, 문제는 위 작업이Render Thread
에서 렌더링이 끝나고 UI가 사용자 눈에 표시 되려 하는 순간과 겹치게 된다.
→ 그래서UI Thread
의View
infate & bind
작업은 비용이 매우 크면서 동시에 렌더링 된 UI가 표시가 되기 때문에
→ 순간적으로 버벅거림이 생기게 된다. - 따라서
Prefetch 방식
을 통해 스크롤 할때inflate
가 필요한 경우,UI 스레드
가 유휴상태에 들어가는 동안 미리 추가적으로onBindViewHolder()
를 호출한다.
사용 조건
RecyclerView
버전이 업데이트 되면서setItemPrefetchEnabled()
가 기본적으로true
로 설정되어 있다. 그러나,ViewCache
메모리를 추가적으로 사용하게 된다.
→ 추가적인 비용 소요- 따라서
스크롤이 조금만 발생을 하도록 유도한 UX
,전체 아이템 갯수가 매우 적을 경우
→false
반대로많은 스크롤이 일어나는 일반적인 경우
→true
init 시에만 AsycLayoutInflate 사용, UI 스케줄러 제작(안드로이드 개발자 Phil Olson의 방식 )
Smooth RecyclerView scrolling in Android
Tips for using complex views without skipping frames
medium.com
RecyclerView
에adapter
를set
하면서 동시에RecyclerView Adapter
를 사용하는 경우가 아닌,Adapter
에 뒤늦게 아이템이 insert 되는 경우,AsycLayoutInflate
를 사용해 미리onCreateViewHolder
에서 사용할View
를 만들어 두어onCreateViewHolder
의 부담을 더는 방법
예시
class SmoothListAdapter(val context: Context) : ListAdapter<ListItem, SmoothListAdapter.ListItemViewHolder>(ListItemViewHolder.MyDiffCallback()) { data class ListItem(val id: String, val text: String) class ListItemViewHolder(view: View) : RecyclerView.ViewHolder(view) { private val asyncLayoutInflater = AsyncLayoutInflater(context) private val cachedViews = Stack<View>() init { //Create some views asynchronously and add them to our stack for (i in 0..NUM_CACHED_VIEWS) { asyncLayoutInflater.inflate( R.layout.list_item, null ) { view, layoutRes, viewGroup -> cachedViews.push(view) } } } override fun onCreateViewHolder( parent: ViewGroup, viewType: Int ): ListItemViewHolder { //Use the cached views if possible, otherwise if we ran out of cached views inflate a new one val view = if (cachedViews.isEmpty()) { LayoutInflater.from(context).inflate(R.layout.list_item, parent, false) } else { cachedViews.pop().also { it.layoutParams = LinearLayout.LayoutParams(MATCH_PARENT, WRAP_CONTENT) } } return ListItemViewHolder(view) } fun populateFrom(listItem: ListItem) { //TODO: populate your view } override fun onBindViewHolder(viewHolder: ListItemViewHolder, position: Int) = viewHolder.populateFrom(getItem(position)) class MyDiffCallback : DiffUtil.ItemCallback<ListItem>() { override fun areItemsTheSame(firstItem: ListItem, secondItem: ListItem) = firstItem.id == secondItem.id override fun areContentsTheSame( firstItem: ListItem, secondItem: ListItem ) = firstItem == secondItem } companion object { const val NUM_CACHED_VIEWS = 5 } } }
- 만약 위 코드 처럼
ListAdapter
를 사용한다면 미리adapter
를 초기화 해두고submitList(colleaction<T>)
를 뒤늦게 사용하게 된다면 미리 만들어둔view
를stack
에 담아놨던걸 그대로 사용하면 된다. - 또한
UI 스케줄러
를 따로 만들어 사용 가능하다.
object UIJobScheduler { private const val MAX_JOB_TIME_MS: Float = 4f private var elapsed = 0L private val jobQueue = ArrayDeque<() -> Unit>() private val isOverMaxTime get () = elapsed > MAX_JOB_TIME_MS * 1_000_000 private val handler = Handler() fun submitJob(job: () -> Unit) { jobQueue.add(job) if (jobQueue.size == 1) { handler.post { processJobs() } } } private fun processJobs() { while (!jobQueue.isEmpty() && !isOverMaxTime) { val start = System.nanoTime() jobQueue . poll ().invoke() elapsed += System . nanoTime () - start } if (jobQueue.isEmpty()) { elapsed = 0 } else if (isOverMaxTime) { onNextFrame { elapsed = 0 processJobs () } } } private fun onNextFrame(callback: () -> Unit) = Choreographer.getInstance().postFrameCallback { callback() } }
- 위 코드는 최대 4ms를 사용한다.
MAX_JOB_TIME_MS
상수를 바꿔서 원하는 최대 ms를 설정할 수 있다.
AsyncLayoutInflater 를 사용해서 infalte, bind(Dmitrii Kachan 의 방식)
Improve UI Performance with Async RecyclerView Layout Loading
RecyclerView is one of the most commonly used Android UI components. It can be very powerful but unfortunately it sometimes becomes very…
proandroiddev.com
AsyncLayoutInflater
를 사용해onCreateViewHolder
에서ViewHolder
를 생성하고,
생성 후onBindViewHolder
에서bind
까지 하는 방식
→ 기존에UI Thread
가 하던 일을background Thread
에 맡기는 형식
→background Thread
에서 레이아웃 계산부터 모두 비동기 방식으로 사용이 가능하다.
예시
open class AsyncCell(context: Context) : FrameLayout(context, null, 0, 0) { init { layoutParams = LayoutParams(MATCH_PARENT, WRAP_CONTENT) } open val layoutId = -1 // override with your layout Id private var isInflated = false private val bindingFunctions: MutableList<AsyncCell.() -> Unit> = mutableListOf() fun inflate() { AsyncLayoutInflater(context).inflate(layoutId, this) { view, _, _ -> isInflated = true addView (createDataBindingView(view)) bindView () } } private fun bindView() { with(bindingFunctions) { forEach { it() } clear() } } fun bindWhenInflated(bindFunc: AsyncCell.() -> Unit) { if (isInflated) { bindFunc() } else { bindingFunctions.add(bindFunc) } } // override for usage with dataBinding open fun createDataBindingView(view: View): View? = view }
- 위에
FrameLayout
를 더미로 만들고addView
를 해서 아래처럼 사용
class RecyclerViewAsyncAdapter(private val items: List<TestItem>) : RecyclerView.Adapter<RecyclerView.ViewHolder>() { override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder = SmallItemViewHolder(SmallItemCell(parent.context).apply { inflate() }) override fun getItemCount(): Int = items.size override fun onBindViewHolder( holder: RecyclerView.ViewHolder, position: Int ) { if (holder is SmallItemViewHolder) { setUpSmallViewHolder(holder, position) } } private fun setUpLargeViewHolder(holder: LargeItemViewHolder, position: Int) { (holder.itemView as LargeItemCell).bindWhenInflated { items[position].let { item -> holder.itemView.binding?.item = item } } } private fun setUpSmallViewHolder(holder: SmallItemViewHolder, position: Int) { (holder.itemView as SmallItemCell).bindWhenInflated { items[position].let { item -> holder.itemView.binding?.item = item } } } private inner class SmallItemViewHolder internal constructor(view: ViewGroup) : RecyclerView.ViewHolder(view) private inner class SmallItemCell(context: Context) : AsyncCell(context) { var binding: SmallItemCellBinding? = null override val layoutId = R.layout.small_item_cell override fun createDataBindingView(view: View): View? { binding = SmallItemCellBinding.bind(view) return view.rootView } } }
- 단점으로 복잡한
View
를 가진 아이템에서는 간혹 UI가 제대로 렌더링 못하는 경우가 있거나, 오히려 더 느려지는 문제점이 생기기도 한다.
→ 마찬가지로 상황에 맞게 사용해야 한다
ViewHolder paramter로 넘어가는 View를 inflate 해서 생성하지 않고 class로 만들기
onCreateViewHolder
에서 기본적으로XML
를LayoutInflate
로View
로inflate
해서 사용하는게 일반적인 경우- 대신
XML
를 사용하지 않고ItemView
자체를 class로 직접 만들어ViewHolder
paramter 로 넘기는 방법
→inflate
을 하지않고 바로View
를 넘기기 때문에ViewHolder
생성 비용을 아낄 수 있음
예시
class ProductCardView : MaterialCardView { ... } abstract class MyAdapter : RecyclerView.Adapter<MyAdapter.ViewHolder>() { override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder { return RecyclerView.ViewHolder( ProductCardView(parent.context) ) } ... }
단점
- 하지만
유지보수 측면
에서XML
에 비해 효율은 좋지 않음. - 성능을 중심으로 코딩 하는 상황이 아니라면 가급적 사용하지 않는 방법.
Compose LazyComposable 사용
- Compose를 통해 목록형 UI를 만드는 방법
늦은 초기화
를 통해 View가 사용자 눈에 보일 때 UI 렌더링 합니다.
→ 복잡한 Adpater 관리도 사라지며composable function
안에서 쉽게 List 관리가 가능하다.XML
과 혼용해 사용할 수 있기 때문에 List부분만LazyComposable
를 이용해 만드는 것도 하나의 방법
예시
LazyColumn { // Add a single item item { Text(text = "First item") } // Add 5 items items (5) { index -> Text(text = "Item: $index") } // Add another single item item { Text(text = "Last item") } }
'IT > Android' 카테고리의 다른 글
[Android/Refactoring] Memory Leak - 2. 자주 발생하는 안드로이드 메모리 누수 방지하기 (0) | 2023.07.25 |
---|---|
[Android/Refactoring] Memory Leak - 1. 안드로이드 앱에서의 메모리 누수 찾기 (1) | 2023.07.21 |
[Android] RecyclerView Deep Dive - 1. RecyclerView 정의와 동작원리 및 생명주기 (0) | 2023.07.05 |
[Android/FCM] (5) Android에서의 알림 수신 구현 (0) | 2023.03.11 |
[Android/FCM] (4) Android 설정 (0) | 2023.03.07 |