본문 바로가기

Android/Tech

LayoutInflater 가 xml 을 객체화 하는 과정

Unsplash, Will O.

동기

네이버 부스트캠프 멤버십 과정에 참여 중인데, xml 기반으로 UI 를 작성하라는 요구 사항이 있어 오래간만에 xml 관련 코드를 잔뜩 작성하고 있습니다. 다만, 최근에 Jetpack Compose 에서의 렌더링에 대해 공부하고 있다 보니, xml 은 어떻게 UI 가 될까? 라는 궁금증이 생겼고, 이를 해결하기 위해 LayoutInflater 를 찾아가보기로 했습니다.

 


LayoutInflater

 

Layout 은 알겠는데, Inflate 라는 단어는 다소 생소해서 찾아봤는데, Inflate 는 '부풀리다'를 의미합니다. xml 에 작성된 코드를 바탕으로 UI 를 그려내니, 부풀리다는 의미가 썩 어울리는 것 같습니다.

 

LayoutInflater 에 작성된 주석에는 해당 클래스의 역할과 생성 등에 관해 알 수 있습니다.

 

Instantiates a layout XML file into its corresponding View objects. 
It is never used directly. Instead, use android.app.Activity.getLayoutInflater() or 
Context.getSystemService to retrieve a standard LayoutInflater instance that is already
hooked up to the current context and correctly configured for the device you are running on.

 

그렇다면, 어떠한 과정을 통해 객체화할 수 있는 걸까요?

객체화 과정

override fun onCreate(savedInstanceState: Bundle?) {
    super.onCreate(savedInstanceState)
    setContentView(R.layout.activity_main)
}

 

처음 Activity 를 생성하면, 위와 같은 코드가 자동으로 생성됩니다. Activity 가 생성되고 난 뒤 레이아웃의 리소스 ID 를 가져와 파라미터로 넘겨주고 있으니, setContentView() 메서드를 살펴보면 되겠습니다.

setContentView() 메서드가 아니더라도 다양한 방식으로 레이아웃을 객체화할 수 있습니다만, 사실 LayoutInflater 클래스의 inlfate() 메서드를 호출하는 것에는 차이가 없습니다.

 

@Override
public void setContentView(@LayoutRes int layoutResID) {
    initViewTreeOwners();
    getDelegate().setContentView(layoutResID);
}

 

해당 메서드 내에서는 두 줄의 코드를 실행하고 있습니다. initViewTreeOwners() 메서드의 경우, ViewTree 를 구성(및 재구성)을 위한 객체들을 설정해 주는 작업을 실행하는 메서드입니다. 이 때, 내부에서는 ViewTreeLifecycleOwner.set() 메서드가 실행되는데, 파라미터로 넘겨주는 DecorView 가 전체 레이아웃을 의미합니다.

 

 

디버그 툴을 이용하여 확인한 결과, mDecorCatpionView 가 null 이므로 화면 전체에 해당하는 새로운 ViewGroup 이 DecorView 에 추가됩니다. 즉, xml 에 선언한 루트가 객체화되어 배치될 수 있도록 최상위 ViewGroup 을 생성해주는 부분입니다.

getDelegate().setContentView()

initViewTreeOwners() 메서드를 통해 ViewTree 를 구성(또는 재구성)하기 위해 필요한 객체들을 획득하여 할당해 주는 작업을 수행하였으니, 이들이 어떻게 사용되는지 확인할 수 있겠습니다.

 

getDelegate().setContentView() 는 AppCompatDelegate 의 구현체를 얻어 와 setContentView() 를 실행합니다. Compat Prefix 는 Compatibility 를 의미하므로, 여러 기기 버전에 대응하기 위한 클래스임을 네이밍으로 표시하고 있는 것이기에, 굳이 상위 클래스의 setContentView() 를 찾아 볼 필요는 없습니다. 즉, 다음 코드는AppCompatDelegate 의 setContentView() 메서드입니다.

 

@Override
public void setContentView(int resId) {
    ensureSubDecor();
    ViewGroup contentParent = mSubDecor.findViewById(android.R.id.content);
    contentParent.removeAllViews();
    LayoutInflater.from(mContext).inflate(resId, contentParent);
    mAppCompatWindowCallback.bypassOnContentChanged(mWindow.getCallback());
}

 

여기서도 집중해야 할 것은 LayoutInflater.from(mContext).inflate(resId, contentParent); 라인입니다.

mContext 는 Activity 이며, Activity 에 할당된 LayoutInflater 를 획득하고 이를 활용하여 레이아웃을 객체화합니다. 

inflate()

public View inflate(@LayoutRes int resource, @Nullable ViewGroup root, boolean attachToRoot) {
    final Resources res = getContext().getResources();
    if (DEBUG) {
        Log.d(TAG, "INFLATING from resource: \"" + res.getResourceName(resource) + "\" ("
              + Integer.toHexString(resource) + ")");
    }

    View view = tryInflatePrecompiled(resource, res, root, attachToRoot);
    if (view != null) {
        return view;
    }
    XmlResourceParser parser = res.getLayout(resource);
    try {
        return inflate(parser, root, attachToRoot);
    } finally {
        parser.close();
    }
}

 

