일 | 월 | 화 | 수 | 목 | 금 | 토 |
---|---|---|---|---|---|---|
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 |
- 안드로이드 유닛테스트란
- ANR이란
- 안드로이드 유닛 테스트
- jvm 작동 원리
- 안드로이드 라이선스 종류
- 안드로이드 레트로핏 사용법
- 큐 자바 코드
- 안드로이드 라이선스
- 서비스 쓰레드 차이
- jvm이란
- 안드로이드 유닛 테스트 예시
- android retrofit login
- rxjava disposable
- rxjava cold observable
- 클래스
- ar vr 차이
- android ar 개발
- 스택 큐 차이
- 플러터 설치 2022
- 2022 플러터 설치
- 자바 다형성
- 멤버변수
- Rxjava Observable
- rxjava hot observable
- 서비스 vs 쓰레드
- 객체
- 스택 자바 코드
- 안드로이드 os 구조
- 2022 플러터 안드로이드 스튜디오
- 안드로이드 레트로핏 crud
- Today
- Total
나만을 위한 블로그
[Android] 리사이클러뷰가 있는 레이아웃의 성능 향상 - 1 - 본문
이 포스팅은 아래의 미디엄 포스팅을 바탕으로 작성했다.
리사이클러뷰는 한 화면에 보여주기 힘들 정도로 많은 데이터를 표시할 때 유용한 뷰다. 그러나 잘못 사용하면 심하게 깜박이거나 버벅이는 문제가 발생해서 사용자 경험에 치명적일 수 있다.
위 링크에서 이것에 대한 해결책을 제시해주고 있는 것 같아서 공유 겸 써두고 봐두기 용으로 포스팅한다.
RecyclerView Initializing
액티비티 소스코드에서 리사이클러뷰에 적용할 수 있는 함수 중 아래와 같은 함수가 있다.
recyclerview.setHasFixedSize(true)
이 함수는 리사이클러뷰에서 아이템을 추가, 제거하는 변동이 생길 때마다 아이템의 크기(가로, 세로)를 계산하지 않도록 리사이클러뷰에 명령하는 함수다. 아이템의 크기가 제각각인 리사이클러뷰를 만들어야 한다면 사용하지 않는 게 낫지만, 대부분의 경우 아이템 크기가 들쑥날쑥한 경우는 없으니 사용하면 좋을 것 같다.
Caching Items
또한 리사이클러뷰에 아래 함수도 적용할 수 있다.
recyclerview.setItemViewCacheSize(10)
setItemViewCacheSize()는 스크롤 때문에 보이던 뷰가 화면 밖으로 거의 완전히 사라질 경우, RecyclerViewPool에 들어가지 않고 캐시에 저장시켜서 다시 화면에 표시됐을 때 어댑터의 onBindViewHolder()를 거치지 않고 그대로 리사이클러뷰에 표시한다.
즉 같은 뷰를 또 그리는 불필요한 과정이 사라지기 때문에 성능을 개선할 수 있다.
RecyclerView Pool
여기서 RecyclerViewPool이란 아래와 같다.
https://developer.android.com/reference/androidx/recyclerview/widget/RecyclerView.RecycledViewPool
RecyclerViewPool을 쓰면 여러 리사이클러뷰 간에 뷰를 공유할 수 있다. 리사이클러뷰에서 뷰를 재활용하려면 RecyclerViewPool의 인스턴스를 만들고 setRecycledViewPool()을 사용한다. 리사이클러뷰는 pool을 제공하지 않으면 자동으로 생성한다.
https://www.tothenew.com/blog/minimize-number-of-layout-inflation-in-recyclerview-adapter/
리사이클러뷰는 내부적으로 ViewPool을 써서 더 이상 화면에 표시되지 않고 재활용할 수 있는 스크랩 뷰를 저장한다. 일반적으로 리사이클러뷰는 해당 리사이클러뷰로만 제한되는 별도의 ViewPool을 생성한다. RecyclerView.setRecycledViewPool() 때문에 여러 리사이클러뷰에서 ViewPool을 공유할 수 있다. 이를 통해 RecyclerView.Adapter의 레이아웃 인플레이션 개수를 줄일 수 있다. 레이아웃 인플레이션은 비용이 많이 드는 프로세스기 때문에 전체 성능이 향상된다...(중략)
중첩 리사이클러뷰는 가끔 볼 수 있는 디자인인데 가로 리사이클러뷰는 잘 스크롤 되지만 세로 리사이클러뷰는 비정상적으로 동작할 때가 있다. 이것은 중첩된 가로 리사이클러뷰의 모든 뷰가 inflation되기 때문이다. 이 모든 뷰에 별도의 ViewPool이 있는데 이것이 이 문제의 원인이다.
이 때 RecyclerViewPool을 써서 리사이클러뷰가 pool을 공유하게 할 수 있는데, 이 방법은 같은 뷰 타입을 쓰는 어댑터를 여러 리사이클러뷰에서 사용할 경우(리사이클러뷰의 똑같은 아이템 XML에서 여러 데이터 종류를 표시하는 등) 유용하다. 중첩된 모든 RecyclerViewPool에 단일 pool을 설정하면 세로 스크롤이 원활해진다.
val viewPool = RecyclerView.RecycledViewPool()
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder {
holder.innerRecyclerView.setRecycledViewPool(viewPool)
}
이렇게 하면 모든 가로 리사이클러뷰는 똑같은 ViewPool을 쓰게 되고 스크랩된 뷰를 재사용할 수 있다. 때문에 뷰 생성 시간이 줄어들고 사용자는 더 부드럽게 스크롤되는 걸 볼 수 있다.
Remove Layout Redundancy
리사이클러뷰 관련 이야기는 아니지만, 만약 아래 이미지처럼 XML에 다중 레이아웃이 있을 경우 UI 성능이 낮아져 사용자 경험에 안 좋을 수 있다.
이 때 <merge> 태그를 쓸 수 있다. 이 태그는 한 레이아웃을 다른 레이아웃에 포함시킬 때 뷰 계층 구조에서 중복 뷰그룹을 제거하는 데 도움을 주는 태그다.
기본 레이아웃이 여러 레이아웃에서 2개의 연속 뷰를 재사용할 수 있는 수직 리니어 레이아웃이라면 재사용 가능한 레이아웃엔 자체 루트 뷰가 필요하다. 그러나 다른 리니어 레이아웃을 재사용 가능한 루트 뷰로써 사용한다면 수직 리니어 레이아웃 안에 수직 리니어 레이아웃이 만들어진다.
그래서 <merge>를 쓰는데, <merge>가 아닌 <include>를 써서도 성능 향상을 이뤄낼 수 있다.
아래는 <merge> 사용 예시다.
이렇게 하면 뷰 로드, 표시 시간을 50% 이상 줄일 수 있고 레이아웃 크기를 더 빨리 조정할 수 있다.
화면에서 공통적으로 사용하는 툴바 같은 게 있다면 이 방식을 써서 만들 수 있겠다.
Load Views on demand
ViewStub은 런타임 시 레이아웃 리소스를 느리게 확장할 때 쓸 수 있는 뷰인데, 보이지 않고 크기가 0이다.
즉 화면에 아무것도 렌더링하지 않기 때문에 화면을 그리는 데 참여하지 않는다. 때문에 ViewStub은 inflate 할 때 매우 저렴하게 뷰 계층 구조를 유지할 수 있다. 필요할 때만 뷰를 불러와서 메모리 사용량, 렌더링 속도를 높일 수 있다. 복잡한 뷰가 생길 경우 리소스 로드를 지연시킬 수도 있다.
DiffUtil vs notifyDataSetChanged()
리사이클러뷰의 데이터 세트가 바뀌었을 때 변경사항을 알려야 할 수 있다. 그 때 notifyDataSetChanged()를 호출한다. 그런데 이 방식은 단점이 몇 개 있다.
- 모든 뷰를 다시 그린다 : 리사이클러뷰는 뷰의 어디가 바뀌었는지 몰라서 모든 아이템이 유효하지 않다고 가정하고 뷰를 다시 생성한다. 즉 현재 리사이클러뷰에 표시되고 있는 모든 아이템과 데이터가 유효하지 않은 것이라고 가정한다. 그래서 표시되는 모든 뷰가 재생성된다. notifyDataSetChanged()를 쓰면 뷰가 빠르게 깜빡이는 걸 볼 수 있는데 다시 그려지기 때문이다.
- 비싼 비용 : 화면을 아예 새로 그리기 때문에 호출할 때마다 새 인스턴스와 리소스가 필요해서 비용이 많이 든다. 이걸 쓰는 것보다 새 어댑터를 할당하는 게 더 싸게 먹힐 수 있다.
그래서 notifyDataSetChanged()의 해결책으로 DiffUtil이 있다.
https://developer.android.com/reference/androidx/recyclerview/widget/DiffUtil
DiffUtil은 두 리스트 간의 차이를 계산하고 첫 번째 리스트를 두 번째 리스트로 변환하는 업데이트 작업 목록을 출력하는 유틸리티 클래스다. 리사이클러뷰 어댑터의 업데이트 계산 시 사용할 수 있다. 백그라운드 쓰레드에서 DiffUtil 사용을 단순화할 수 있는 ListAdapter, AsyncListDiffer를 참조하라
DiffUtil은 차이 알고리즘을 써서 한 리스트를 다른 리스트로 변환하기 위한 최소 업데이트 개수를 계산한다. 이 알고리즘은 이동된 아이템을 처리하지 않으므로 DiffUtil은 결과에서 2번째 패스를 실행해 이동된 아이템을 감지한다. DiffUtil, ListAdapter, AsyncListDiffer는 리스트가 사용 중에 바뀌지 않도록 요구한다. 이는 일반적으로 리스트 자체와 해당 아이템이 모두 직접 수정되지 않아야 함을 의미한다. 대신 컨텐츠가 바뀔 때마다 새 리스트를 제공해야 한다. DiffUtil에 전달된 리스트가 변경되지 않은 아이템을 공유하는 것이 일반적이므로, DiffUtil을 쓰기 위해 모든 데이터를 다시 불러올 필요는 없다. 리스트가 크면 이 작업에 상당한 시간이 걸릴 수 있으므로 백그라운드 쓰레드에서 실행하고 DiffResult를 가져온 다음 메인 쓰레드의 리사이클러뷰에 적용하는 게 좋다...(중략)...리스트가 이미 동일한 제약 조건(게시물 리스트에 대해 생성된 타임스탬프 등)으로 정렬됐다면 이동 감지를 비활성화해서 성능을 향상시킬 수 있다...(중략)
즉 리사이클러뷰 안에서 변경이 발생한 아이템만 업데이트하고 변경되지 않은 아이템은 내버려둔다. 작동 방식만 읽어봐도 notifyDataSetChanged()보다 성능이 더 좋을 거라고 생각할 수 있다.
DiffUtil을 사용했을 때의 장점은 아래와 같다.
- 바뀐 아이템만 다시 그린다
- 기본 애니메이션 제공
보통 DiffUtil을 별도로 쓰는 게 아닌 ListAdapter 안에 companion object로 만들어 사용하곤 한다.
예전에 Jetpack Navigation, Room DB, Flow를 써서 예제 앱을 만든 적이 있는데 여기서 ListAdapter를 사용했었다. 아직 사용 예시를 본 적이 없다면 확인해 보자.
https://onlyfor-me-blog.tistory.com/559
참고한 사이트)
https://jaeryo2357.tistory.com/70
https://skytitan.tistory.com/231
https://gift123.tistory.com/67
'Android' 카테고리의 다른 글
[Android] 리사이클러뷰 페이징 처리 (페이징 라이브러리 X) (0) | 2023.01.30 |
---|---|
[Android] 지연 초기화, lateinit vs Lazy (0) | 2023.01.28 |
[Android] hilt 사용 시 Cannot create an instance of class ViewModel 에러 해결 (0) | 2023.01.25 |
[Android] Flow, LiveData를 써서 네트워크 연결 상태를 확인하는 방법 (0) | 2023.01.20 |
[Android] 앱 시작 시간의 종류 (Cold / Warm / Hot Start) (0) | 2023.01.19 |