Learn & Run

Android - ListView에서의 getView() 메서드, ConvertView와 ViewHolder Pattern를 통해 성능 개선하기 본문

Android

Android - ListView에서의 getView() 메서드, ConvertView와 ViewHolder Pattern를 통해 성능 개선하기

iron9462 2021. 6. 21. 21:10

Android 공부를 시작하다보면 여러개의 아이템을 보여줄 수 있는 ListView를 접하게 된다. 좀 더 진도를 나아가보면 훨씬 더 중요하고 자주 쓰이는 RecyclerView를 접하게 되지만, 이번 포스팅에서는 ListView에 초점을 두어 작성해보려고 한다. 이전 포스팅에서 BaseAdapter를 상속한 Adapter의 getView() 메서드에서 인플레이션 과정이 일어나는 것을 설명했다. 다시 한 번 알아보도록 하고, 이것을 개선시킬 수 있는 방법인 ConvertView와 ViewHolder pattern에 대해서도 알아보도록 하자.

 

 

 

1. BaseAdapter의 getView() 메서드 다시 확인하기

 

위의 이미지는 BaseAdapter를 상속받은 Adapter의 getView메서드 이다. 아이템으로 보여질 각 레이아웃이 inflate메서드를 통해서 메모리에 객체화되는 과정이 일어날 것이다. 그런후에 레이아웃에 포함된 뷰들을 참조하여 필요한 작업을 할 것이고 그런후에 반환해줄 것이다. 하지만 getView메서드의 파라미터에 convertView라는 것이 있다. 분명히 쓸 일이 있으니까 줄것인데 내가 정의한 view랑 convertView는 어떻게 다른건지 알아보도록 하자.

 

 

2. ConvertView란 무엇인가

 

우선 converView를 알아보기전에 ListView에 먼저 알아보아야 한다. 리스트뷰는 다른 일반 View들과 다르게 성능을 위해 설계된 View이다. 위의 getView() 메서드를 다시 생각해보자. 다음에 보여질 새로운 아이템의 데이터를 보여주려면 인플레이션 과정이 계속해서 새롭게 일어나야한다. 굳이 이럴 필요가 있을까? 각 아이템은 내용만 다를뿐 레이아웃은 동일한 아이템이기 때문에 재사용할 수 있으면 얼마나 좋을 것인가? 또한 모든 아이템에 인플레이션 과정이 일어난다면 성능상으로도 좋지 않을 것 이라고 생각해볼 수 있을 것이다. 아무튼 이 문제를 해결하기 위해 설계한 View가 ListView이다. 이 문제를 해결하기 위해 ListView 는 아이템의 전체 개수가 아닌 화면에 보여지는 개수 만큼만 View를 그려놓은 뒤 그것을 재활용하는 구조로 설계 되었다. 예를 들어 아이템의 개수가 50개라 가정하고 실제로 화면에 보여지는 아이템이 5개라고 한다면 모든 아이템들의 View를 생성하여 보여주는 것이 아니라 화면에 보이는 5개의 아이템만 보여준다. 여기서 ListView 스크롤이 내려가게되면 첫 번째 View는 사라지고 6번째의 View가 보여진다. 이 때 6번째의 View는 새로 생성되는 것이 아니라 기존에 만들어진 View를 재사용하게 되는데 그것이 바로 convertView인 것이다. 아래 그림을 확인해보도록 하자.

 

 

위에서 설명한 것처럼 보이던 View가 안보이는 상태가 되면 ConvertView에 저장이되고 다음의 새로운 View가 보일때 저장시켜놓은 ConvertView를 재사용하게 된다.

 

1번에서는 모든 View에서 inflater과정을 거쳤다. 하지반 위의 이미지를 보면 convertView가 null일 때와 그렇지 않을 때를 분기처리 한 것을 확인할 수 있다. convertView가 null이라는 의미는 아직 재사용될 뷰가 없다는 것이기 때문에 처음 화면이 보였을 때 호출되는 스코프라고 생각할 수 있다. 화면에 최대 보일 수 있는 아이템이 5개라고 한다면 if문은 5번만 호출되고 이후에는 재사용된 뷰만을 사용할 것이다. 이로써 인플레이션 과정을 줄이면서 성능적인 장점을 얻을 수 있게 되었다.

 

 

3. ViewHolder는 무엇인가

 

위의 2번 글만 읽어봐도 이미 개선할 것이 모두 끝난 것처럼 보인다. 충분히 인플레이션 과정을 줄이게 되었기 때문이다. 하지만, 조금이라도 좋은 성능을 만들 수 있다면 고려해봐야 하는 법이다. convertView를 사용하며 추가적으로 더 개선시킬 수 있는 코드가 있는 데 (실제 Google I/O에서 권장됨) 개별 View가 존재하는 경우 View의 setTag(), getTag() 메서드를 이용하여  findViewById() 메서드의 빈도수까지 줄일 수 있는 ViewHolder Pattern 방법이다. ListView에서 아이템이 보여질 때마다 getView( ) 메서드가 호출되게 되는데, 여기서 해당되는 데이터를 View에 표시하기위해 findViewById( )를 통해 해당되는 View를 얻어온 후 데이터를 표시하는 것이 일반적이다. 하지만 아이템 View 구조가 복잡할 경우 매번 findViewById( )를 호출하는 것은 매우 값비싼 작업이기 때문에 매끄러운 스크롤을 방해할 가능성이 있다. 실제로 ViewGroup마다 성능 퍼포먼스 차이가 있는 것도 알아두면 좋다. 아무튼 아래 이미지를 통해서 ViewHolder pattern에 대해 감각을 익혀보도록 하자. convertView가 없을 경우에만 View를 생성하고 각각의 데이터들을 findViewById() 메서드를 이용해 찾아준 후에 ViewHolder 멤버 변수에 참조시켜주도록 한다. 그러면 convertView가 재사용 될 때에는 findViewById() 메서드를 부를 필요가 없어지게 되어 성능 퍼포먼스를 증가시킬 수 있다. (여기서 성능 퍼포먼스란 복잡한 레이아웃 구조에 findViewById() 메서드를 많이 사용할 때를 말함)

 

  • convertView만 사용할 때

위의 이미지를 보면, 기존의 아이템이 아닌 새로운 아이템이 화면에 보인다 할지라도 또한 convertView가 null이건 아니건 convertView를 참조해 findViewById 작업을 계속해서 진행해주게 된다. 위의 코드는 매우 짧고 간단한 작업이기 때문에 눈에 띄는 성능 문제가 있진 않을 것이다. 하지만, 이러한 사소한 습관들이 모여서 앱의 성능을 좌지우지 하게 될 것이다.

 

  • convertView + ViewHolder pattern 사용할 때

위의 이미지를 보면 convertView에 저장된 View가 없을 때, 인플레이션 과정과 findViewById() 메서드를 호출하게 되고, 이 때 찾은 View들을 ViewHolder 객체에 각각 저장하게된다. 이후에 convertView가 사용될 시점에서는 findViewById() 메서드를 호출하는게 아닌 기존에 찾아놓은 View들을 단순히 사용만 할 뿐이다. 지금 까지의 흐름을 이해했다면, convertView만 사용하는 것과 ViewHolder pattern까지 적용하는 것의 차이를 느낄 수 있을 것이다.