땀이 삐질삐질 나는 개발 일기

[안드로이드] Recyclerview 제대로 알고 쓰자 ! 본문

개발 Tip

[안드로이드] Recyclerview 제대로 알고 쓰자 !

삐질 2020. 3. 10. 21:08

안녕하세요 개발자 삐질입니다

 

오늘 제가 소개하고 싶은 내용은 제대로 된 리사이클러뷰의 개념 입니다.

 

여러분, 리사이클러뷰는 리스트뷰 -> 리사이클러뷰  즉 리스트형식의 UI를 구성하는 데 있어서 없어서는 안될

 

범국민 컴포넌트가 되어버렸습니다.

 

하지만 말이죠..? 리사이클러뷰... 제대로 알고 쓰십니까?

리사이클러뷰란 뭔가요? 어떻게 작동하는지 알고 계신가요?

 

이 질문에 틀리지만 자신있게 대답할 수 있는 분들이 얼마나 있을까 저는 묻고싶습니다.

 

보통은 이런 질문에 대답은 이렇습니다.

A: 어.. 화면에 보이는 양 만큼 홀더를 처음에 만들구요~  화면에서 그 홀더가 사라지면 그걸 떼서 밑에 갖다 붙이고 다시 바인드뷰홀더에서 데이터를 셋  해요 . 

 

아니 그래서 어떻게 갖다 붙이는 건데요~~~  A: .........?

 

이제부터 제대로 알아보자구요! 

(정확히는 리사이클러뷰가 리사이클러뷰 어댑터를 핸들링 하는 과정입니다.)

 

리사이클러뷰는 크게 7가지의 패턴을 거칩니다.  -> 오버라이드 할수 있는 메서드가 내부적으로 호출됨

 

-  리사이클러뷰 화면에 붙이기 - ( 최초 1번 )  

onAttachedToRecyclerView

 

1. 홀더의 생성  

onCreateViewHolder

2. 홀더의 바인딩 

onBindViewHolder

3. 홀더를 화면에 온전히 보여짐 

onViewAttachedToWindow

4. 홀더를 화면에 온전히 보여지지 않음 onViewDetachedFromWindow

 

5. 재활용 할 홀더를 가져오기 onViewRecycled

 

............

-  리사이클러뷰 화면에 떼어내기 - ( 마지막 1 번) onDetachedFromRecyclerView

 

의 반복작업입니다.  리사이클러뷰가 알아서 해주는 작업을 내가 무엇하러 알아야 되냐구요??

 

아뇨.. 이 과정에 대해 대수롭지 않게 여기신다면 아마도 (Memory Leak)을 경험하고 계실 확률이 굉장히 큽니다. 또는 잠재적인  (Memory Leak)을 떳떳하게 여기고 계신 거구요.

 

프로그래밍의 기본은  메모리 할당 / 메모리 해제가 Pair(짝)를 이루는 작업 입니다. 이것이 바로 좋은 프로그래밍입니다.

 

대게 리사이클러뷰를 잘 모르고 쓰는 분들은 , 홀더의 생성 , 홀더의 바인딩 까지만 코드를 작성하시고 그 이후부터는 신경쓰지 않을거에요.

왜냐면, 당장 간단한 이미지나 텍스트 핸들링에서는 크게 문제가 발생하지 않거든요 ^^ ... 하지만 이게 동영상이라던지 아주 큰 미디어작업 또는 메모리를 필요로 하는 작업일 경우 할당 된 메모리를 해제해야 하는 상황에 해제하지 않고 계속 할당만 하게 된다면 지옥을 맛볼 겁니다.

 

 

아래 예시를 가져왔습니다.

 

 

자 아무 행동도 하지 않고 처음 리사이클러뷰에 어댑터를 Set 했을 때 호출되는 과정입니다.

 

1. 리사이클러뷰가 화면에 어태치 됨

2. 화면에 총 10개의 뷰 홀더를 그리기위해 생성함 ( 메모리 할당 )

3. 각 홀더를 View와 바인딩 함 ( 아직 화면에 그려짐 허나 온전히 홀더의 전체가 보여지는 것은 아님 )

