리스트 뷰의 재활용 메커니즘은 어떻게 작동하나요?


저는 이전에 이곳에 올렸던 문제를 갖고 있었습니다. 당시 저는 리스트 뷰와 뷰 재활용을 이용해 최적화된 그리드 뷰에 대한 개념이 없었기 때문에, Luksprog의 답변을 통해 그리드 뷰에 뷰들을 추가하는 방식을 수정할 수 있었습니다. 문제는 지금 제가 이해할 수 없는 문제에 직면해 있다는 것입니다. 아래 코드는 제 BaseAdapter에 구현된 'getView()` 함수에 대한 코드입니다 :


public View getView(int position, View convertView, ViewGroup parent) {
        if(convertView == null) {
            LayoutInflater inflater = LayoutInflater.from(parent.getContext());
            convertView = inflater.inflate(R.layout.day_view_item, parent, false);
        }
        Log.d("DayViewActivity", "Position is: "+position);
        ((TextView)convertView.findViewById(R.id.day_hour_side)).setText(array[position]);
        LinearLayout layout = (LinearLayout)convertView.findViewById(R.id.day_event_layout);

        //layout.addView(new EventFrame(parent.getContext()));

        TextView create = new TextView(DayViewActivity.this);
        LinearLayout.LayoutParams params = new LinearLayout.LayoutParams(0, (int)TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, 62, getResources().getDisplayMetrics()), 1.0f);
        params.topMargin = (int)TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, 1, getResources().getDisplayMetrics());
        params.bottomMargin = (int)TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, 1, getResources().getDisplayMetrics());
        create.setLayoutParams(params);
        create.setBackgroundColor(Color.BLUE);
        create.setText("Test"); 
        //the following is my original LinearLayout.LayoutParams for correctly setting the TextView Height
        //new LinearLayout.LayoutParams(0, (int)TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, 60, getResources().getDisplayMetrics()), 1.0f)   
        if(position == 0) {
            Log.d("DayViewActivity", "This should only be running when position is 0. The position is: "+position);
            layout.addView(create);
        }

        return convertView;
    }

}

문제는 스크롤을 해보면 아래와 같이 0번 포지션이 아닌, 6번과 8번 포지션에 뷰가 추가되어 있으며, 더불어 8번 포지션에는 두개나 추가되어있다는 겁니다. 저는 지금 여전히 리스트 뷰와 그리드 뷰의 사용에 대해 이해하려 하고 있고, 왜 이런 현상이 나타나는지 이해할 수 없습니다. 제가 이 질문을 올리는 주된 이유 중 하나는 리스트 뷰와 그리드 뷰의 재활용 뷰나 이 글에 올라온 스크랩 뷰 메커니즘을 이해하지 못하고 있는 사람들에게 도움을 주기 위함입니다.


추가적으로 리스트 뷰 작동 방식에 대한 전반적 이해를 도울 google IO talk의 링크를 추가합니다. 해당 내용은 여기서 확인할 수 있습니다.

  • 2016년 05월 28일에 작성됨

조회수 209


1 답변


좋아요
0
싫어요
채택취소하기

우선 저 또한 리스트 뷰의 재활용 메커니즘과 컨버터 뷰의 사용 메커니즘을 이해하지 못하고 있었지만, 며칠간의 조사 끝에 android.amberfog에 올라와있는 이미지에서 나타내고 있는 리스트 뷰의 메커니즘에 대해 어느정도 이해할 수 있게 되었습니다.

만약 질문하신 분께서 화면상에 보여줄 수 있는 최대 줄 수를 보여주는 어댑터를 지닌 리스트 뷰를 만든다면, 해당 리스트 뷰를 스크롤해도 줄 수가 증가하지 않는 것을 볼 수 있을 겁니다. 이것은 리스트 뷰를 보다 효율적이고 빠르게 작동하기 위해 안드로이드에서 사용하고 있는 트릭 때문입니다. 위 이미지에서 보여주고 있는 리스트 뷰의 진실은 이렇습니다. 리스트 뷰는 기본적으로 7개의 보이는 아이템을 갖고 있습니다. 만약 사용자가 스크롤을 하면 첫번째 아이템은 더이상 보이지 않게 될 것이고, getView() 함수에서는 해당 뷰(첫번째 아이템)를 recycler에게 넘깁니다. 따라서

System.out.println("getview:"+position+" "+convertView);

위의 코드를 아래와 같이 getView() 함수에 넣어보면

public View getView(final int position, View convertView, ViewGroup parent)
{
    System.out.println("getview:"+position+" "+convertView);
    View row=convertView;
    if(row==null)
    {
        LayoutInflater inflater=((Activity)context).getLayoutInflater();
        row=inflater.inflate(layoutResourceId, parent,false);

        holder=new PakistaniDrama();
        holder.tvDramaName=(TextView)row.findViewById(R.id.dramaName);
        holder.cbCheck=(CheckBox)row.findViewById(R.id.checkBox);

        row.setTag(holder);

    }
    else
    {
        holder=(PakistaniDrama)row.getTag();
    }
            holder.tvDramaName.setText(dramaList.get(position).getDramaName());
    holder.cbCheck.setChecked(checks.get(position));
            return row;
    }

