리스트뷰 퍼포먼스 팁



안드로이드의 ListView는 확장성및 성능을 위해 설계된 뷰이다. 일반 View를 기반으로 스크롤처리를 하면 뷰를 inflate할때 상당한 시간과 메모리부족등의 문제가 발생될 것이다. 이러한 문제점을 해결하기위해 ListView를 제공하고 있다.

요즘도 흔하지만 안드로이드 초기 시절 ListView를 스크롤 할때 상당히 끊김이 심한 경우를 볼 수 있었다. (국내 포털 N사앱에서 스크롤시 ANR발생되는 등) 이는 ListView의 구조를 정확하게 이해하지 않고 개발을 했기 때문이다. 

그래서 ListView가 어떤 원리로 작동되는지와 어떻게 하면 성능을 최적화해서 좀 더 자연스러운 스크롤을 할 수 있는지에대해 써볼까 한다.

ListView의 작동 원리

ListView는 View를 재활용한다. 화면에 보여질 뷰를 inflate 한다음 스크롤시 재활용 하는 방식이다.  View inflate는 많은 비용이 드는 작업이며, 화면에 보이지도 않은 View를 미리그려 메모리의 문제가 생길 수 있기 때문에 한번 생성된 뷰를 재활용해서 데이터만 바꾸는 구조이다.

ListView내부 코드를 보면 ScrapView라는 화면에 보여질 View 배열이 존재 한다. ListView의 포지션에 따라 이 ScrapView의 위치가 바뀌게되는 구조로 재활용 하게 된다.

  

ConvertView 재활용 

ListView의 Adapter에는 getView()라는 메소드가존재 한다. 해당 포지션이 보이면 이 getView()가 호출된다. 다들 아는것 처럼 인수로 View convertView, int position, ViewGroup parent 가 들어 온다.

convertView는 위에서 언급한 재활용 되는 ScrapView[]이다.재활용 하기때문에 최초 한번만 convertView는 NULL값이 들어 오며 그후에 호출 되면 이미 생성된 View가 들어 오게 된다.

그렇기 때문에 아래 처럼 convertView가  NULL일 때만 inflate해주면 된다.

public View getView(int position, View convertView, ViewGroup parent) {
    if (convertView == null) {
        convertView = mInflater.inflate(R.layout.your_layout, null);
    }

    TextView text = (TextView) convertView.findViewById(R.id.text);
    text.setText("Position " + position);

    return convertView;
}

ViewHolder Pattern

inflate된 View를 findViewById()메소드를 통해 찾는 것이 일반적이다. 레이아웃 구조가 복잡하고, 많은 데이터를 View에 설정 할때 이또한 많은 비용이 발생하게 된다. 이렇게 많은 비용이 발생하게 된다면 스크롤시 자연스러움을 보장 할 수 없게된다.

이미 찾은 View를 ViewHolder Pattern을이용해서 convertView의 Tag값이 저장해두는 방식을 쓰면 매번 findViewById()할 필요가 없다. convertView가 최초로 호출 될때 findViewById()로 찾은 View를 Tag에 저장하며, 그 이후로 호출되면 Tag에 저장된 View를 불러오는 방식이다.

public View getView(int position, View convertView, ViewGroup parent) {
    ViewHolder holder;

    if (convertView == null) {
        convertView = mInflater.inflate(R.layout.your_layout, null);

        holder = new ViewHolder();
        holder.text = (TextView) convertView.findViewById(R.id.text);

        convertView.setTag(holder);
    } else {
        holder = convertView.getTag();
    }

    holder.text.setText("Position " + position);

    return convertView;
}

private static class ViewHolder {
    public TextView text;
}

비동기 처리

getView()는 UI Thread에서 작동 되기때문에 인터넷이나 로컬에서 사진을 불러오는등 동적인 작업시 전혀다른 Thread를 통해서 비동기 처리해야 된다. 그렇지 않을경우 스크롤시 동기화처리되어 아주 많이 끊기게 된다. 안드로이드에서 비동기 처리방법으로 AsyncTask를 사용하면된다.

public View getView(int position, View convertView,
        ViewGroup parent) {
    ViewHolder holder;

    ...

    holder.position = position;

    new ThumbnailTask(position, holder)
            .executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR, null);

    return convertView;
}

private static class ThumbnailTask extends AsyncTask {
    private int mPosition;
    private ViewHolder mHolder;

    public ThumbnailTask(int position, ViewHolder holder) {
        mPosition = position;
        mHolder = holder;
    }

    @Override
    protected Cursor doInBackground(Void... arg0) {
        // Download bitmap here
    }

    @Override
    protected void onPostExecute(Bitmap bitmap) {
        if (mHolder.position == mPosition) {
            mHolder.thumbnail.setImageBitmap(bitmap);
        }
    }
}

private static class ViewHolder {
    public ImageView thumbnail;
    public int position;
}

그외 사항

스크롤시 빠르게 지나가는 경우 삭제될 데이터들에 대한 자원을 낭비 하게 된다. 이런 경우를 대비해서 스크롤 중일때는 기본데이터만 가져오고 스크롤이 멈추면 모든데이터를 가져오는 방식도 추천할만하다. ListView의 onScrollChangeListener을 통해 스롤되는중이거나 스크롤이 끝났음을 Callback 받을수 있으며 Adapter에서 flag를 만들어 getView에서 모든데이터를 가져올때와 기본데이터만 보여줄것을 구현 하면된다.

이처럼 위의 사항을 잘 지킨다면 아무리 많고 복잡한 데이터일지라도 끊김 없는 아주 자연스러운  ListView를 구현 할 수 있을 것이다.