4. 각 홀더를 화면에 붙임 ( 이때 View와 Binding 된 홀더가 온전히 그려짐 ) 

 

 

처음 이 작업 이후,  더 아래에 있는 아이템을 보기 위해 스크롤 해 보겠습니다. 그때 호출되는 메서드는 아래와 같습니다.

 

 

현재 홀더의 상태와, 호출된 로그를 비교 해 볼까요?

 

0번 홀더는 사라지고 있지만 10번째 홀더는 보여지고 있습니다. 그렇기 때문에  0번 홀더는 아무런 동작이 없습니다.

 

onCreateViewHolder: 11

onBindViewHolder: 11

 

 

가 호출되었죠 ( 포지션은 +1 된겁니다 로그상 보기 쉽게 ) 이때 ,11번 홀더의  onViewAttachedToWindow가 호출되지 않은 이유는 Binding되어 그려지기는 하지만 완전하게 보여지지는 않는 상태이기 때문에 onViewAttachedToWindow가 호출되지 않는 겁니다

 

이때 최초 10개만 보여줄 수 있는 영역에 걸쳐진 홀더들 까지 총 11개의 홀더를 메모리 할당했습니다. 

그리고 View 와 Bind했고, 그렸습니다.

그럼 이제 0번째 홀더가 사라지게 된다면 어떤 메서드가 호출이 될까요? 

사라진 0번을 바로 12번째 홀더로 재활용 할것이라 생각하시나요?

그렇다면 다시 onBindViewHolder()가 호출이 되서 해당 홀더를 View와 Bind해야할까요? 

.

.

.

.

.

.

.

.

 

아닙니다.  사라진놈은 아직 그대로 사라져있어요. 새로 12번째 홀더가 나타나면 다시  홀더를 생성 ( 메모리 할당) 하게 됩니다.

 

onCreateViewHolder: 12  -> 12번째 홀더 생성

 

onBindViewHolder: 12 -> 12번째 홀더 바인딩

 

onViewDetachedFromWindow: 0 -> 0번이 사라짐

 

이상하지 않나요? 우리가 흔히들 알고 있는 상식은 화면에서 사라진 뷰는 다시 아래에 가지고 와서 재활용을 할 텐데? 그게 리사이클러뷰 아닐까요? 이 대답은 반은 맞지만 반은 틀린 대답입니다.

 

결과적으로 리사이클러뷰는 사라진 뷰를 "바로" 재활용하진 않습니다. 

다시 조금더 로그를 살펴보도록 하죠.

 

 

스크롤을 더 하다보니 13번째 홀더가 만들어지고 ... 14번째 홀더가 만들어지고 ... 1번 홀더가 디태치되고, 2번 홀더가 디태치 되고. .

엇!!!! 새로운 녀석이 호출 되었습니다. 

 

onViewRecycled: 0

우리가 찾고 있던 녀석이 바로 이 녀석입니다. 이 녀석의 의미는 나 지금 재활용 할 홀더를 가져왔어 ! 라는 의미입니다. 

자 곧이 조금 더 스크롤 해 보겠습니다.

앗 15가 등장했는데 onCreateViewHolder가 아니라 바로  onBindViewHolder 입니다. 

이제서야 납득이 가는 부분입니다. ( ^^..홀더 자체의 레퍼런스 값을 찍어보면 같음을 확실히 알수 있습니다.)

0번째 홀더가 재사용 되겠음을 알리는 onViewRecycled: 0 가 호출이 되고, 이녀석이 다시 15번째 홀더로써의 Bind가 되는 부분입니다.

 

리사이클러뷰는 이 동작을 계속해서 반복하게 됩니다.

 

이후 리사이클러뷰를 파괴하게 되면 화면에서 사라짐과 동시에 onDetachedFromRecyclerView가 호출됩니다.

이렇게 길게 리사이클러뷰의 동작방식을 알아보았습니다. 그런데 중요한 것은 아직 남아있습니다.

 

위에 저는 Memory Leak 현상을 언급했습니다.  왜일까요? 

