만족

[Android] ListView 성능 향상 본문

[Android] ListView 성능 향상

FrontEnd/Android Satisfaction 2020. 5. 6. 19:10

ListView가 낮은 성능을 보이는 데는 나의 경험에 기반한 몇 가지 이유가 있다.

 

1. getView 구현부에서 항상 view inflatation을 하는 경우

2. getView 구현부에서 ViewHolder패턴을 사용하지 않아 항상 업데이트하는 경우

3. ListView 높이 재설정 시 아이템 갯수만큼 measure하는 경우

4. 필요하지 않은 데이터까지 한번에 렌더링하는 경우

 

ListView 작업은 UIThread에서 이루어지기 때문에, 여기에서 많은 시간을 사용한다면 

로그캣에 Layout Skipping메시지가 뜨면서 일정 시간동안 화면이 멈추게 된다.

 

위의 원인만 제거해주더라도 체감할 수 있을 정도의 성능 향상이 있을 것이다.

 

1. getView 구현부에서 항상 view inflatation을 하는 경우

 

@Override
    public View getView(int position, View convertView, ViewGroup parent) {
   	convertView= inflater.inflate(layout, parent, false);
            
        TextView title= convertView.findViewById(R.id.text);
        title.setText("["+data.get(position).type+"] "+data.get(position).title);
        title.setOnClickListener((View v)->{
            Common.openInternet(data.get(position).link, context);
        });
        return convertView;
    }

getView의 두 번쨰 파라미터인 convertView는 항상 Null이 아니다.

첫 번째 호출 후 부터는 convertView는 인플레이트된 뷰의 인스턴스를 가지고 있다.

 

인플레이션은 매우 값비싼 작업이므로, convertView가 null일 때만 인플레이트하게 변경한다.

    @Override
    public View getView(int position, View convertView, ViewGroup parent) {
        if(convertView== null){
            convertView= inflater.inflate(layout, parent, false);
        }

        TextView title= convertView.findViewById(R.id.text);
        title.setText("["+data.get(position).type+"] "+data.get(position).title);
        title.setOnClickListener((View v)->{
            Common.openInternet(data.get(position).link, context);
        });
        return convertView;
    }

2. getView 구현부에서 ViewHolder패턴을 사용하지 않아 항상 업데이트하는 경우

 

뷰 홀더 패턴이란, 위의 코드블럭에서 

TextView title= convertView.findViewById(R.id.text);
title.setText("["+data.get(position).type+"] "+data.get(position).title);
title.setOnClickListener((View v)->{
	Common.openInternet(data.get(position).link, context);
});

이 부분을 홀더에 넣어두고(별도의 공간에 해당 코드가 적용된 상태를 저장), 다음 번 부터는 저장된 상태를 가지고 동작하게끔 만드는 패턴이다.

 

해당 패턴을 강제하는 컴포넌트가 있는데, 그것이 RecyclerView이다.

 

직접 적용하는 것 보다, 요구되는 인터페이스가 모두 세팅되어 있는 RecyclerView를 사용하는 것이 더 좋다.

 

해당 적용사항은 나머지 원인을 모두 수정한 다음에도 성능이 나아지지 않을 경우 설정하도록 한다.

 

3. ListView 높이 재설정 시 아이템 갯수만큼 measure하는 경우

 

ScrollView안쪽에 ListView를 사용하면, ListView의 높이값이 아이템 하나의 높이값으로 축소되는 문제가 발생한다.

 

해당 문제를 구글에 검색하면, 대부분 아래와 같은 솔루션을 얻을 수 있다.

public static void setListViewHeightBasedOnChildren(@NonNull ListView listView) {
        ListAdapter listAdapter = listView.getAdapter();

        int totalHeight = 0;
        for(int i=0; i< listAdapter.getCount(); i++){
            View listItem = listAdapter.getView(i, null, listView);
            listItem.measure(View.MeasureSpec.makeMeasureSpec(0, View.MeasureSpec.UNSPECIFIED),
                    View.MeasureSpec.makeMeasureSpec(0, View.MeasureSpec.UNSPECIFIED));
            totalHeight += listItem.getMeasuredHeight();
        }

        ViewGroup.LayoutParams params = listView.getLayoutParams();
        if(totalHeight> 0){
            params.height = totalHeight + (listView.getDividerHeight() * (listAdapter.getCount() - 1));
        }else{
            params.height = 0;
        }
        listView.setLayoutParams(params);
        listView.requestLayout();
    }

그런데, 이 메서드의 listItem.measure부분은 시간이 오래 걸리는 작업이다.

 

이 메서드를 아이템의 갯수만큼 반복하므로, 갯수가 많아질수록 퍼포먼스는 급격하게 떨어진다.

 

만약 ListView의 아이템의 높이가 모두 동일하다면,

아이템의 갯수만큼 measure하지 말고, 한번 measure한 다음 아이템의 갯수만큼 곱하는 것이 더 효율적이다.

public static void recalculateListViewHeight(@NonNull ListView listView){
        ListAdapter listAdapter = listView.getAdapter();
    
        int totalHeight = 0;
        if(listAdapter.getCount()> 0){
            View listItem = listAdapter.getView(0, null, listView);
            listItem.measure(View.MeasureSpec.makeMeasureSpec(0, View.MeasureSpec.UNSPECIFIED),
                    View.MeasureSpec.makeMeasureSpec(0, View.MeasureSpec.UNSPECIFIED));
            totalHeight = listItem.getMeasuredHeight()* listAdapter.getCount();
        }
    
        ViewGroup.LayoutParams params = listView.getLayoutParams();
        if(totalHeight> 0){
            params.height = totalHeight + (listView.getDividerHeight() * (listAdapter.getCount() - 1));
        }else{
            params.height = 0;
        }
        listView.setLayoutParams(params);
        listView.requestLayout();
    }

4. 필요하지 않은 데이터까지 한번에 렌더링하는 경우

 

사실은 모든 유저가 리스트뷰의 리스트 끝까지 내려서 확인하지는 않는다. 

 

따라서 모든 아이템을 초기에 전부 렌더링할 필요는 없다.

 

예를 들어 아이템이 총 100개 있다면,

초기에는 20개를 렌더링하고 해당 리스트의 마지막까지 스크롤 했을 때 추가로 20개를 렌더링해주는 방식이다.

 

private fun initLogListView(requireChangeAdapter: Boolean): Long {
        val timer = System.currentTimeMillis()
        if (logList.adapter == null || requireChangeAdapter) {
            rowAdapter = PlayerLogAdapter(rows.subList(0, renderCount), activity)
            logList.adapter = rowAdapter
        } else {
            rowAdapter.updateData(rows.subList(0, renderCount))
            rowAdapter.notifyDataSetChanged()
        }
        Common.recalculateListViewHeight(logList)
        return System.currentTimeMillis() - timer
    }

(코틀린 코드)

 

Adapter를 생성할 때, List전달부분에 전부 다 전달하지 않고, 렌더링할 아이템의 갯수만큼 subList메소드를 이용해 갯수를 줄인 후 렌더링한다.

 

만약 사용자가 현재 리스트뷰의 마지막 아이템까지 스크롤링 했음이 감지되면

adapter의 List를 갱신해주고, notifyDataSetChanged()를 이용해 리스트 데이터가 변경되었음을 어댑터에 알린다.

 

물론 아이템 갯수가 변경되었으니, ListView의 Height도 다시 계산하게 한다.

 



Comments