본문 바로가기
Programming/Android

ListView 성능개선에 관한 글[퍼온 글]

by ciwhiz 2012. 12. 6.
http://croute.me/284


리스트뷰 레퍼런스



ListView 관련 포스트

[Android] 커스텀 리스트뷰 : Custom ListView - Layout : 레이아웃

[Android] 리스트뷰 아이템 클릭 이벤트 - ListView Item Click

[Android] ListView(리스트뷰) 아이템 추가/삭제




 ListView, [View getView() in Adpater]

ListView는 화면에 보여줄 하나의 아이템에 해당하는 뷰를 ArrayAdapter의 getView 메소드에서 만들어냅니다.

getView에서 View를 생성하고, 데이터를 입혀서 반환하는 거죠. 기본적으로는 아래와 같습니다.

1
2
3
4
public View getView(int position, View convertView, ViewGroup parent)
{
    return super.getView(position, convertView, parent);
}



이제 이 getView 메소드를 재구현하는 여러가지 방법들을 알아보도록 하겠습니다.


Dumb
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
LayoutInflater mInflater;
 
...
 
public View getView(int position, View convertView, ViewGroup parent)
{
    convertView = mInflater.inflate(R.layout.list_row, null);
 
    TextView tvData = (TextView) convertView.findViewById(R.id.list_row_tv_data);
    ImageView ivPhoto = (ImageView) convertView.findViewById(R.id.list_row_iv_photo);
 
    ...
    // 데이터 세팅
 
    return convertView;
}

위의 방법은 매 position마다 View를 새로 생성합니다.
하나의 View를 새로 생성해 내는건 당연히 엄청난 노력이 필요하겠죠.

접기

 ListView, Adapter에서의 View 재활용


위와같이 리스트뷰가 있을때, 스크롤을 해본다고 합시다.

그럼 아래처럼 될거에요.
계속해서 새로운 뷰를 만들어 내는게 아닌, 기존에 만들어진 뷰를 다시 사용해서 화면에 보여주는거지요.


그래서 아래의 Corret code에서는 Adapter의 이러한 View 재활용을 이용해 View가 null인지를 체크하고,
null일때(아직 화면에 보여진적이 없을때)만 뷰를 inflate해서 생성합니다.
이 경우 조금이라도 화면에 보여지는 뷰까지 합쳐서 한번에 화면에 보여지는 뷰의 갯수는 10개입니다.(0~9)
그리고 이렇게 10개만 View를 inflate해서 만들어 놓은 후 계속해서 재활용해서 사용하는 것이지요.

한번에 화면에 보여지는 갯수 만큼만 생성해서 사용할 수 있다는 것을 기억하세요~ ㅎ
 

접기




Correct
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
LayoutInflater mInflater;
 
...
 
public View getView(int position, View convertView, ViewGroup parent)
{
    if(convertView == null)
    {
        convertView = mInflater.inflate(R.layout.list_row, null);
    }
 
    TextView tvData = (TextView) convertView.findViewById(R.id.list_row_tv_data);
    ImageView ivPhoto = (ImageView) convertView.findViewById(R.id.list_row_iv_photo);
 
    ...
    // 데이터 세팅
 
    return convertView;
}

Correct code는 Dumb code에서 조금 개선한 코드입니다.
Adapter에서는 View를 자동적으로 재활용하기 때문에 그걸 이용해서, 이미 뷰를 생성해 놓은 상황이라면 새로 생성하지는 않고, 그 뷰를 통해 자식 뷰들을 찾아와 데이터를 바꿔서 리턴하는 것입니다.

하지만 여기서도 약간의 속도저하의 원인이 남아있는데요, 그건 바로 findViewById()라는 메소드 입니다.

어떤 뷰에서 입력한 id에 해당하는 View를 리턴해 주는 메소드인데요, 이 메소드는 layout에서 계층구조가 심해질수록 더 오래걸리겠죠. 
그래서 findViewById 메소드까지 최소한으로 사용하도록 개선하면 더 좋습니다.