사실상 텍스트 뷰라던지 이런 라이프 사이클을 가지는 뷰들은 웬만해서는 알아서 회수가 되는 편입니다만

동영상 플레이어  또는 Bitmap등 그래픽을 담당하는 녀석들은 주로 뷰가 파괴된다고 해서 메모리에서 알아서 또는 바로 회수되지는 않습니다.  한 마디로 메모리에 일정기간 상주 해 있는다는 얘기지요 . 특히나 이런 녀석들을 익명 인스턴스라던지 잘못된 코드로 작성했을 경우 영원한  Leak을 유발할 수 있겠죠 .

 

바로 이런 녀석들을 필요에 따라 onViewDetachedFromWindow onViewRecycled 안에서 회수 ( 메모리 해제 )를 명시적으로 진행 해 주어야 합니다. 또는 이미지 모듈인 Glide모듈로 ImageLoad Request를 할때 이미 안 보일 뷰거나 재활용돼서 Request를 Cancel해야 할 때 등등 이로써 가장 중요한 리사이클러뷰 사용법과 , 주의점을 설명했습니다. 이 과정은 필수입니다. 안 해도 문제생기지 않던데? 라면 "아직은 운이 좋은 상태"인 겁니다.  반드시 이점 참고해 개발할 수 있도록 하셨으면 좋겠습니다.

 

 

 

------------------------리사이클러뷰 오버라이드 메서드 ------------------------------------


class RecyclerTestAdapter : RecyclerView.Adapter<RecyclerView.ViewHolder>() {
    var holderSize =0
    override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder {
        Log.e("onCreateViewHolder","${holderSize++}")
        var binding =
            ItemRecyclerTestViewBinding.inflate(LayoutInflater.from(parent.context), parent, false)
        return ItemViewHolder(binding)
    }

    override fun getItemCount(): Int {
       return 50
    }

    override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) {
        if(holder is ItemViewHolder){
            holder.bind()
        }
    }

    override fun onDetachedFromRecyclerView(recyclerView: RecyclerView) {
        super.onDetachedFromRecyclerView(recyclerView)
        Log.e("onDetachedFromRecyclerView","onDetachedFromRecyclerView")
    }

    override fun onFailedToRecycleView(holder: RecyclerView.ViewHolder): Boolean {
        return super.onFailedToRecycleView(holder)
        Log.e("onFailedToRecycleView","onFailedToRecycleView")

    }



    override fun onViewAttachedToWindow(holder: RecyclerView.ViewHolder) {
        super.onViewAttachedToWindow(holder)
        Log.e("onViewAttachedToWindow","${holder.adapterPosition}")

    }

    override fun onViewRecycled(holder: RecyclerView.ViewHolder) {
        super.onViewRecycled(holder)
        Log.e("onViewRecycled","${holder.adapterPosition}")

    }


    override fun onViewDetachedFromWindow(holder: RecyclerView.ViewHolder) {
        super.onViewDetachedFromWindow(holder)
        Log.e("onViewDetachedFromWindow","${holder.adapterPosition}")

    }

    override fun onAttachedToRecyclerView(recyclerView: RecyclerView) {
        super.onAttachedToRecyclerView(recyclerView)
        Log.e("onAttachedToRecyclerView","onAttachedToRecyclerView")

    }

    inner class ItemViewHolder(var binding: ItemRecyclerTestViewBinding) :
        RecyclerView.ViewHolder(binding.root) {

        fun bind() {
            Log.e("onBindViewHolder","${adapterPosition}")
            binding.textview.text="해당 어뎁터는 ${adapterPosition} 홀더입니다"

        }
    }

}

 

초급 안드로이드 개발자를 위한 카카오톡 오픈 채팅방을 운영 중 입니다. 
(저는 마냥 친절한 방장은 아닙니다.  다만, 다 같이 성장하고 싶은 방장이며 개발자 입니다)

https://open.kakao.com/o/gn4xqQ6open.kakao.com/o/gH0XvThcopen.kakao.com/o/gH0XvThc

Comments