현재 보여지는 열에서는 컨버터 뷰가 null값임을 로그캣을 통해 볼 수 있을 겁니다. 왜냐하면 초기에는 recycler에 아무런 뷰도 들어있지 않고, 따라서 getView() 함수에서는 보여지는 아이템을 위해 각각 새로운 뷰를 만들기 때문입니다. 하지만 스크롤을 하여 첫번째 아이템이 해당 아이템의 상태(이를테면 위 코드에선 TextView의 문구나 체크박스가 체크되어있는지와 같은 상태)와 함께 Recycler로 보내집니다. 이제 사용자가 스크롤을 해도 리스트 뷰는 새로운 뷰를 생성하지 않고 recycler에 들어있는 뷰(컨버터 뷰)를 사용하게 되고, 새로운 여덟번째 아이템이 컨버터 뷰를 이용하여 그려졌기 때문에 로그캣을 통해 컨버터 뷰가 null값이 아님을 알 수 있을 겁니다. 따라서 만약 첫번째 아이템의 체크박스를 체크했었다면, 여덟번째 아이템의 체크박스가 이미 체크된 상태로 보여질 것 입니다. 이것이 바로 성능 최적화를 위해 사용되는 리스트 뷰의 뷰 재사용 방식입니다.

중요한 사실

1. getView()함수가 뷰의 높이를 측정하기 위해 몇몇 자식뷰를 강제로 불러감으로서 리스트를 스크롤하기도 전에 컨버터 뷰가 반환되는 등의 예상치못한 결과가 발생할 수 있으니, 절대 리스트 뷰의 layout_heightlayout_widthwrap_content로 설정하지 마세요. 항상 match_parent나 고정된 폭/높이 값을 사용하시길 바랍니다.

2. 만약 리스트 뷰 다음에 레잉아웃이나 뷰를 사용하고 싶고, 리스트 뷰의 layout_heightfill_parent로 설정하게 되면 리스트 뷰 다음에 오는 뷰가 나타나지 않을 것 같다는 의문이 든다면, 리스트 뷰를 레이아웃 안에 넣어놓는 것이 더 좋은 방법일 겁니다. 예를 들어, 리니어 레이아웃의 높이와 폭을 특정한 값으로 고정시키고, 그 안에 리스트 뷰의 높이와 폭을 리니어 레이아웃과 동일하도록 설정하면, 리스트 뷰는 레이아웃과 동일한 높이을 지닐 것입니다. 이는 getView() 함수에 정확한 높이와 폭으로 뷰를 렌더링하도록 알릴 것이고, getView() 함수가 뷰의 높이나 폭을 측정하기 위해 임의의 아이템을 계속해서 읽어들여 스크롤하기도 전에 컨버터 뷰가 리턴되는 문제 등이 발생하지 않을 것 입니다. 테스트해 본 결과, 리스트 뷰를 리니어 레이아웃 안에 포함시키는 방식은 어떠한 문제도 일으키지 않고 마법처럼 잘 작동했습니다.

01-01 14:49:36.606: I/System.out(13871): getview 0 null
01-01 14:49:36.636: I/System.out(13871): getview 0 android.widget.RelativeLayout@406082c0
01-01 14:49:36.636: I/System.out(13871): getview 1 android.widget.RelativeLayout@406082c0
01-01 14:49:36.646: I/System.out(13871): getview 2 android.widget.RelativeLayout@406082c0
01-01 14:49:36.646: I/System.out(13871): getview 3 android.widget.RelativeLayout@406082c0
01-01 14:49:36.656: I/System.out(13871): getview 4 android.widget.RelativeLayout@406082c0
01-01 14:49:36.666: I/System.out(13871): getview 5 android.widget.RelativeLayout@406082c0
01-01 14:49:36.666: I/System.out(13871): getview 0 android.widget.RelativeLayout@406082c0
01-01 14:49:36.696: I/System.out(13871): getview 0 android.widget.RelativeLayout@406082c0
01-01 14:49:36.706: I/System.out(13871): getview 1 null
01-01 14:49:36.736: I/System.out(13871): getview 2 null
01-01 14:49:36.756: I/System.out(13871): getview 3 null
01-01 14:49:36.776: I/System.out(13871): getview 4 null

제 설명이 완벽하진 않았겠지만 이를 이해하기 위해 며칠을 투자하였습니다. 따라서 저와 같은 다른 입문자들이 제 경험을 통해 도움받을 수 있을 거라고 생각하며, 리스트 뷰의 작동방식이 굉장히 복잡하고 교묘하게 동작하여 입문자들이 이를 이해하는데에 어려움을 겪고 있는만큼 이 답변을 통해 리스트 뷰의 작동방식에 대해 사람들이 조금이나마 더 이해할 수 있길 바랍니다.

  • 2016년 05월 29일에 작성됨

로그인이 필요한 기능입니다.

Hashcode는 개발자들을 위한 무료 QnA사이트 입니다. 작성한 답변에 다른 개발자들이 댓글을 작성하거나 좋아요/싫어요를 할 수 있기 때문에 계정을 필요로 합니다.
► 로그인
► 계정만들기
Close