FragmentでViewの参照を持つとメモリリークする話と実装

View Bindingのドキュメントが更新され、onDestroyViewのタイミングで保持しているBindingの参照を解放する節が追記されました。

Use view binding in fragments

Fragment自体のライフサイクルのほうが、FragmentのViewのライフサイクルより長いので、FragmentでBindingの参照を保持するとリークしてしまうためです。

この記事では、メモリリークをしないために、どのような実装が考えられるかを紹介していきます。

1. onDestoryViewで解放する

公式ドキュメントに載っている方法です。

// onCreatedViewで初期化
private var _binding: ResultProfileBinding? = null
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() {
  _binding = null
}

onDestroyViewで参照を解放するコードを書きます。シンプルですが、冗長なのかなと思います。

2. AACサンプルで使っているAutoClearedValueを使う

takahiromさんにTwitterで教えてもらったんですが、AACサンプルではDelegationを使って、自動で参照を解放しているようです。



次のように使います。

// onCreatedViewで初期化する
var binding by autoCleared<RepoFragmentBinding>()
var adapter by autoCleared<RepoFragmentAdapter>()

AutoClearedValueは、viewLifecycleOwnerLiveDataを購読しており、onDestroyViewのタイミングで、自動的に参照を解放してくれます。また、ReycyclerView.Adapterでも同様に使うことが出来ます。

詳細な実装はAutoClearedValue.ktを見てください。

3. DataBinding-Ktxを使う

DataBinding-ktxを使うことで、valで定義することが可能になります。

private val binding: ViewBindingFragmentBinding by viewBinding()

override fun onCreateView(
  inflater: LayoutInflater,
  container: ViewGroup?,
  savedInstanceState: Bundle?
): View? {
  return binding.root
}

内部で、リフレクションを用いており、ViewBindingの場合でもbinding.rootとbindingにアクセスするだけで、自動的にBindingインスタンスを生成してくれます。 また、AutoClearedValueと同様に、viewLifecycleOwnerLiveDataを購読しており、自動で参照を解放してくれます。

4. View.setTag, getTagを使った実装を使う

自動的に解放する部分の実装の話なんですが、setTag、getTagを使った実装でも参照を解放することが可能です。

class MainFragment : Fragment(R.layout.main_frag2) {
  private val binding: MainFrag2Binding by viewBinding()
}

// ViewDataBinding.kt
fun <T : ViewDataBinding> Fragment.viewBinding(): ReadOnlyProperty<Fragment, T> =
  object : ReadOnlyProperty<Fragment, T> {
    override fun getValue(thisRef: Fragment, property: KProperty<*>): T {
      val view = thisRef.view!!
      var binding = view.getTag(R.id.fragment_binding) as? T
      if (binding == null) {
        binding = DataBindingUtil.bind(view)
        view.setTag(R.id.fragment_binding, binding)
      }
      return binding!!
    }
  }

Gist/サンプルコード

viewLifecycleOwnerLiveDataを使わないパターンの実装になります。また、このコードではFragmentのコンストラクタからレイアウトIDを渡すことを想定しています。 コンストラクタからIDを渡すことで、onCreateViewを省略することが出来ます。

この例では、DataBindingを想定していますが、ViewBindingで使う場合には、リフレクションを用いるか、もしくはファクトリを渡す必要があります。

リフレクションを使う場合は、次のようになります。

inline fun <reified T : ViewBinding> Fragment.viewBinding(): ReadOnlyProperty<Fragment, T> =
  object : ReadOnlyProperty<Fragment, T> {
    override fun getValue(thisRef: Fragment, property: KProperty<*>): T {
      val view = thisRef.view!!
      var binding = view.getTag(R.id.fragment_binding) as? T
      if (binding == null) {
        val method = T::class.java.getMethod("bind", View::class.java)
        binding = method.invoke(null, view) as T
        view.setTag(R.id.fragment_binding, binding)
      }
      return binding
    }
  }

個人的な感想

AACサンプルで使っているAutoClearedValueを使うのが良いのではと思っています。なぜかっていうと、RecyclerView.AdapterなどのBinding以外でも使うことが出来るからです。より汎用的だと思います。

とはいえ、bindingを保持する変数をvalにしたいよねって話であったり、コンストラクタからレイアウトID渡したいよねっていう話があると思うので、そのときは3、4の方法を参考にするのが良いと思います。

まとめ

追記

wada811さんから、DataBinding-ktxではonCreatedViewでの初期化が必須との指摘を頂き、修正しました。ご指摘ありがとうございます😄

Written by
あんどろいどでぃべろっぱぁー🍎