실질적으로 xml 을 파싱하여 View 형태로 반환하는 메서드이고, 이 중 눈 여겨 볼만한 부분은 inflate() 메서드 콜 사이트입니다.

 

public View inflate(XmlPullParser parser, @Nullable ViewGroup root, boolean attachToRoot) {
	...
}

 

XmlPullParser 를 이용하는데, 흔히 외부 통신을 통해서 얻어 온 xml 문서를 파싱하는 데에도 사용됩니다. 안드로이드에서도 xml 형식을 이용하고 있기 때문에 당연히 필요한 요소입니다.

 

final Context inflaterContext = mContext;
final AttributeSet attrs = Xml.asAttributeSet(parser);

 

xml 에 정의된 View 를 파싱하여 AttributeSet 을 얻어 와 attrs 라는 변수에 할당하고, 이를 View 의 서브 타입인, 실제 사용될 루트 ViewGroup 객체에 사용합니다. 

 

// Inflate all children under temp against its context.
rInflateChildren(parser, temp, attrs, true);

 

이후 호출되는 것은 rInflateChildren() 메서드이며, 접두사 r- 은 recursive 를 의미합니다. rInflateChildren() 메서드 내에서는 rinflate() 메서드를 호출하고, rinflate() 메서드 내에서 또 다시 rInflateChildren() 메서드를 호출하는 형태의 재귀를 통해 내부에 선언한 컴포넌트들도 View 로 변환합니다.

 

void rInflate(XmlPullParser parser, View parent, Context context,
        AttributeSet attrs, boolean finishInflate) throws XmlPullParserException, IOException {
    	...
    	rInflateChildren(parser, view, attrs, true);
	viewGroup.addView(view, params);
	}
}

 

이 재귀 호출이 모두 완료되면 addView() 메서드를 호출하는데, 해당 메서드 내에서 최종적으로 requestLayout() 및 invalidate() 를 호출합니다.

 

다시 돌아와, 재귀 호출 내에서 View 생성을 위해 호출되는 것은 tryCreateView() 메서드입니다.

 

 

FrameLayout 내부에 TextView 를 선언한 xml 을 사용하였기 때문에, parent 는 FrameLayout 이며 nameTextView 로 확인됩니다. 여기서 name 은 Tag 라는 이름으로 계속 사용되는데, xml 에 선언된 '<TextView></TextView> 와 같은 태그를 의미합니다.

 

tryCreateView() 메서드에서는 Factory 인터페이스 구현체를 통해 createView() 를 호출하고 있으며, 이 때, name(Tag) 를 패싱하여 해당 name 에 따라 적절한 View 의 서브 타입 객체를 반환합니다.

 


 

즉, 전체 과정을 도식화한다면 위와 같을 것입니다. 각 순서에 대해 간략히 작성합니다.

 

  1. Activity 에서 setContentView() 메서드 실행
  2. initViewTreeOwners() 메서드 실행
  3. xml 에 선언한 View 컴포넌트(루트 포함)가 배치될 수 있도록 최상위 ViewGroup 생성 및 배치
  4. 내부에서 LayoutInflater 의 inflate() 메서드 실행
  5. inflate() 메서드 내에서는 XmlPullParser 를 직접 생성하여 주어진 xml 을 inflate()
  6. infltae() 메서드에서는 rInflateChildren(), rInflate() 메서드의 재귀 호출을 통해 xml 을 파싱
  7. 각 태그와 attrs 를 View 생성의 역할을 위임받은 객체(LayoutInflater.Factory 인터페이스 구현체)에 전달
  8. Factory 인터페이스 구현체는 전달받은 태그와 attrs 에 따라 적절한 View 서브 타입 객체 생성
  9. 생성된 View 는 addView() 메서드를 통해 ViewGroup(루트) 에 추가

생각보다 훨씬 복잡한 구조로 구현되어 있고, View 나 ViewGroup 등과 관련된 코드는 외부로 쉽게 노출되는 코드도 아니라서 확인하고 학습하는데 큰 어려움을 겪었습니다. 게다가, 버전에 따라 대응이 다르다보니 브레이크 포인트도 제대로 설정이 안 되어서 제대로 찾아가기도 힘들더군요.

 

JetpackCompose 를 자주 사용하다보니, 기존의 UI 체계를 다시금 이해하고 구현을 파악하는 데에도 쉽지 않았던 것 같습니다.

 

그래도, 내부적으로 어떠한 과정을 거쳐 UI 가 구현되는지에 대해 파악하고 나니, 궁금했던 것들이 꽤 해소되는 느낌입니다. 내부적으로 수많은 함수를 호출하고, ViewGroup 이 있는 경우에는 또 해당 ViewGroup 내부에서 requestLayout, invalidate() 등이 연속적으로 호출되기 때문에... ConstraintLayout 의 사용이 권고될만 하다 싶었습니다.