Fast
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
LayoutInflater mInflater;
 
...
 
class ViewHolder
{
    TextView tvData;
    ImageView ivPhoto;
}
 
...
 
public View getView(int position, View convertView, ViewGroup parent)
{
    ViewHolder holder;
 
    if(convertView == null)
    {
        holder = new ViewHolder;
 
        convertView = mInflater.inflate(R.layout.list_row, null);
 
        holder.tvData = (TextView) convertView.findViewById(R.id.list_row_tv_data);
        holder.ivPhoto = (ImageView) convertView.findViewById(R.id.list_row_iv_photo);
         
        convertView.setTag(holder);
    }
    else
    {
        holder = (ViewHolder) convertView.getTag();
    }
 
    ...
    // 데이터 세팅
 
    return convertView;
}
 



이 세가지 방법에 대해 성능을 측정한 결과라고 합니다.
10,000개의 데이터를 가진 List를 통해 ListView 및 Adapter를 만들고,
위의 세가지 방법으로 getView를 만들어서 측정을 한것이죠.



//
이번에는 마지막에 살펴봤던 ListView를 더 쓸모 있게 만드는 방법을 소개합니다.


내용이 좀 길어져서 2파트로 나누었습니다.

파트 1은 ListView 위젯을 더 복잡하게 꾸미고 성능을 향상 시키는 방법을 소개하고,

파트 2는 사용자와 Interact가 가능한 ListView를 먼저 살펴보고 이를 재활용 하는 방법에 대해 다룹니다.


1. XML을 이용해 꾸미기

스마트폰의 제한적인 스크린 사이즈 때문에 List위젯들(ListView, spinner등)의 중요성은 아주 크다.

list 위젯의 생김새를 결정하는 것은 전적으로 어떤 구성의 Adapter를 해당 list위젯에 연결하느냐에 달려있다.

다음 예는 전 장에서 사용한 썰렁한 text 대신, 아이콘+Text로 이루어진 행(row) xml layout 을 ListView에 적용한 예이다.

소스 펼치기

다음은 실행 화면이다.

2. Java 코드 내부에서 동적으로 꾸미기

위 단락의 예제와 같이 별도의 XML layout파일을 아답터에 적용 하고 List에 아답터를 연결하여 list의 각 row의 생김새를 바꾸는 작업은 어렵지 않다.

하지만, 다음과 같은 경우에는 XML layout 파일 기반의 방법은 사용이 어렵다.

  • 모든 행(row)가 일률적인 layout으로 구성되지 않는 List.
  • 모든 row가 일률적 layout이라도, layout을 구성하는 content가 제 각각일 때. (eg. 각 row마다 icon이 다를 때 등)

이런 경우 Adapter를 상속하는 커스텀 adapter를 만들고 getView()를 오버라이딩해서 문제를 해결할 수 있다.

소스 펼치기


위 소스에서 나오는 LayoutInflater 클래스는 쉽게 말해서 XML parser이다.

XML 형태로 제공된 layout(\res\layout\row.xml)을 View 객체로 구조화 하고 View 객체가 포함하는 여러 위젯 (여기서는 LinearLayout, ImageView, TextView)을 update 함으로 각 행(row)에 대한 layout을 커스터마이징 한다.

실행 결과는 다음과 같다. 글자수가 4자 이상인 row의 아이콘만 x로 바뀐 것을 볼 수 있다.

3. ListView 성능향상을 위한 여러 방법

위에서와 같이 getView() 를 오버라이딩하여 커스텀 뷰를 얻는 것은 표면적으론 아무 문제가 없지만 ListView의 특징하나를 고려할 때 몇 가지 문제가 있음을 알 수 있다.

문제의 근원은 "ListView는 표현된 row 정보를 cache하지 않는다"인데 이는 다시 말하면 화면에 새로 표현되는 row는 무조건 getView()를 호출한다라는 뜻이다.

