FragmentでViewの参照を持つとメモリリークする話と実装
Updated at Sat, Jan 18, 2020View Bindingのドキュメントが更新され、onDestroyViewのタイミングで保持しているBindingの参照を解放する節が追記されました。
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を使って、自動で参照を解放しているようです。
DroiKaigiでは、Adapterとか持ちたい場合もあるので、AACのサンプルにあるAutoCleardValueにしてみました https://t.co/IUNmeQLzfB
— takahirom (@new_runnable) January 17, 2020
次のように使います。
// 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!!
}
}
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の方法を参考にするのが良いと思います。
まとめ
- FragmentでViewの参照を持つBindingを保持するとメモリリークする
- 解放しておくとより丁寧
- AutoClearedValueが汎用的に使える
- Bindingの参照をvalにしたいなら、DataBinding-Ktxか、4の方法を参考にすると良さそう
追記
wada811さんから、DataBinding-ktxではonCreatedViewでの初期化が必須との指摘を頂き、修正しました。ご指摘ありがとうございます😄
DataBinding-ktx ですが、onCreateView での初期化は必須ですー
— wada811 (@wada811) January 18, 2020