Foundation/Android

[Android] LiveData setValue(), postValue() deep dive

개발왕 금골드 2024. 2. 11. 18:26
반응형

개요

LiveData는 수명 주기를 인식하는 Observable data holder class 이다. LiveData는 몇 가지 특성을 갖고 있다.

  • 데이터를 보유할 수 있다. 또한, 모든 유형의 데이터에 사용할 수 있는 Wrapper class이다.
  • Observable 하다. LiveData 객체에서 보유한 데이터가 변경되면 관찰자에게 알림이 제공된다.
  • 수명 주기를 인식한다. LiveData에 관찰자를 연결하면, 관찰자는 LifecycleOwner(일반적으로 Activity 또는 Fragment)와 연결된다. LiveData는 STARTED, RESUMED와 같은 활성 수명 주기 상태인 관찰자만 업데이트 한다.

 

LiveData 객체의 setValue(), postValue()

postValue()

작업을 메인 스레드에 전달하여 지정된 값을 설정하도록 한다. 메인 스레드가 전달된 값을 실행하기 전에 postValue()를 여러 번 호출한 경우 마지막 값만 발송된다.

 

여러 번 호출한 경우 마지막 값만 전송되는 이유, postValue() 내부가 어떻게 생겼는지 찾아보자.

protected void postValue(T value) {
   boolean postTask;
   synchronized (mDataLock) {
       postTask = mPendingData == NOT_SET;
       mPendingData = value;
   }
   if (!postTask) {
       return;
   }
   ArchTaskExecutor.getInstance().postToMainThread(mPostValueRunnable);
}

 

LiveData 안에 postValue() 메서드는 이렇게 생겼다. 여기서 postTask 값이 중요하다.

우선 mPendingData 라는 값은 NOT_SET으로 초기화되어 있다. 그렇기 때문에 postValue()를 최초 호출한 경우 postTask 값은 (mPendingData == NOT_SET) 수식에 의해 true 값으로 설정되고, mPendingData가 우리가 설정하려고 하는 값으로 갱신된다.

 

postValue() 내부는 우선 이 정도만 보고 postToMainThread()를 보면,

@Override
public void postToMainThread(@NonNull Runnable runnable) {
    if (mMainHandler == null) {
        synchronized (mLock) {
            if (mMainHandler == null) {
               mMainHandler = createAsync(Looper.getMainLooper());
            }
        }
    }
    //noinspection ConstantConditions
    mMainHandler.post(runnable);
}

 

DefaultTaskExecutor의 postToMainThread 함수에서 postValue()의 identity가 실행된다. 이 곳에서 MainLooper를 갖는 mMainHandler를 생성하기 때문에 postValue는 Main Thread가 아니어도 값을 할당할 수 있게 된다.

 

앞 서 postValue()에서 mPostValueRunnable 객체를 postToMainThread에 전달했는데, 다수의 postValue()를 호출하면 handler 객체의 post()를 통해서 Main Looper의 message queue로 차곡차곡 전달되어야 하는 것이 아닌가? 라는 생각이 들 수 있다.

protected void postValue(T value) {
   boolean postTask;
   synchronized (mDataLock) {
       postTask = mPendingData == NOT_SET;
       mPendingData = value;
   }
   if (!postTask) {
      return;
   }
   ArchTaskExecutor.getInstance().postToMainThread(mPostValueRunnable);
}

 

하지만, 아까 봤던 postValue() 함수에서 postTask = (mPendingData == NOT_SET) 식에 의해 postTask가 false로 변경되기 때문에 if 문에 의해 runnable 객체가 postToMainThread()로 전달되지 않는다.

 

mPostValueRunnable 객체를 보면,

private final Runnable mPostValueRunnable = new Runnable() {
   @SuppressWarnings("unchecked")
   @Override
   public void run() {
       Object newValue;
       synchronized (mDataLock) {
           newValue = mPendingData;
           mPendingData = NOT_SET;
       }
       etValue((T) newValue);
   }
};

 

여기서 mPendingData가 다시 NOT_SET으로 할당된 후 setValue()를 호출하게 된다. NOT_SET으로 값이 변경되는 것이 중요한데, 앞 서 mPostValueRunnable 객체가 Message Queue에 남아 아직 처리되지 않았다면, mPendingData가 NOT_SET이 아닌 값으로 최신 값만 갱신된다는 것을 의미하며, 동시에 다른 Runnable 객체는 Message Queue에 쌓이지 않는다.

 

setValue()

value 값을 set한다. 반드시 메인 스레드에서 실행해야 하며, 만약 Observer 객체가 존재한다면, 변경된 값을 알림으로 전달한다.

@MainThread
protected void setValue(T value) {
    assertMainThread("setValue");
    mVersion++;
    mData = value;
    dispatchingValue(null);
}

 

assertMainThread() 함수에 의해서 만약 Main Thread가 아닌 곳에서 setValue()를 실행했다면 IllegalStateException이 발생한다.

version을 증가시키는 데, 현재 버전이 이전 버전보다 낮으면 return 되면서 onChanged가 호출되지 않는 역할을 한다.

 

dispatchingValue(null)을 실행한다.

