들어가기전에,
이전 포스팅에서 DataBinding 클래스가 어떤 원리로 생성되는지 살펴보았습니다. 이전 포스팅을 보지 않으신 분들은 아래 링크를 확인 부탁드립니다.
https://jtm0609.tistory.com/244
[DataBinding] DataBinding에 대한 고찰 (1) - DataBinding은 어떻게 생성되는가?
위 포스팅은 DataBinding에 대한 기본적인 이해가 있는 독자를 염두에 두고 작성되었습니다. DataBinding이란?데이터 바인딩은 선언적 형식으로 레이아웃의 컴포넌트와 앱의 데이터 결합을 지원하는
jtm0609.tistory.com
이 글에서는 XML에 바인딩 된 데이터가 실제 변경되었을 때 내부적으로 어떤 동작이 일어나는지 알아보겠습니다.
Click Events & Data Update
XML에 TextView와 Button이 있다고 가정하자.
나는 Button을 클릭하면 Text가 변경되기를 원한다. 여기서 ViewModel의 역할이 생기는데, 여기서 모든 로직을 작성한다. (MVVM을 사용하는 경우 이미 알고 있을 것이다.)
아래는 작성한 XML이다.
<?xml version="1.0" encoding="utf-8"?>
<layout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools">
<data>
<variable
name="viewmodel"
type="com.example.myapplication.MainViewModel" />
</data>
<androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".MainActivity">
<TextView
android:id="@+id/count_textview"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@{Integer.toString(viewmodel.count)}"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintRight_toRightOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintBottom_toBottomOf="parent"
tools:text="0" />
<Button
android:id="@+id/action_button"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="30dp"
android:onClick="@{() -> viewmodel.onIncrement()}"
android:text="Increment"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintRight_toRightOf="parent"
app:layout_constraintTop_toBottomOf="@id/count_textview" />
</androidx.constraintlayout.widget.ConstraintLayout>
</layout>
그리고 다음은 작성한 ViewModel 클래스이다.
class MainViewModel : ViewModel() {
private val _count = MutableLiveData<Int>()
val count: LiveData<Int> = _count
fun onIncrement() {
_count.value = (_count.value ?: 0) + 1
}
}
필자는 ViewModel을 사용하기 위해 MainActivity를 업데이트 했다.
class MainActivity : AppCompatActivity() {
private lateinit var viewModel: MainViewModel
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
val binding: ActivityMainBinding =
DataBindingUtil.setContentView(this, R.layout.activity_main)
viewModel = ViewModelProvider(this).get(MainViewModel::class.java)
binding.lifecycleOwner = this
binding.viewmodel = viewModel
}
}
Note: 바인딩 클래스에 LiveData를 사용하기 위해서는 LiveData의 Scope를 정의하기 위해 lifecycleOwner를 지정해야한다.
이제 사용자가 버튼을 클릭할때마다 카운트가 증가하고, 이 카운트는 LiveData이고 counte_textView에 의해 XML에 observed 되기 때문에, 항상 최신 데이터를 볼 수 있다.
내부 구조
그렇다면, 이게 정확히 어떻게 일어나는 것일까?
DataBinding 클래스는 이 View가 업데이트 되어야 한다는 것을 어떻게 아는것 일까?
이것에 대한 정답은 AcitivtyMainBinding class에서 찾을 찾을 수 있다.
protected ActivityMainBinding(Object _bindingComponent, View _root, int _localFieldCount,
Button actionButton, TextView countTextview) {
super(_bindingComponent, _root, _localFieldCount);
this.actionButton = actionButton;
this.countTextview = countTextview;
}
위 코드에서 'localFieldCount' 라는 필드를 볼 수 있는데, 이 매개변수는 XML에서 observing하고 있는 View의 개수를 의미한다. (LiveData)
따라서, 위의 예제에서 처럼 하나의 View만 observing하고 있을 경우 'localFieldCount'는 1이 될 것이다.
(이전 포스팅에서는 단순히 variable에 model 값만 등록하고 observing 하고 있는 View가 없었기 때문에 'localFieldCount'는 0이 되겠다.)
LiveData Listener 배열 생성
ActivityMainBinding은 ViewDataBinding을 상속하고 있기 때문에 부모 클래스의 생성자를 호출한다. 이 생성자에서는 WeakListener 라는 배열을 생성하게 되며, 이는 XML에 정의된 LiveData 값의 변화를 감지하는 역할을한다.
/**
* The observed expressions.
*/
private WeakListener[] mLocalFieldObservers;
protected ViewDataBinding(DataBindingComponent bindingComponent, View root, int localFieldCount) {
mBindingComponent = bindingComponent;
mLocalFieldObservers = new WeakListener[localFieldCount];
........
}
위에서 볼 수 있 듯, localFieldCount 값만큼의 WeakListener 객체를 담는다. 즉, 관찰하는 LiveData 값의 개수에 맞게 WeakListener 배열을 할당하고, 각 LiveData 값의 변화에 따라 해당 WeakListener 객체가 이를 감지하는 것이다.
LifeCycleOwner와 DataBinding
액티비티가 lifecycleOwner를 설정할때, Activity Scope를(예제의 경우)가진 ViewDataBinding 클래스는 Listener, 즉 onStartListener를 등록한다.
@OnLifecycleEvent(Lifecycle.Event.ON_START)
public void onStart() {
ViewDataBinding dataBinding = mBinding.get();
if (dataBinding != null) {
dataBinding.executePendingBindings();
}
}
@OnLifecycleEvent(Livecycle.Event.ON_START)는 액티비티의 라이플 사이클 상태가 ON_START에 도달할 때마다 바인딩을 실행하라는 의미이다.
이 때 executePendingBindings()가 호출되어 UI에 데이터가 반영된다.
구체적으로 어떻게 최종적으로 UI가 반영되는지 계속 알아보자
executeBindings()
executeBindings() 메서드는 실제로 데이터 바인딩을 처리하는 메서드로, ActivityMainBindingImpl 클래스에서 구현된다.
@Override
protected void executeBindings() {
......
if (viewmodel != null) {
// read viewmodel.count
viewmodelCount = viewmodel.getCount();
}
updateLiveDataRegistration(0, viewmodelCount);
........
}
ActivityMainBindingImpl에는 ViewModel에서 count 값을 가져와 updateLiveDataRegistration 메서드에 전달하는 작업을 한다.
LiveData Listener 등록
updateLiveDataRegistration 메서드는 우리가 전달한 생성했던 WeakListener에 대해 lifecyleOwner을 등록하고, 해당 LiveData 객체의 변경사항을 수신하도록 요청한다.
protected boolean updateLiveDataRegistration(int localFieldId, LiveData<?> observable) {
mInLiveDataRegisterObserver = true;
try {
return updateRegistration(localFieldId, observable, CREATE_LIVE_DATA_LISTENER);
} finally {
mInLiveDataRegisterObserver = false;
}
}
이제 사용자가 버튼을 클릭하면 LiveData의 Listener의 'onChanged()' 메서드가 호출된다.
@Override
public void onChanged(@Nullable Object o) {
ViewDataBinding binder = mListener.getBinder();
if (binder != null) {
binder.handleFieldChange(mListener.mLocalFieldId, mListener.getTarget(), 0);
}
}
onChanged() 메서드는 LiveData 값이 변경되었을 때 호출되는 메서드로, handleFieldChange()메서드를 호출한다.
그리고 handleFieldChange에서는 onFileldChange()의 구현을 호출한다. 아래와 같이 말이다.
@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
protected void handleFieldChange(int mLocalFieldId, Object object, int fieldId) {
..
boolean result = onFieldChange(mLocalFieldId, object, fieldId);
if (result) {
requestRebind();
}
}
이 함수는 onFieldChange()가 true가 반환하는 경우에만, 즉 데이터가 변경되어 UI에 반영되어야 할 때 예약(Pending)중인 바인딩을 실행하고 View를 업데이트 하는 리바인딩을 요청한다.
onFiledChange()구현부는 아래와 같다.
@Override
protected boolean onFieldChange(int localFieldId, Object object, int fieldId) {
switch (localFieldId) {
case 0 :
return onChangeViewmodelCount((androidx.lifecycle.LiveData<java.lang.Integer>) object, fieldId);
}
return false;
}
'count' 라는 이름의 LiveData가 있으므로 바인딩 클래스는 ViewModel과 변수 이름을 결합하여 유사한 이름, 즉 'viewModelCount'를 생성하고 이와 관련된 모든 메서드도 같은 방식으로 생성된다.
onChangeViewModelCount 메서드 내부에는 업데이트 되는 'mDirtyFlags' (Long Type)가 있는데, true를 반환하고 예약(Pending)중인 바인딩을 실행하고 View를 업데이트하는 리바인딩을 요청한다.
synchronized(this) {
mDirtyFlags |= 0x1L;
}
이 과정은 우리가 버튼을 클릭할 때 마다 반복된다.
바인딩 예약
실제 리바인딩하는 작업은 아래와 같다. 실제 바인딩 작업을 실행해야 할 필요가 있을 때 호출된다.
protected void requestRebind() {
if (mContainingBinding != null) {
mContainingBinding.requestRebind();
} else {
final LifecycleOwner owner = this.mLifecycleOwner;
if (owner != null) {
Lifecycle.State state = owner.getLifecycle().getCurrentState();
if (!state.isAtLeast(Lifecycle.State.STARTED)) {
return; // wait until lifecycle owner is started
}
}
synchronized (this) {
if (mPendingRebind) {
return;
}
mPendingRebind = true;
}
if (USE_CHOREOGRAPHER) {
mChoreographer.postFrameCallback(mFrameCallback);
} else {
mUIThreadHandler.post(mRebindRunnable);
}
}
}
코드를 해석해보면 아래와 같이 동작한다.
1. mContainingBinding !=null 체크
- 만약 현재 객체가 바른 바인딩 객체를 포함하고 있으면, 포함된 바인딩 객체에 requestRebind()를 호출하여 다시 바인딩을 요청한다.
2. LifecycleOwner 상태 확인
- LifecycleOwner 객체가 존재할 경우, 이 객체의 라이프사이클 상태를 확인하여, 바인딩을 재시도하기에 적합한 시점인지 체크한다.
- 그리고 Lifecycle이 STARTED 상태 이상일 때만, 바인딩 재시도를 진행해야 하므로, 이 조건을 만족하지 않으면 바인딩을 지연시킨다.
if (owner != null) {
Lifecycle.State state = owner.getLifecycle().getCurrentState();
if (!state.isAtLeast(Lifecycle.State.STARTED)) {
return; // wait until lifecycle owner is started
}
}
라이플사이클이 STARTED 이상일 때만 바인딩 재시도를 진행하고, 그렇지 않으면 바인딩 재시도 요청을 보류한다. 이는 onStart() 상태 이전에 바인딩을 재시도하는 것을 방지하기 위해서이다.
3. synchronized로 mPendingRebind 체크
- mPendingRebind 플래그를 사용하여 중복된 재바인딩 요청을 방지한다.
- mPendingRebind가 true일 경우 이미 재바인딩 요청이 처리 중이므로, 추가적인 재바인딩 요청을 무시한다.
synchronized (this) {
if (mPendingRebind) {
return;
}
mPendingRebind = true;
}
- mPending가 true이면 이미 재 바인딩이 대기 중이므로, 중복 호출을 막기 위해 return한다. 만약 아직 대기 중인 재바인딩이 없다면, mPendingRebind를 true로 설정하여 재바인딩이 대기중임을 나타낸다.
4. 프레임 콜백 등록
- 바인딩 재시도를 프레임 콜백을 통해 처리하도록 실행한다.
mChoreographer.postFrameCallback(mFrameCallback);
- 화면의 렌더링 주기에 맞춰 mFrageCallback을 호출한다. 프레임 단위 콜백을 실행하므로, 화면 갱신이나 애니메이션 작업에 맞춰 바인딩이 진행될 수 있도록 예약한다.
마무리하며,
DataBinding이 내부적으로 이벤트를 어떻게 처리하고 View에 업데이트를 어떻게 할 수 있는지 알아보았습니다.
가장 좋은 점은 lifecycleOwner를 통해 Scope를 제공함으로써 우리가 LiveData를 사용할때, Lifecycle에 대해 걱정할 필요가 없는 것이 큰 장점인 것 같습니다.
참고자료
https://proandroiddev.com/android-data-binding-under-the-hood-part-2-fdcbb0f54700
Android Data Binding: Under the Hood (Part 2)
Let’s talk about the magic happening behind the dynamic data updates and click actions when we use data binding for our views.
proandroiddev.com
https://velog.io/@wantique/DataBinding
DataBinding
Dive in DataBinding
velog.io
'Android > DataBinding' 카테고리의 다른 글
[Android] DataBinding에 대한 고찰 (1) - DataBinding은 어떻게 생성되는가? (0) | 2025.01.08 |
---|---|
[Android] executePendingBindings() 꼭 써야 할까? (0) | 2025.01.08 |
[Android] 데이터바인딩(DataBinding) 이란? (0) | 2023.09.07 |