(방 금 전에 화면에 그려진 row라고 하더라도 화면을 벋어났다 다시 화면에 그려질 때 getView()가 또 호출된다. 스크롤 바를 이용해 ListView를 위아래로 왔다 갔다 할 때 계속 호출될 getView()를 생각해보면 이해가 쉽다)

이와 특징은 다음과 같은 문제를 일으킨다.

  • 반복된 View객체의 생성: getView()가 호출 시 마다 View객체를 새로 생성하고 이를 소멸(garbage collection)해야 함.
  • 반복된 layout XML 파일 파싱. (LayoutInflater를 이용)
  • 반복된 findViewById()의 호출.


휴 대용 기기 플랫폼에서 전원사용에 대한 중요성은 무척 크며, 안드로이드도 예외는 아니다. 위와 같은 불필요한 operation은 불필요한 전원소모로 이어진다. 또, 불필요한 operation은 사용자 UI의 반응 속도를 저하 할 수도 있다.

결론적으로, 배터리 소모를 최소화 하고, 빠른 UI 반응 속도로 최고의 UX(user experience)를 제공하는 효과 적인 코드 디자인이 필요하다는 소리이다.

ListView를 최적화 시키는 두 가지 방법은 다음과 같다.

[convertView 사용하기]

ListView 의 행(row)가 화면에 그려질 때마다 getView()가 호출된다면, getView()는 LayoutInflater 객체를 생성하고 row의 XML layout을 파싱하고 새로 생성된 View객체에 대입하여 이를 return해야 한다..

이런 반복되는 불필요한 절차를 없애기 위해서는 getView() 메소드의 두 번째 인자, converView를 활용하면 된다.

View getView (int position, View convertView, ViewGroup parent)

두 번째 인자 convertView는 null 또는 바로 전에 생성했던 View 객체이다.

지금 화면에 표현할 ListView의 row의 layout이 바로 전에 표현했던 row와 같은 형태의 layout을 가진다면 view를 새로 생성 할 필요 없이 이를 재활용 할 수 있는 것이다.


접기

ConvertView를 사용한 getView (MyFancyListView03.java)

01package com.holim.test;
02
03import android.app.Activity;
04import android.app.ListActivity;
05import android.content.Context;
06import android.os.Bundle;
07import android.view.LayoutInflater;
08import android.view.View;
09import android.view.ViewGroup;
10import android.widget.ArrayAdapter;
11import android.widget.ImageView;
12import android.widget.ListView;
13import android.widget.TextView;
14
15public class MyFancyListView03 extends ListActivity {
16
17// ListView에 뿌릴 Data
18String[] items={ "Android""iPhone""UI""Java""SDK",
19"Adapter""List""This""is""Fun" };
20TextView tv;
21/** Called when the activity is first created. */
22@Override
23public void onCreate(Bundle savedInstanceState) {
24super.onCreate(savedInstanceState);
25setContentView(R.layout.main);
26
27// 커스텀 ArrayAdapter 선언/초기화.
28MyArrayAdapter aa = new MyArrayAdapter(this);
29
30// 본 Activity의 아답터로 aa 지정.
31setListAdapter(aa);
32
33tv = (TextView)findViewById(R.id.selection);
34}
35
36// ListView의 아이템이 클릭되면 호출되는 callback.
37public void onListItemClick(ListView l, View v, int position, long id) {
38tv.setText(items[position]);
39}
40
41// ArrayAdapter에서 상속받는 커스텀 ArrayAdapter 정의.
42class MyArrayAdapter extends ArrayAdapter<String> {
43// 생성자 내부에서 초기화
44Context context;
45TextView label;
46ImageView icon;
47
48// 생성자
49MyArrayAdapter(Context context) {
50super(context, R.layout.row, items);
51
52// instance 변수(this.context)를 생성자 호출시 전달받은 지역 변수(context)로 초기화.
53this.context = context;
54}
55
56// ListView에서 각 행(row)을 화면에 표시하기 전 호출됨.
57public View getView(int position, View convertView, ViewGroup parent){
58View row = convertView;
59
60if(row == null) {
61// LayoutInflater의 객체 inflater를 현재 context와 연결된 inflater로 초기화.
62LayoutInflater inflater = ((Activity)context).getLayoutInflater();
63
64// inflator객체를 이용하여 \res\laout\row.xml 파싱
65row = (View)inflater.inflate(R.layout.row, null);
66}
67
68// TextView 객체 label을 row 객체 내부에 있는 R.id.label로 초기화
69label = (TextView)row.findViewById(R.id.label);
70
71// label에 텍스트 설정.
72label.setText(items[position]);
73
74// items 배열에서 현제 처리중인 위치의 text 길이가 4 이상이면
75// 아이콘을 'x' 마크로 변경. 4미만일때는 아무 변화없이 xml layout 초기값인 '첵크'마크 사용됨.
76if(items[position].length()>4) {
77
78// 이미지뷰 객체 icon을 row내부에 정의된 R.id.icon으로 초기화.
79icon = (ImageView)row.findViewById(R.id.icon);
80
81// icon 객체가 표현할 리소스를 'x' 마크(R.drawable.delete)로 지정.
82icon.setImageResource(R.drawable.delete);
83
84}
85
86// 커스터마이징 된 View 리턴.
87return row;
88}
89};
90}

