티스토리 뷰

Kotlin사용자라면 Sythetic pulgin이 deprecated되고 ViewBinding(구글의 권장사항)으로 마이그레이션 해야된다는 걸 알고 있을 것이다. 그런데 ViewBinding을 프레그먼트에서 사용하게 될 경우 Memory leak과 관련된 이슈가 있는 것도 아마 알 것이다. (구글 문서에 나와있으므로)

 

아래는 개발자 문서의 샘플코드이다.(https://developer.android.com/topic/libraries/view-binding)

private var _binding: ResultProfileBinding? = null
// This property is only valid between onCreateView and
// onDestroyView.
private val binding get() = _binding!!

override fun onCreateView(
    inflater: LayoutInflater,
    container: ViewGroup?,
    savedInstanceState: Bundle?
): View? {
    _binding = ResultProfileBinding.inflate(inflater, container, false)
    val view = binding.root
    return view
}

override fun onDestroyView() {
    super.onDestroyView()
    _binding = null
}

그리고 이렇게 중요노트가 달려있다.

Note: Fragments outlive their views. Make sure you clean up any references to the binding class instance in the fragment's onDestroyView() method.

 

프레그먼트에 있는 뷰들이 원래 보다 오래 살아있을 수 있기 때문에 이런 증상이 발생한다는 것이다. 이건 구글이 프레그먼트를 리팩토링하면서 기본적으로 프레그먼트에 있는 뷰를 프레그먼트가 종료되어도 살수 있도록 변경했기 때문이다. 그렇게 때문에 GC가 메모리를 회수하려고 해도 binding이 레퍼런스를 계속 가지고 있기 때문에 회수하지 못하고 메모리 누수가 나게 되는 것이다. 왜 이렇게 했는지는 정확하게 이해하지 못하겠지만 말이다.

 

그런데, 프레그먼트가 몇 개 안되는 앱이라면 그냥 구글의 코드처럼 따라하면 될 것이다. 그러나 사이즈가 큰 앱같은 경우는 위와 같은 코드는 보일러플레이트 코드이고, 실수로 onDestoryView에 널을 세팅하는 것을 잊어버릴 수가 있다. 그러면 이 문제를 해결하려면 어떻게 하는 것이 좋을까?

 

이미 전 세계의 똑똑한 개발자들이 이에 대한 여러가지 해결방법을 제시하고 있는데, Property Delegation 을 이용하는 방법이 그 중의 하나이다. (https://zhuinden.medium.com/simple-one-liner-viewbinding-in-fragments-and-activities-with-kotlin-961430c6c07c)

 

소스코드를 전체 보기 전에 프레그먼트에서 우리가 원하는 모습의 코드를 보기로 하자. 우리는 이 코드가 아주 짧았으면 좋겠고, 자동으로 알아서 프레그먼트가 onDestory될 때 메모리를 해제해줫으면 좋겠다. 따라서 아래와 같은 모습으로 사용하면 좋을 것이다.

class HomdFragment: Fragment() {
   
   private val binding by viewBidning { FragmentHomeBinding.bind(requiredView()) }

}

 

위처럼, 사용할 수 있다면 정말 깔끔할 것 같다. 위에서 by는 Property Delegation 키워드이다.  by 뒤에 나오는 코드는 ReadOnlyProperty 또는 ReaWriteProperty를 구현해 주어야 한다. Property Delegate 는 해당 Property를 읽거나 쓰는 코드를 다른 클래스 등이 처리하도록 하는 것이다. 우리도 이 테크닉을 적용하여 View binding을 우리가 직접하지 않고 다른 클래스에 맡기도록 하자. 그런데 위처럼 Property Delegation을 하면서 nCreateView에서 바인딩하는 코드를 사용한다면 위와 같이 만들 필요가 전혀 없다. 그럼 어떻게 하면 onCreateView 에서 레이아웃을 inflate 하는 부분이 자동으로 처리되게 할 수 있을까?

실마리는, 프레그먼트 클래스의 생성자에 있다. 프레그먼트 클래스를 들여다 보면, 아래와 같은 생성자를 볼 수 있다.

@ContentView
public Fragment(@LayoutRes int contentLayoutId) {
    this();
    mContentLayoutId = contentLayoutId;
}
    
@MainThread
@Nullable
public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container,
            @Nullable Bundle savedInstanceState) {
    if (mContentLayoutId != 0) {
        return inflater.inflate(mContentLayoutId, container, false);
    }
    return null;
}

즉, Layout ID를 넘겨주면 onCreateView에서 자동으로 inflate를 해주게 되는 것이다. 따라서 우리가 호출하고 픈 모양의 Property호출은

class HomeFragment: Fragment(R.layout.fragment_home) {
   
   private val binding by viewBidning { FragmentHomeBinding.bind(requiredView()) }

}

 

여기서 viewBinding을 보면 일단은 프레그먼트에 속하는 함수이고 일기만 할 것이므로 ReadOnlyProperty를 리턴하도록 해야한다는 것을 알 수 있다. 그리고 viewBinding은 매개가 하나인데, bind 호출해서 requiredView()를 바인딩 해주고 있다. 모든 Binding class들은 ViewBinding interface를 구현하고 있고 Bind(View) 함수를 가지고 있다. 아래와 같은 모양이 될 것이다.

 

inding> Fragment.viewBinding(onBind: (View) -> T) = FragmentViewBindingDelegate<T>(this, onBind)

 

이제 FragmentViewBindingDelegate를 구현해 보자. 먼저 depency에 아래 라이브러들이 필요할 것이다.

implementation "androidx.lifecycle:lifecycle-runtime-ktx:2.3.1"
implementation "androidx.lifecycle:lifecycle-common-java8:2.3.1"
implementation "androidx.lifecycle:lifecycle-viewmodel-ktx:2.3.1"

구현된 코드이다.

FragmentViewBindingDelegate<BINDING: ViewBinding>(
    private val fragment: Fragment,
    private val onBind: (View) -> BINDING
): ReadOnlyProperty<Fragment, BINDING> {
    private var binding: BINDING? = null

    private val lifecycle get() = fragment.viewLifecycleOwner.lifecycle

    init {
        fragment.lifecycle.addObserver(object: DefaultLifecycleObserver {
            val viewLifecycleOwnerLiveDataObserver = Observer<LifecycleOwner?> { lifecycleOwner ->
                val viewLifecycleOwner = lifecycleOwner ?: return@Observer

                viewLifecycleOwner.lifecycle.addObserver(object: DefaultLifecycleObserver {
                    override fun onDestroy(owner: LifecycleOwner) {
                        binding = null
                    }
                })
            }

            override fun onCreate(owner: LifecycleOwner) {
                fragment.viewLifecycleOwnerLiveData.observeForever(viewLifecycleOwnerLiveDataObserver)
            }

            override fun onDestroy(owner: LifecycleOwner) {
                fragment.viewLifecycleOwnerLiveData.removeObserver(viewLifecycleOwnerLiveDataObserver)
            }
        })
    }

    override fun getValue(thisRef: Fragment, property: KProperty<*>): BINDING {
        val localBinding = binding
        if (localBinding != null) {
            return localBinding
        }

        if (!lifecycle.currentState.isAtLeast(Lifecycle.State.INITIALIZED)) {
            throw IllegalStateException("Should not attempt to get bindings when Fragment views are destroyed.")
        }

        return onBind(thisRef.requireView()).also {
            binding = it
        }
    }
}

 

키포인트는  프레그먼트의 Lifecycle에 Observer를 등록해서 뷰가 inflate되었다면, 뷰를 바인딩하는 부분과  Destory될 때 자동으로 bindig변수가 널이 되게 하는 것이다. 그리고 viewLifecycleOwnerLiveData를 observe함으로써 Fragment가 onCreate되었다가 바로 onDestory되는 경우가 생길 때 onStart나 onStop이 호출되지 않기 때문에 생길 수 있는 문제를 방지하는 코드가 들어간 점이다. 참고로 LiveData.observeForever는 observe가 프레임웤이 알아서 observer를 관리해주는 대신 observeForever는 항상 active상태가 되고 매뉴얼로 제거해주어야 한다. 테스트 코드나 위의 경우에는  observeForever가 적절한 사용이 될 것이다.

 

그럼, 액티비티는, 액티비티는 프레그먼트와는 다르기 때문에 메모리누수를 걱정할 필요가 없다. 위의 코드를 액티비티용으로 만들어서 사용할 수도 있을 것이다.

댓글
공지사항
최근에 올라온 글
최근에 달린 댓글
Total
Today
Yesterday
링크
«   2024/04   »
1 2 3 4 5 6
7 8 9 10 11 12 13
14 15 16 17 18 19 20
21 22 23 24 25 26 27
28 29 30
글 보관함