Android

[Android] Paging3 Frame Drop Issue

Sangyoon98 2023. 4. 23. 19:29

페이징 라이브러리 사용 중 프레임 드랍 이슈를 겪게 되어 작성해본다.

 

문제점

페이징된 목록들을 스크롤 시 해당 에러를 출력하면서 프레임이 끊기며 스크롤 되는 문제가 발생했다.

"Skipped 611 frames! The application maybe doing too much work on its main thread."

 

시행 착오

먼저 프레임 드랍과 함께 출력된 에러를 보고 메인 스레드에서 너무 많은 작업을 했다길래
페이징 데이터를 UI에 그리는 과정에서 너무 많은 UI 소스 때문에 과부하가 걸린다고 생각했다.

(사실 이때부터 페이징 데이터를 로드하는 작업이 너무 많다고 알려주고 있었는데 다르게 생각해버렸다..)

그럴만도 한게 사진처럼 카드뷰 하나에 벡터 이미지와 받아오는 데이터들이 많기도 하고

제작 일정 상 빠르게 만들어야 해서 API 통신부터 페이징, 레이아웃 구성까지 한큐에 정신없이 제작했기 때문에 포인트를 잘못잡았다.

 

한번에 제작했기 때문에 내가 뭘 잘못했나 싶어서 통신 코드부터 페이징 데이터, 레이아웃을 다 점검했었다.

 

API통신으로 가져오는 데이터 중 필요없는 데이터는 최대한 지워보기도 하고,

페이징을 잘못했나 싶어 Paging 소스와 어댑터, 뷰홀더 할 거 없이 다 확인해봤다.

 

그래서 생각한 단순한 방법이 페이징을 로드하는 함수를 메인 스레드가 아닌 Dispatchers.IO에서 동작하도록 했다.

suspend fun load(params: LoadParams<Int>): LoadResult<Int, RowItems> =
    withContext(Dispatchers.IO) {
        ...
        //페이징 코드
        ...
    }

하지만 프레임 드랍 이슈는 계속 있었다.

이때부터 점점 산으로 가기 시작한 것이다!

 

페이징에 로드 데이터 내부에 문제가 있을 것이라 생각하고 로드 데이터를 건들기 시작했다.

해당 API에서는 데이터를 출력하는 시작 아이템 위치와 종료 아이템 위치값을 모두 전달해줘야 한다.

여기서 종료 아이템 위치값을 시작 아이템 위치값에 간단한 연산을 통해 구해서 페이징 데이터로 넘겨주는 코드가 있었는데

연산쪽에서 지연이 생기는 줄 알고 없애도 보고 로직도 바꿔봤다.

그래도 안되길래 구글링 열심히 하면서 페이징 데이터 스트림도 건들여보고 다 해봤다.

 

뷰에 문제가 있겠구나 싶어 레이아웃 부분을 보다가 NestedScrollView라는 글씨를 보고 순간 아차싶었다.

 

문제 원인

현재 페이징이 사용된 레이아웃의 구조는 다음과 같다.

액티비티 내부에 앱바와 TabLayout과 ViewPager를 같이 사용하고 있고

ViewPager 프래그먼트 내부에 페이징 처리된 RecyclerView가 위치해 있다.

중간에 NestedScrollView가 있는데 이는 스크롤시 AppBar의 Material Guildine을 따르기 위해 존재한다. (스크롤시 AppBar 축소효과 또는 색상 변경)

 

NestedScrollView와 Paging의 특성이 상반된 기능을 가지고 있어 문제가 발생한 것이다.

 

NestedScrollView

NestedScrollView에 대해 간단하게 알아보면 ScrollView처럼 사용된다고 한다.

ScrollView 처럼 뷰를 스크롤하는 기능이지만 콘텐츠의 길이만큼 뷰를 늘리고 뷰를 통째로 스크롤 하는 방식이다.

뭔 차이냐 싶겠지만 스크롤이 두개 이상 있는 뷰같은 경우에 스크롤의 부자연스러움을 막고자 있는 기능이다.

RecyclerView 또한 스크롤 기능이 있기 때문에 NestedScrollView 내부에 많이 사용한다.

 

RecyclerView(Paging)

RecyclerView에 대해 간단하게 알아보면,

ListView는 스크롤시 아이템 뷰가 사라지면 삭제되고 아래에 새로운 아이템이 생성되는 반면에

RecyclerView는 아이템 뷰의 재사용성을 높이기 위해 스크롤시 아이템 뷰가 뷰에서 사라지면 맨 아래 뷰로 이동을 해서 다시 보여주는 방식이다.

Paging 또한 스크롤시 마지막 아이템뷰에 도달하면 아래에 새로운 아이템 뷰로 이동을 해서 다시 보여주게 된다.

 

문제 파악

결론은 Paging3는 맨 아래 아이템 뷰에 도달하면 다음 아이템 뷰를 띄워야 하는데,

NestedScrollView가 내부에 있는 RecyclerView의 콘텐츠 길이(아이템 뷰)만큼 뷰를 늘려서 다음 아이템 뷰가 보여지게 되고,

다음 아이템 뷰가 생성됨에 따라 NestedScrollView는 또 그만큼 뷰를 계속 늘리게 돼서 결국 동시에 모든 페이지를 로드하게 된 것이다.

데이터의 양도 1000개가 넘는데다 모조리 서버와 통신해서 가져오기 때문에 네트워크 통신 코루틴이 못해도 100개 이상 동시에 실행되는걸 UI 스레드가 못버티는게 당연했던 것이였다.

 

글을 작성하다가 어느 블로그에서 비슷한 문제를 포스팅해놓은 글을 발견해서 잠깐 빌려 쓰자면

StackOverflow 답변
- in the nested scrolling case you must be careful not to give RV an infinite height as otherwise it will try to layout / load all items
- If you give RV an infinite height, it will try to bind every item because it thinks every item is visible. Nested scrolling is just not a supported use-case, you need to give RV a finite / bounded height

- NestedScroll의 경우 리사이클러뷰가 모든 아이템을 로드하려고 하기 때문에 리사이클러뷰에 높이를 지정해줘야 한다
- 리사이클러뷰에 높이를 무한대로 설정하면 모든아이템이 보이는것으로 생각하기 때문에 모든 아이템을 바인드하려고 한다. 따라서 높이를 지정해 주어야 한다

그렇다면, 해결방법은 감싸주고있던 NestedScrollView를 없애주거나 리아시클러뷰의 높이를 지정해주는 것
NestedScrollView를 없애고 RecyclerView내의 메서드인 nestedScrollingEnabled를 사용해서 중첩스크롤을 해결 (default true)
 

[Android] Paging3 load all pages at once - 동식이 블로그

[Android] Paging3 load all pages at once

dongsik93.github.io

NestedScrollView를 없애주거나 RecyclerView의 높이를 지정해주면 된다고 한다.

 

문제해결

본인 또한 NestedScrollView를 제거해 문제를 해결했다.

수정된 페이징이 사용된 레이아웃의 구조는 다음과 같다.

안그래도 ViewPager에 사용하는 프래그먼트들의 높이가 NestedScrollView 때문에 다른 프래그먼트들에게도 영향을 끼쳐 ViewPager를 제거했었는데 이번 문제로 인해 NestedScrollView를 제거하고 ViewPager와 프레임 드랍 이슈 모두 잡을 수 있게 됐다.

NestedScrollView를 제거함에 따라 Material Guideline을 지키는 AppBar 기능이 제한됐지만 나름 만족하는 결과가 나와 만족하고 있다.