접기

위 코드에서 보면 convertView가 null이면 새로운 View를 inflation하고 그렇지 않으면 그전에 inflation된 View를 재활용하여 불필요한 View의 생성과 XML 파싱을 방지한다.

하지만 ListView의 각 row가 각기 다른 View layout (어떤 row는 하나의 TextView로 구성되어 있고 어떤 row는 2개의 TextView로 구성)일 때는 위와 같은 방법을 적용하기 힘들 수도 있다.

물론, 조금의 잔머리 (상황에 따라 2개의 TextView로 구성된 row중 한 TextView에 android:visiability="gone"와 같은 속성을 적용해 감춰 버리면 된다.)를 써서 해결할 수도 있지만 문제의 근원이 완전히 해결된 건 아니다.

해결 방법은 차차 알아 보기로 하자.

[Holder Pattern 사용하기]

getView()에서 또 다른 오버해드는 잦은 findViewById()의 호출이다.

LayoutInflater에 의해 inflation 된 row View내부에서 수정할 위젯의 객체를 뽑아내는 역할을 하는 findViewById()는 수정이 필요한 위젯의 숫자가 많을수록 그 오버로드가 커진다.

Holder Pattern을 사용해 inflation 된 root view에서 한번 뽑아낸 위젯의 인스턴스를 보관(hold) 하고 다음 사용시 이를 재활용한다면 오버로드를 크게 줄일 수 있을 것이다.

Holder Pattern을 구현하기 위해서 다음과 같이 ViewWrapper 클래스를 사용한다.


접기

ViewRapper 클래스를 이용한 Holder Pattern구현 (MyFancyListView04.java)