void dispatchingValue(@Nullable ObserverWrapper initiator) {
   if (mDispatchingValue) {
       mDispatchInvalidated = true;
       return;
   }
   mDispatchingValue = true;
   do {
       mDispatchInvalidated = false;
       if (initiator != null) {
          considerNotify(initiator);
          initiator = null;
       } else {
           for (Iterator<Map.Entry<Observer<? super T>, ObserverWrapper>> iterator =
                  mObservers.iteratorWithAdditions(); iterator.hasNext(); ) {
               considerNotify(iterator.next().getValue());
               if (mDispatchInvalidated) {
                   break;
               }
           }
       }
   } while (mDispatchInvalidated);
   mDispatchingValue = false;
}

 

dispatchingValue 안에 몇 가지 boolean으로 선언된 변수들이 보이는 데, 중복 처리를 하지 않기 위한 안전 장치로 보인다.

else 블록에서 for문이 사용되는 이유는 하나의 LiveData 객체를 여러 곳에서 observe 할 수 있기 때문이다. LiveData와 연결되어 있는 다수의 Observer 들을 모두 호출하는데 이 때 considerNotify로 전달되는 객체는 ObserverWrapper 객체이다. ObserverWrapper 객체는 LifecycleOwner 정보와 Observer에 대한 정보를 담고 있다. 하나의 LiveData가 변경되었다면, 이 LiveData를 구독하고 있는 모든 Observer에게 알림을 전달한다.

중요한 것은 do-while 문 안에 considerNotify()라는 함수가 있는데 이 함수에서 변경된 값에 대한 알림을 보낸다.

 

private SafeIterableMap<Observer<? super T>, ObserverWrapper> mObservers = new SafeIterableMap<>();

 

mObservers라는 값은 LiveData 클래스 안에 전역 변수로 생성되어 있는 Map형 변수이다. 우리는 위에서 한 번도 mObservers에 key, value를 전달한 적이 없는데 이 함수에서 mObserver.onChanged((T) mData); 라는 수식이 보인다. mObservers.putIfAbsent(observer, wrapper); 사실 mObservers에 값을 추가하는 역할은 observe 함수에서 한다.

@MainThread
public void observe(@NonNull LifecycleOwner owner, @NonNull Observer<? super T> observer) {
   assertMainThread("observe");
   if (owner.getLifecycle().getCurrentState() == DESTROYED) {
       // ignore
       return;
   }
   LifecycleBoundObserver wrapper = new LifecycleBoundObserver(owner, observer);
   ObserverWrapper existing = mObservers.putIfAbsent(observer, wrapper);
   if (existing != null && !existing.isAttachedTo(owner)) {
        throw new IllegalArgumentException("Cannot add the same observer"
             + " with different lifecycles");
   }
   if (existing != null) {
       return;
   }
   owner.getLifecycle().addObserver(wrapper);
}

 

observe 함수는 많은 일을 한다. 먼저 수명 주기를 인식하는 LiveData이기 때문에 현재 상태를 체크한다.

owner.getLifecycle().addObserver(wrapper); observe의 매개변수로 받은 LifecycleOwner와 Observer로 ObserverWrapper를 생성하는데, 마찬가지로 Observer를 Owner의 수명주기에 연결하기 위한 작업을 객체 내부에서 한다.

mObservers.putIfAbsent(observer, wrapper); 매개변수 Observer와 생성한 ObserverWrapper를 mObservers라고 하는 Map 변수에 추가한다. 하나의 LiveData가 여러 번 observe될 수도 있고 Observer 객체를 object로 선언해서 같은 Observer를 여러 번 사용할 수도 있기 때문이다.

 

마지막으로 considerNotify() 함수가 있다.

private void considerNotify(ObserverWrapper observer) {
   if (!observer.mActive) {
       return;
   }
   // Check latest state b4 dispatch. Maybe it changed state but we didn't get the event yet.
   //
   // we still first check observer.active to keep it as the entrance for events. So even if
   // the observer moved to an active state, if we've not received that event, we better not
   // notify for a more predictable notification order.
   if (!observer.shouldBeActive()) {
       observer.activeStateChanged(false);
       return;
   }
   if (observer.mLastVersion >= mVersion) {
      return;
   }
   observer.mLastVersion = mVersion;
   observer.mObserver.onChanged((T) mData);
}

 

considerNotify의 매개변수로 ObserverWrapper가 전달된다. 함수를 보면 해당 observer가 ACTIVE 상태일 때만 변경된 값에 대한 알림을 보내도록 되어 있다. 전달 받은 ObserverWrapper의 mObserver 역시 observe 함수 안에서 객체를 생성할 때 이미 할당되었다. 할당이 되어 있기 때문에 onChanged() 함수를 호출해서 변경된 데이터인 mData 값을 전달해준다.

이런 로직을 통해, observer.mObserver.onChanged((T) mData); observer 객체의 onChanged() 함수를 호출한다. onChanged 함수는 앞 서 observe를 호출하면서 정의한 함수이다.

 

viewModel.someLiveData.observe(viewLifecycleOwner) { data ->
   // 우리가 정의한 Observer interface의 onChanged
}

 

결론

LiveData가 어떻게 수명 주기를 인식하는지, 알림을 어떻게 제공하는지 알아보았다. 단순히 공식 블로그에 나온 개념만 외우고 있을 때는 별로 중요하게 생각하지 않았는데, 막상 궁금해서 함수를 열어보니, 은근히 복잡했다. 평소 LiveData에 대해서 궁금했는데, 이번에 LiveData에 대해서 더 이해하게 된 것 같아서 뿌듯한 마음이다.

 


참고 자료 : https://velog.io/@ams770/AAC-LiveData-postValue-Deep-Dive

 

velog

 

velog.io

 

반응형