[개념] Android ViewHolder 패턴

쓰리원·2022년 4월 2일
0
post-thumbnail

1. ViewHolder 패턴을 이해하기 전 사전지식

1. inflate

inflate는 과정은 마크업 코드로 작성되어 있는 XML이 화면을 통해서 보여지게 되는데, 이 때 사전적으로 팽창해서 새롭게 생기는 것을 의미하게 됩니다.

(그림으로 나타내는 inflate 흐름)

즉, 안드로이드에서 inflate는 xml 작성된 View의 코드를 View 객체로 만드는(메모리에 올리는 것) 용도로 쓰는 단어로 알 수 있습니다.

2. setContentView()

setContentView 메서드를 통해서 위의 그림과 같은 inflate 발생합니다. 아래는 AppCompatDelegateImpl.java에 있는 setContentView 메서드 입니다.

위 사진의 setContentView(int resId) 메서드는, 중간에 inflate 함수를 사용하여 resId를 contentParent와 연결 하는것을 확인해볼 수 있습니다.

3. LayoutInflater

FrameLayout이나 Adapter를 사용할 때처럼 전체 화면 중에서 일부분만을 차지하는 화면 구성요소들을 XML레이아웃에서 로딩하여 보여주고 싶을 때 LayoutInflater의 inflate 메서드를 사용할 수 있습니다. setContentView(int resId)를 통해 화면으로 보여지게 하는 것과 같은 방식이라고 볼 수 있습니다.

위의 이미지는 BaseAdapter를 상속받은 Adapter의 메서드들 입니다.

이 중 getView 메서드를 살펴보자면, 아이템으로 보여질 각 레이아웃이 inflate 메서드를 통해서 메모리에 객체화되는 과정이 발생합니다. 이 후 레이아웃의 childView들은 findViewById()를 호출해서 찾을 것이고 찾은 뷰에 데이터를 set해주는 작업을 하고 반환할 것 입니다.

(childView findViewById() 자세한 설명은 아래에서 확인 할 수 있습니다.)

그런데 getView메서드의 파라미터에 convertView라는 것이 있습니다. 그러면 정의한 view랑 convertView는 어떻게 다른건지 알아보겠습니다.

4. convertView

위의 getView() 메서드를 보면 다음에 보여질 새로운 View의 데이터를 출력하기 위해서 inflate가 계속 발생해야 합니다. 이렇게 매번 inflate 하는 작업은 cost가 큰 일이라서 매끄러운 스크롤을 보장하지 못합니다. 그런데 각 View는 View 내부의 Data 값만 다를뿐 레이아웃은 동일합니다. 그래서 재사용을 할 수 있다면 성능이 좋아질 것으로 예측할 수 있습니다.

위 예측을 바탕으로 성능을 개선한 View가 ListView 입니다. ListView 는 아이템의 전체 개수가 아닌 화면에 보여지는 개수만 View를 생성합니다. 그러므로 뷰를 추가적으로 볼 때 inflate가 계속 발생하지 않고 기존의 뷰를 재사용하는 구조로 설계 되었습니다. (아래 그림 참고)

예를 들어 위 그림을 통해 View의 개수가 1000개라 가정하고 실제로 화면에 보여지는 View이 5개라고 한다면 모든 View를 생성하여 보여주는 것이 아니라 화면에 보이는 5개의 View만 보여줍니다.

여기서 ListView 스크롤이 내려가게되면 첫 번째 View는 사라지고 6번째의 View가 보입니다. 이 때 6번째의 View는 새로 생성되는 것이 아니라 기존에 만들어진 View를 재사용하게 되는데 그것이 바로 convertView입니다.

위의 코드와 같이 convertView 가 null일 때만 inflate 되도록 해 ListView를 위한 Adapter의 getView를 구현하게 됩니다.

2. ViewHolder 패턴

뷰의 구성 요소들을 findViewById()를 통해 적용시킬 수 있습니다. 그러나, 데이터가 많아질 경우 계속 findViewById()를 호출하면 많은 cost로 인해 느려질 것 입니다. 왜 느려지는지 아래에서 알아보겠습니다.

1. findViewById() (View.java)

위의 코드를 살펴보면 TextView, ImageView..등과 같은 단일 뷰에 대한 findViewById() 는 View에 지정된 mID와 매개변수가 같으면 return 하고 일치하지 않으면 null을 return 합니다. 그러므로 View 클래스의 findViewById를 반복적으로 호출하게 될 경우 비용이 크지 않을 것으로 예상됩니다. 문제는 아래의 ViewGroup.java에서의 findViewById() 입니다.

2. findViewById() (ViewGroup.java)

