Android findViewById에 대해서(성능 및 자세한 고찰)

글에 앞서 소개

안녕하세요? 룩핀에서 안드로이드 개발을 하고 있는 오경식이라고 합니다.
혹시 잘못 된 내용이나 궁금한 점이 있으실 경우에는 언제든지 메일이나 메시지 부탁드리겠습니다.

이글을 쓰게된 계기

안드로이드 개발을 하면서 List관련 화면에서 View를 재사용하지 않고 구현하는 경우에 findViewById를 계속 호출 하게 되어 성능저하가 일어날 수 있다는 것으로 많이들 알고 있으실 겁니다.
예전 kmshack님의 ListView 성능 개선 글을 읽어보시면 글 중 ListView에 findViewById의 값 비싼 호출을 반복하는 문제를 해결하기 위한 ViewHolder Pattern 및 그 외 문제점에 대한 글을 쓰셨습니다.
지금 현재 많이들 쓰고 계신 RecyclerView에서는 이 문제를 해결하기위해 ViewHolder Pattern을 꼭 사용하도록 하였습니다만

최근 kunny님이 쓰신 kotlin Android Extensions - 리사이클러뷰의 뷰홀더에서 올바르게 사용하는 방법을 보시면 뷰홀더를 Kotlin Android Extensions를 이용해 저 처럼 잘못 작성하시는 경우에는 kotlin 코드의 바이트코드를 자바코드로 변환하시면 kunny님 글에 있듯이 호출 비용이 큰 findViewById를 매번 호출하고 있었습니다.

해당 글을 읽고 기존 코드를 수정하면서 findViewById가 왜 그런 호출 비용이 커서 성능 관련 이슈를 발생시키는지에 대한 궁금증이 생겨 그 범인에 대해서 알아보고 글을 쓰게 되었습니다.

범인을 찾아보자

findViewById이 성능상의 문제가 될 수 있는 이유를 찾아서 설명하기 위해서 안드로이드 소스코드를 찾아 보게 되었습니다.

View

1
2
3
4
5
6
public final View findViewById(int id) {
if (id < 0) {
return null;
}
return findViewTraversal(id);
}

을 보시면 id가 0 이상인 경우 즉 유효한 경우에 findViewTraversal 을 호출 합니다.

1
2
3
4
5
6
protected View findViewTraversal(int id) {
if (id == mID) {
return this;
}
return null;
}

findViewTraversal을 보시면 단지 전달 된 id가 View의 id와 같은지 검사하고, 그렇지 않으면 null을 반환합니다.
딱히 성능에 영향을 끼칠만한 요소는 보이지 않습니다.
그렇다면 ViewGroup을 보시겠습니다.

ViewGroup

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
@Override
protected View findViewTraversal(int id) {
if (id == mID) {
return this;
}
final View[] where = mChildren;
final int len = mChildrenCount;
for (int i = 0; i < len; i++) {
View v = where[i];
if ((v.mPrivateFlags & PFLAG_IS_ROOT_NAMESPACE) == 0) {
v = v.findViewById(id);
if (v != null) {
return v;
}
}
}
return null;
}

전달 된 id가 ViewGroup의 ID와 같은지 여부를 확인하고 같은경우에 자기자신을 반환하고 아닌 경우 자기 자신이 가진 Child View에 대해 루프를 돌려 각 Child View에 findViewById를 호출하고 있습니다.
그렇다면 만약 ViewGroup안에 Child View가 10개가 있고 그 해당 뷰들을 찾아 넘길려면 최소 10번의 loop를 돌게됩니다.

리스트처럼 뷰가 반복적으로 사용되는 곳에서 위 메서드가 호출되는 상황이 반복된다면 꽤나 큰 리스크라고 생각이 됩니다.

결론

코드를 잘 짜도록 하자.