Stable Id를 이용한 RecyclerView 성능 향상법



RecyclerView는 ListView를 완전히 대체할 수 있을 만큼 기능과 성능이 크게 향상되었다. ListView에서도 어떻게 하면 끊김 없이 빠른 스크롤을 지원할까라는 고민을 해왔었고, RecyclerView를 사용하다 보면 똑같은 고민을 또 하게 될 것이다.

ViewHolder라는 패턴을 통해 ListView의 성능을 RecyclerView에서 크게 향상할 수 있었다. 재활용하는 뷰들의 클래스를 View 태그 또는 Array에 저장하고 필요할 때 바로 가져와서 사용하는 방법으로 성능을 크게 향상하였다. ListView에서 재활용되는 뷰를 해당 포지션에 맞게 가져오는 곳에서 성능을 향상할 수 있었다면, RecyclerView는 가져온 View에 데이터를 바인드 시 최적화할 수 있는 방법을 제공하고 있다.

HasStableIds사용을 통해 데이터 바인드 시 onBindViewHolder()를 최적화 되게 호출할 수 있다. 아래 2가지 중 하나만이라도 해당한다면 성능을 크게 향상할 수 있다.

  • 똑같은 데이터가 반복적으로 나타는 리스트이다.
  • notifyDataSetChanged를 자주 호출한다.

HasStableIds는 Adapter.setHasStableIds(boolean)을 통해 설정할 수 있으며, 사용하는 경우 어댑터의 getItemId(int)를 반드시 구현해야 작동한다.

getItemId(int)를 통해 해당 아이템은 고정된 상태로 설정된다. 예를 들어 아래와 값을 반환되게 구현했다면 어떤 성능적인 변화가 일어날까?

position
return
0
100
1
200
2
300
3
100
4
400
5
500

onBindViewHolder(view, int)는 포지션이 0, 1, 2, 4, 5 만 호출된다. 3번은 0번째 포지션에서 같은 고정된 ID를 반환했기 때문에 같은 데이터로 인식하여 onBindViewHolder(view, int)가 호출되지 않는다. 같은 데이터임을 알고 데이터 바인드를 할 필요가 없기 때문에 호출되지 않으며 그만큼 성능은 향상된다.

position
return
0
100
1
200
2
600
3
300
4
100
5
400
6
500

포지션 2번에 데이터를 추가하고 notifyDataSetChanged()를 호출하였다. 이때 onBindViewHolder(view, int)는 현재 보이고 있는 포지션이 모두 호출되지만 StableId를 사용하게 된다면 이미 호출된 고정된 ID를 제외한 위치가 호출된다. notifyDataSetChanged()를 하였음에도 변경되는 ID만을 골라 해당 포지션만 onBindViewHolder(view, int)를 호출하게 됨으로 그만큼 성능은 향상된다.




ViewPager의 PagerAdapter POSITION_NONE의 비밀

ViewPager의 PagerAdapter POSITION_NONE의 비밀




요즘 안드로이드 UI는 페이지단위가 대세인듯 합니다. 마켓뿐안 아니라 내놓으라는 인기앱들을 보면 모두 페이징 개념의 UI로 구성되어 있습니다. 안드로이드에서 이런 페이징을 쉽게 구현하기 위해 ViewPager을 Support해주고 있습니다. 



ViewPage는 View또는 Fragment를 페이지 단위로 관리 할수 있는 커스텀 뷰입니다. 

PageAdapter와 ViewPager을 같이 쓰면서 자원낭비에 대한 문제점을 알아보고자 합니다.

PageAdapter의 notifyDataSetChanged는 데이터가 바꼈으니 Notify를 하는 메소드 입니다.

PageAdapter에서 Fragment를 새로 생성하여 View를 만들때 이런 데이터 작업을 하게 됩니다, 그래서 notifyDataSetChanged를 하게 되더라도 Fragment내부의 View들이 새로고침이 되지 않는 문제점이 발생합니다. 강제로 OnCreateView를 태워 View를 그리거나 View를 refresh하도록 구현 해야 합니다.

그래서 대안으로 PageAdapter에서 getItemPosition을 오버라이드 하여 포지션값을 POSITION_NONE으로 주는 방법입니다.

POSITION_NONE으로 인해 ViewPager는 destoryItem()이 호출되어 Fragment가 삭제 된것으로 판단하게 되어 onCreateView가 호출되어 다시 그리는 방식입니다.

        public int getItemPosition(Object object) {
            return POSITION_NONE;
        }

데이터를 바꾸기 위해 notifyDataSetChanged를 호출 하면 Fragment를 새로 생성한다? 

단지 간단한 View일 경우 그렇게 문제 될점은 없을지 모르지만 ListView같은 복잡한 View들로 구성되어 있을때 자원이 낭비되는 문제점을 발생 할 수 있습니다.

따라서 이런 불필요한 낭비를 막기위해서는 instantiateItem에서 View들에 대한 정보를 저장해놓은 다음 데이터업데이트시 View정보를 불러와 refresh하도록 구현하길 권합니다.

SparseArray< View > views = new SparseArray< View >();

@Override
public Object instantiateItem(View container, int position) {
    View root = //refresh할 뷰
    ((ViewPager) container).addView(root);
    views.put(position, root);
    return root;
}

@Override
public void destroyItem(View collection, int position, Object o) {
    View view = (View)o;
    ((ViewPager) collection).removeView(view);
    views.remove(position);
    view = null;
}

@Override
public void notifyDataSetChanged() {
    int key = 0;
    for(int i = 0; i < views.size(); i++) {
       key = views.keyAt(i);
       View view = views.get(key);
       //refresh할 작업들 
    }
    super.notifyDataSetChanged();
}

이 처럼 작업하여 자원의 낭비를 막을 수 있습니다.

POSITION_NONE은 장점도 있다. 예를 들어 가로세로 전환시 ViewPager의 View들이 새로운레이아웃을불러 들이는 경우에는 이 방법을 써야 합니다.

어떻게 하라고 권고 할 말한 사항은 아닌만큼 앱의 특성에 맞게 사용하시길 바랍니다.