[Android]LeakCanaryに怒られない実装をする
はじめに
Androidアプリのメモリリークを検出してくれる LeakCanary というライブラリがあります。
プロジェクトに導入するだけでメモリリーク発生時にログを出力してくれて便利なのですが、わりと普通に組んでいるはずなのにメモリリークで怒られて「あれー?」となることがあります。
本記事では、わりとカジュアルに発生するメモリリーク事例をピックアップし、解消方法を紹介していきます。
1. DataBinding/ViewBindingでメモリリーク
公式でも紹介されている話なので知っている方も多いかと思います。
Fragmentで以下のような実装を行うとメモリリークします。
class MainFragment(R.layout.fragment_main) {
private lateinit var binding: FragmentMainBinding
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
binding = DataBindingUtil.bind(view)
binding.lifeCycleOwner = viewLifeCycleOwner
}
}
Fragment のライフサイクルと View のライフサイクルが別であり、View が破棄されても Fragment のライフサイクルが続くことが原因でリークが発生します。
解消方法には以下のようなものがあります。
- a. onDestoryView に解放処理を入れる
- b. binding の宣言をローカル変数にする
- c. DataBinding-ktx / ViewBinding-ktx を使用する ←(個人的オススメ)
a. onDestroyViewに解放処理を入れる
lateinit var
で宣言していた binding
を private var
で宣言し、 onDestroyView
で解放処理を入れます。公式で紹介されているスタンダードな方法です。
class MainFragment(R.layout.fragment_main) {
private var _binding: FragmentMainBinding? = null
private val binding get() = _binding!!
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
_binding = DataBindingUtil.bind(view)
binding.lifeCycleOwner = viewLifeCycleOwner
}
override fun onDestroyView() {
_binding = null
}
}
少し冗長な以外は特に問題点はありません。
b. binding の宣言をローカル変数にする
onViewCreated
内でだけ binding
を使う場合には、 binding
をローカル変数として宣言すればメモリリークは防げます。
class MainFragment(R.layout.fragment_main) {
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
val binding = DataBindingUtil.bind(view)
binding.lifeCycleOwner = viewLifeCycleOwner
}
}
基本的に DataBinding でレイアウトを組んでいて、Fragmentからは参照しない場合には使える方法かなと思います。
問題点は、 ViewBinding ではあまり使えないことです。
c. DataBinding-ktx / ViewBinding-ktx を使用する
wada811さんが公開している DataBinding-ktx / ViewBinding-ktx というライブラリを使用する方法です。
ライブラリを導入して by dataBinding()
で変数を宣言するだけでバインディングを行ってくれ、解放も自動的に行ってくれるので解放忘れなども防ぐことができます。
class MainFragment(R.layout.fragment_main) {
private val binding: FragmentMainBinding by dataBinding()
}
使用するライブラリに制限がなければ、積極的に利用していくことをオススメします。
2. RecyclerViewでメモリリーク
Fragment で RecyclerView を使用している時、他の画面へ遷移した後などに RecyclerView.adapter
がメモリリークを起こすことがあります。
これはどうやら Fragment が Adapter への参照を持ってしまっていると起きるようで、
class MainFragment : Fragment() {
val adapter = MainAdapter()
}
こちらの RecyclerView 周りで割と簡単に起きるメモリリーク 2つとその解決方法 の記事のように、 onDestroyView
で RecyclerView.adapter = null
を入れてあげることで解消しました。
override fun onDestroyView() {
recyclerView.adapter = null
super.onDestroyView()
}
3. ViewPager2でメモリリーク
ViewPager2 + FragmentStateAdapter + TabLayout の組み合わせで、別画面へ遷移した際にメモリリークが発生した例です。
Fragment
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
binding.pager.adapter = PagerAdapter(this)
TabLayoutMediator(binding.tabs, binding.pager) { tab, position ->
...
}.attach()
}
Adapter
class PagerAdapter(
fragment: Fragment
) : FragmentStateAdapter(fragment) {
...
}
特別なことは何もしていないはずだけどメモリリークが発生しておかしい…と思っていたところ、 IssueTracker に報告がありました。
ViewPager2 holds on to FragmentStateAdapter after detach
ViewPager2 が detach 後も FragmentStateAdapter への参照を持ち続けてしまっているらしく、メモリリークが発生してしまいます。
これは IssueTracker のコメント欄にもあるように、 Adapter に Fragment を直接渡さず、 childFragmentManager
と viewLifeCycleOwner.lifecycle
を渡すことで解消できました。
Fragment
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
val lifecycle = viewLifecycleOwner.lifecycle
binding.pager.adapter = PagerAdapter(childFragmentManager, lifecycle)
TabLayoutMediator(binding.tabs, binding.pager) { tab, position ->
...
}.attach()
}
override fun onDestroyView() {
binding.pager.adapter = null
super.onDestroyView()
}
Adapter
class PagerAdapter(
childFragmentManager: FragmentManager,
lifecycle: Lifecycle
) : FragmentStateAdapter(childFragmentManager, lifecycle) {
...
}
おわりに
ひとまずは以上です。
普通に生きているだけなのに LeakCanary に怒られてつらい…となっている人のお役に立てれば幸いです。
Discussion