들어가기 전
이번 포스팅에서는 Android 앱에서 가장 많이 사용한다고 볼 수 있는 RecyclerView의 성능 개선에 대해서 알아보자.
하지만 먼저 알아야 할 도구가 있다. 바로 GPU 렌더링 막대기
개발자 옵션 -> 프로필 GPU 렌더링에서 킬 수 있는 이 기능은 기기의 렌더링 상황을 시각적으로 확인하여 어느 부분에서 지연이 되고 있는지 알 수 있다. 아래는 예시 사진으로 왼쪽은 갤럭시 설정 앱, 오른쪽은 필자가 만든 WASK 앱이다.
- 가로 막대기
- 안드로이드는 초당 60fps를 가져야 사용자는 부드럽다고 느낀다. 프레임 기준으로 계산해보면 1000ms / 60 fps = 16.66...로 프레임 하나당 16ms내에 생성돼야 한다. 가로 막대기는 16ms를 뜻한다.
- 세로 막대기
- 세로 막대기는 한 프레임이 그려지는 데 걸리는 시간을 의미하여 높을 수록 안 좋은 의미다. 막대기에는 다양한 컬러로 이루어져 있으며 컬러는 각기 의미를 가지며 참고 링크에서 확인할 수 있다.
아무리 앱을 최적화해도 이미지 로딩, 복잡한 기능을 처리하다보면 몇 프레임 정도는 16ms를 넘을 수 있다. 하지만 오른쪽의 필자가 만든 WASK 앱은 RecyclerView의 스크롤을 빠르게 할 당시 사진으로 16ms 기준치를 넘는 프레임이 많아 사용자가 버벅거림을 느낄 수 있어 개선이 필요하다.
RecyclerView - 부분 갱신
RecyclerView의 불필요한 다시 그리기는 성능을 안좋게 한다. 우리는 RecyclerView의 리스트 목록 중 일부분만 변경이 발생했을 때, Adapter.notifyDataSetChanged()
를 무심코 호출하는 경우가 간혹 있다. 해당 함수는 RecyclerView의 모든 리스트를 초기화하는 것으로 변경되지 않는 뷰도 비효율적으로 다시 그린다.
- 변경된 위치를 알고 있는 경우
- notifyItemChanged(int position)
- position 위치만 다시 그린다.
- notifyItemRangeChanged(int positionStart, int itemCount)
- position 위치부터 itemCount만큼 다시 그린다.
- notifyItemRangeInserted, notifyItemRangeRemoved 등등..
- 변경된 위치를 모를 경우
DiffUtil
을 사용하면 리스트의 변경된 부분만 갱신할 수 있게 도와준다.
RecyclerView - setHasFixedSize
recyclerview.setHasFixedSize(true)
위 코드는 Adapter Item View의 내용이 변경되어도 RecyclerView의 크기는 고정하겠다는 의미이다.
RecyclerView와 같은 View도 화면에 표시되기 까지 생명 주기를 가진다. 초기 생성될 때 onMeasure, onDrow와 같은 생명 주기들이 거쳐가며 화면에 표시되고, requestLayout() 또는 invalidate() 호출로 특정 생명 주기로 돌아가 뷰를 다시 그리는 작업을 한다.
public class RecyclerView extends ViewGroup implements ScrollingView,
NestedScrollingChild2, NestedScrollingChild3 {
//생략..
protected void onMeasure(int widthSpec, int heightSpec) {
//생략...
if (mHasFixedSize) {
mLayout.onMeasure(mRecycler, mState, widthSpec, heightSpec);
return;
}
//mHasFixedSize true로 설정하지 않아서 RecyclerView 크기가 변경되었는지 확인하는 코드들이 아래로 쭉...
if (mAdapterUpdateDuringMeasure) {
startInterceptRequestLayout();
onEnterLayoutOrScroll();
processAdapterUpdatesAndSetAnimationFlags();
onExitLayoutOrScroll();
if (mState.mRunPredictiveAnimations) {
mState.mInPreLayout = true;
} else {
...생략
해당 함수를 true로 설정해주지 않으면, Adapter 내용이 변경될 때 뷰를 다시 그리기 위해 호출되는 RecyclerView.onMeasure에서 변경된 크기 값을 알아내려는 별도의 로직 실행 없이 종료할 수 있다.
하지만 Adapter의 내용에 따라 크기가 변경된다면 어쩔수 없이 false로 설정하자.
RecyclerView - setItemViewCacheSize
recyclerview.setItemViewCacheSize(int size)
RecyclerView는 이름 뜻에도 알 수 있듯이, 기기의 성능이 안 좋아 findViewById() 함수로 View를 가져오는 것은 많은 리소스를 요구했기 때문에 이를 방지하고자 ViewHolder를 재사용한다. 하지만 뷰 객체만 재사용하는 거기 때문에 onBindViewHolder를 통해 다시 그려줘야 했다.
setItemViewCacheSize
는 스크롤 되어 화면에 사라지는 뷰에 대해서 재사용되는 recycled view pool에 들어가지 않고, Cache에 저장되어 다시 화면에 나왔을 때 onBindViewHolder 호출 없이 그대로 보인다. 따라서 동일한 뷰를 다시 그리지 않기 때문에 성능 개선이 된다.
RecyclerView - sethasstableids
recyclerview sethasstableids(true)
같은 화면에서 이미지, 글 같은 내용을 의미하는 리스트가 여럿 존재할 수 있다. 같은 리스트를 여러 번 그리는 것보다 위 코드를 사용하면 같은 Id를 가진 Item은 onBindVIewHolder 재호출 없이 이전 View를 보여준다.
단, adapter에서 Item의 id를 지정해주는 함수를 오버라이드 해야한다. id는 DB의 기본키 등등 별도로 지정해주면 된다.
@Override
public long getItemId(int position) {
return item[position].id;
}
RecyclerView - 중첩일 때 pool 공유
class OuterAdapter extends RecyclerView.Adapter<OuterRecyclerAdapter.ViewHolder> {
...
@NonNull
@Override
public WheelViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {
View view = LayoutInflater.from(parent.getContext()).inflate(R.layout.date_picker_item, parent, false);
RecyclerView innerRecyclerView = view.findViewById(R.id.recycler);
//Pool 공유
innerRecyclerView.setRecycledViewPool(outerRecyclerView.getRecycledViewPool());
return new WheelViewHolder(view);
}
구글 플레이 스토어 처럼 동일한 뷰에 대해 중첩으로 RecyclerView가 구성돼있을 때, 바깥쪽 RecyclerView의 Pool을 안 쪽 RecyclerView Pool에 공유해주므로 성능을 높일 수 있다.
RecyclerView - 초기화는 ViewHolder에서
getAdapterPosition()
무심코 리스트의 ClickListener를 onBindViewHolder에서 구현한 적이 있었다. 필자도 viewholder 내부에서 position을 얻지 못하는 구나라고 착각했었다. ViewHolder가 재사용되어도 onBindViewHolder는 계속 호출되기 때문에 불필요한 리스너 설정이 계속 이뤄진다는 소리이다. getAdapterPosition()
함수로 호출했을 때, ViewHolder의 위치를 반환해줘서 ClickListener를 ViewHolder에서 무리없이 구현할 수 있다. 단 해당 position이 올바른 위치인지 확인하고 사용하자.
class ViewHolder extends RecyclerView.ViewHolder {
...
public WheelViewHolder(@NonNull View itemView, OnClickListener listener) {
super(itemView);
itemView.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View view) {
if (getAdapterPosition() != RecyclerView.NO_POSITION) {
listener.onClick(view, getAdapterPosition());
}
}
});
}
}
개선 확인
RecyclerView의 성능 개선한 이후 WASK GPU 막대 그래프가 현저히 낮아진 것을 확인할 수 있다.
참고
'Android > Common' 카테고리의 다른 글
[Android] Testable App - JUnit Unit Test (0) | 2020.12.15 |
---|---|
[Android] MVP 적용해보기 - Model 말고 Repository (4) | 2020.11.23 |
[Android] 뷰의 성능 개선 - 오버드로 줄이기 (3) | 2020.10.08 |
[Android] Drawable color 속성을 코드로 변경하기 (0) | 2020.08.24 |
[Android] MVP 적용해보기 - View와 Presenter (0) | 2020.08.11 |