001package com.holim.test;
002
003import android.app.Activity;
004import android.app.ListActivity;
005import android.content.Context;
006import android.os.Bundle;
007import android.view.LayoutInflater;
008import android.view.View;
009import android.view.ViewGroup;
010import android.widget.ArrayAdapter;
011import android.widget.ImageView;
012import android.widget.ListView;
013import android.widget.TextView;
014
015public class MyFancyListView04 extends ListActivity {
016
017// ListView에 뿌릴 Data
018String[] items={ "Android""iPhone""UI""Java""SDK",
019"Adapter""List""This""is""Fun" };
020TextView tv;
021/** Called when the activity is first created. */
022@Override
023public void onCreate(Bundle savedInstanceState) {
024super.onCreate(savedInstanceState);
025setContentView(R.layout.main);
026
027// 커스텀 ArrayAdapter 선언/초기화.
028MyArrayAdapter aa = new MyArrayAdapter(this);
029
030// 본 Activity의 아답터로 aa 지정.
031setListAdapter(aa);
032
033tv = (TextView)findViewById(R.id.selection);
034}
035
036// ListView의 아이템이 클릭되면 호출되는 callback.
037public void onListItemClick(ListView l, View v, int position, long id) {
038tv.setText(items[position]);
039}
040
041// ArrayAdapter에서 상속받는 커스텀 ArrayAdapter 정의.
042class MyArrayAdapter extends ArrayAdapter<String> {
043Context context;
044ViewWrapper wrapper = null;
045
046// 생성자
047MyArrayAdapter(Context context) {
048super(context, R.layout.row, items);
049
050// instance 변수(this.context)를 생성자 호출시 전달받은 지역 변수(context)로 초기화.
051this.context = context;
052}
053
054// ListView에서 각 행(row)을 화면에 표시하기 전 호출됨.
055public View getView(int position, View convertView, ViewGroup parent){
056View row = convertView;
057
058if(row == null) {
059// LayoutInflater의 객체 inflater를 현재 context와 연결된 inflater로 초기화.
060LayoutInflater inflater = ((Activity)context).getLayoutInflater();
061
062// inflator객체를 이용하여 \res\layout\row.xml 파싱
063row = (View)inflater.inflate(R.layout.row, null);
064wrapper = new ViewWrapper(row);
065row.setTag(wrapper);
066}
067else {
068wrapper = (ViewWrapper)row.getTag();
069}
070
071// TextView 객체 label을 row 객체 내부에 있는 R.id.label로 초기화
072wrapper.getLabel().setText(items[position]);
073
074
075// items 배열에서 현제 처리중인 위치의 text 길이가 4 이상이면
076// 아이콘을 'x' 마크로 변경. 4미만일때는 아무 변화없이 xml layout 초기값인 '첵크'마크 사용됨.
077if(items[position].length()>4) {
078
079// 이미지뷰 객체 icon을 row내부에 정의된 R.id.icon으로 초기화.
080wrapper.getIcon().setImageResource(R.drawable.delete);
081// icon 객체가 표현할 리소스를 'x' 마크(R.drawable.delete)로 지정.
082}
083else {
084wrapper.getIcon().setImageResource(R.drawable.ok);
085}
086
087// 커스터마이징 된 View 리턴.
088return row;
089}
090}
091
092//
093// Holder Pattern을 구현하는 ViewWrapper 클래스
094//
095class ViewWrapper {
096View base;
097ImageView icon = null;
098TextView label = null;
099
100ViewWrapper(View base) {
101this.base = base;
102}
103
104// label 멤버 변수가 null일때만 findViewById를 호출
105// null이 아니면 저장된 instance 리턴 -> Overhaed 줄임
106TextView getLabel() {
107if(label == null) {
108label = (TextView)base.findViewById(R.id.label);
109}
110return label;
111}
112
113// icon 멤버 변수가 null일때만 findViewById를 호출
114// null이 아니면 저장된 instance 리턴 -> Overhaed 줄임
115ImageView getIcon() {
116if(icon == null) {
117icon = (ImageView)base.findViewById(R.id.icon);
118}
119return icon;
120}
121}
122}

접기


위의 소스에서 android.view.View의 메소드인 setTag(), getTag의 활용을 참고하자.

(setTag, getTag 메소드 관련 좋은 포스트가 있어 링크 합니다 - 비즈페이님의 블로그)

wrapper내부의 TextView, ImageView멤버 변수가 null 일 때만 findViewById()를 호출 함으로,

한번 초기화가 이루어 지면 다음부터는 저장된 인스턴스를 사용하게 됨으로 오버헤드를 크게 줄일 수 있다.