View.java(단일 뷰)인 경우는 자기와 id가 같은지 확인합니다. ViewGroup.java(레이아웃)의 경우에는 자기와 id가 같은지 비교하고 같지 않은 경우에 자식들에 대해 하나씩 findViewById를 호출하는 형태로 됩니다.

위의 코드를 확인하면 Layout에 포함되어 있는 자식(mChildren)과 카운트(mChildrenCount)를 멤버변수로 가지고 있습니다. 그 카운트만큼 반복문을 통해 해당하는 자식 View를 찾고 존재할 경우 return 없을 경우 null을 return 하고 있습니다.

여기서 성능 저하가 발생하는 이유를 알 수 있습니다. Layout이 깊을수록 자식 ViewGroup이 많을수록 이중, 삼중 반복문이 돌게 되고 반복적인 호출이 일어났을 때 cost가 커지게 됩니다.

(이 과정은 자료구조에서 배우는 트리의 깊이 우선 탐색(DFS 탐색)과 같습니다.)

3. ViewHolder 패턴

ListView에서 아이템이 보여질 때마다 getView() 메서드가 호출되게 되는데, 여기서 해당되는 데이터를 View에 표시하기위해 findViewById()를 통해 해당되는 View를 얻어온 후 데이터를 표시하는 것이 일반적 입니다.

하지만 아이템 View 구조가 복잡하면 위의 내용에 따라 매번 findViewById()를 호출하는 것은 cost가 큰 작업이기 때문에 매끄러운 스크롤을 방해할 가능성이 있습니다.

이를 위해 나온 패턴이 ViewHolder Pattern 입니다.

ViewHolder는 각 뷰 객체를 뷰 홀더에 보관해서 저장해놓고 사용하기 위한 객체입니다. 즉, '전에 만들어 둔 것을 재사용 하겠다' 입니다.

예를 들면 데이터가 1번부터 1000번까지 1000개 있고, 이를 리스트 형태로 보여줄 때 스마트폰의 화면 크기 상 1번부터 4번까지 보여준다고 가정하겠습니다.

사용자가 아래로 스크롤을 하게 되면, 최상단에 있던 1번 및 2번 아이템의 레이아웃은 눈에 보이지 않고, 5번 및 6번 아이템이 화면에 새롭게 보일 것 입니다.

이 때, 5번 및 6번 아이템을 화면에 표시하기 위해 findViewById() 를 계속 호출해서 레이아웃에 데이터를 바인딩하지 않고, 기존에 1번 및 2번 아이템을 그려줄 때 사용했던 View를 재사용하여 이미 불러왔었던 레이아웃에 데이터를 넣어주는 것 입니다.

 override fun getView(position: Int, convertView: View?, parent: ViewGroup?): View? {
        var convertView = convertView
        val holder : ViewHolder

        if (convertView == null) {
            convertView = LayoutInflater.from(parent?.context)
                .inflate(R.layout.board_list, parent, false)

            // 처음 화면을 실행 시, viewholder를 사용
            holder = ViewHolder()
            holder.title = convertView?.findViewById(R.id.titleText)
            holder.content = convertView?.findViewById(R.id.contentText)
            convertView.tag = holder
            
        } else {
            holder = convertView.tag  as ViewHolder
        }
        // 실제 데이터를 holder와 연결
        holder.title!!.text = List[position].title
        return convertView
    }
    private class ViewHolder {
        var title : TextView? = null
        var content : TextView? = null
    }
}

ViewHolder 패턴을 리스트뷰를 기준으로 아래와 같이 사용이 가능합니다. 위 코드를 보면 convertView가 null 일 때 inflate 하고, ViewHolder를 생성해 각요소를 findViewById 를 통해 저장합니다.

그리고, 다음부터는 ViewHolder를 Tag로 불러와서 재사용하게 됩니다. 이렇게하면 xml 리소스에 findeViewById로 직접 접근하지 않아도 됩니다.

그리고, 참고사항으로 여기서 ViewHolder 클래스를 내부 클래스로 만드는 이유는 현 위치의 Adapter만 관련이 있고, 소스의 가독성을 높이기 위함입니다.

3. Outro

이것으로 우리는 ViewHolder 패턴에 대해서 자세히 알 수 있었습니다. 이제 왜 필요한지는 알게 되었으니 실전에서 사용을 해볼 것 입니다.

다음 내용으로는 리스트뷰와 리사이클러뷰에서 ViewHolder를 어떻게 쓰고 있는지 코드로 대해서 자세히 알아보겠습니다.

4. reference

https://www.crocus.co.kr/1584
https://medium.com/@LIP/layout-inflation-as-intended-c7626f269461
http://androidcode.pl/blog/wzorce/viewholder/
https://developer.android.com/guide/topics/ui/layout/recyclerview

profile
가장 아름다운 정답은 서로의 협업안에 있다.

0개의 댓글