Android Practice Manual-Will ViewBinding cause memory leaks?

  • Click to jump=>100 examples of Unity3D special effects
  • Click to jump=>Case project actual source code
  • Click to jump=>Game Script-Assisted Automation
  • Click to jump=>Android Control Complete Manual
  • Click to jump=>Scratch programming case
  • Click to jump=>Full series of soft exams

About the author

Focus onAndroid/Unity and various game development skills, andVarious resource sharing (websites, tools, materials, source code, games, etc.)
If you need anything, please send me a message on the card at the bottom. Communication makes learningno longer lonely.

Practical process

https://juejin.cn/post/7057144897060470815

Solution

Open source wheel

If you don’t want to do it yourself, you can directly use open source projects. Authors have already implemented it in GitHub, and have considered some boundary cases and optimizations. If you are interested, you can check out: ViewBindingPropertyDelegate

class FragmentA : Fragment() {<!-- -->
    private lateinit var imageView: ImageView
    
    override fun onViewCreated(view: View, savedInstanceState: Bundle?) {<!-- -->
        super.onViewCreated(view, savedInstanceState)
        imageView = view.findViewById(R.id.imageView)
    }
}

But this also encountered a problem: when using Navigation or using replace and addToBackStack to switch FragmentA to FragmentB, FragmentA will go to onDestroyView, but will not be destroyed. When FragmentA goes to onDestroyView, Fragment’s reference to the root View will be empty. Since imageView is held by Fragment, imageView is not released at this time, resulting in a memory leak.


When the page becomes complex, the declaration and assignment of variables will also become a repetitive task. More mature frameworks such as Butter Knife generate code through the @BindView annotation to avoid manually writing findViewById code. They also provide Unbinder to unbind in onDestoryView to prevent memory leaks. However, it is mentioned in the official documentation of Butter Knife that Butter Knife is no longer maintained and it is recommended to use ViewBinding as the view binding tool:

Attention: This tool is now deprecated. Please switch to view binding. Existing versions will continue to work, obviously, but only critical bug fixes for integration with AGP will be considered. Feature development and general bug fixes have stopped.

In the official documentation of ViewBinding, the recommended writing method is as follows:

class TestFragment : Fragment() {<!-- -->
    private var _binding: FragmentTestBinding? = null
    // Can only be used in the life cycle between onCreateView and onDestoryView
    private val binding: FragmentTestBinding get() = _binding!!
    
    override fun onCreateView(
        inflater: LayoutInflater,
        container: ViewGroup?,
        savedInstanceState: Bundle?
    ): View {<!-- -->
        _binding = FragmentTestBinding.inflate(inflater, container, false)
        return binding.root
    }
    
    override fun onDestroyView() {<!-- -->
        super.onDestroyView()
        _binding = null
    }
}

Although this method prevents memory leaks, it still requires manual writing of some repetitive code. Most people may even declare lateinit var binding directly, leading to more serious memory leaks. Below we will introduce two liberation options:

Option 1

If there is a BaseFragment in the project, we can put the above logic in the BaseFragment. However, this method adds generics to the base class, so it is relatively intrusive to existing projects.

open class BaseFragment<T : ViewBinding> : Fragment() {<!-- -->

    protected var _binding: T? = null
    
    protected val binding: T get() = _binding!!

    override fun onDestroyView() {<!-- -->
        super.onDestroyView()
        _binding = null
    }
}

Or go one step further and put the logic of onCreateView in the parent class:

abstract class BaseFragment<T : ViewBinding> : Fragment() {<!-- -->

    private var _binding: T? = null
    protected val binding: T get() = _binding!!

    abstract val bindingInflater: (LayoutInflater, ViewGroup?, Bundle?) -> T

    override fun onCreateView(
        inflater: LayoutInflater,
        container: ViewGroup?,
        savedInstanceState: Bundle?
    ): View {<!-- -->
        _binding = bindingInflater.invoke(inflater, container, savedInstanceState)
        return binding.root
    }

    override fun onDestroyView() {<!-- -->
        super.onDestroyView()
        _binding = null
    }
}

When used in subclasses:

class TestFragment : BaseFragment<FragmentTestBinding>() {<!-- -->
    
    override val bindingInflater: (LayoutInflater, ViewGroup?, Bundle?) -> FragmentTestBinding
        get() = {<!-- --> layoutInflater, viewGroup, _ ->
            FragmentTestBinding.inflate(layoutInflater, viewGroup, false)
        }
}

Option 2

With the help of Kotlin’s by keyword, we can hand over the binding task to the Frament life cycle for processing. The simpler version is as follows:

class LifecycleAwareViewBinding<F : Fragment, V : ViewBinding> : ReadWriteProperty<F, V>, LifecycleEventObserver {<!-- -->

    private var binding: V? = null

    override fun getValue(thisRef: F, property: KProperty<*>): V {<!-- -->
        binding?.let {<!-- -->
            return it
        }
        throw IllegalStateException("Can't access ViewBinding before onCreateView and after onDestroyView!")
    }

    override fun setValue(thisRef: F, property: KProperty<*>, value: V) {<!-- -->
        if (thisRef.viewLifecycleOwner.lifecycle.currentState == Lifecycle.State.DESTROYED) {<!-- -->
            throw IllegalStateException("Can't set ViewBinding after onDestroyView!")
        }
        thisRef.viewLifecycleOwner.lifecycle.addObserver(this)
        binding = value
    }

    override fun onStateChanged(source: LifecycleOwner, event: Lifecycle.Event) {<!-- -->
        if (event == Lifecycle.Event.ON_DESTROY) {<!-- -->
            binding = null
            source.lifecycle.removeObserver(this)
        }
    }
}

You can use the by keyword directly when using it, but you still need to assign it in onCreateView:

class TestFragment : Fragment() {<!-- -->
    private var binding: FragmentTestBinding by LifecycleAwareViewBinding()
    
    override fun onCreateView(
        inflater: LayoutInflater,
        container: ViewGroup?,
        savedInstanceState: Bundle?
    ): View {<!-- -->
        binding = FragmentTestBinding.inflate(inflater, container, false)
        return binding.root
    }
}

If you want to omit the repetitive logic of creating ViewBinding in onCreateView, there are two ideas. One is to pass in the layout ID when constructing the Fragment and create the ViewBinding through the bind function generated by viewBinding; the other is to call the inflate method of ViewBinding through reflection. The main difference between the two ideas is that the way to create ViewBinding is different, but the core code is the same, and the implementation is as follows:

class LifecycleAwareViewBinding<F : Fragment, V : ViewBinding>(
    private val bindingCreator: (F) -> V
) : ReadOnlyProperty<F, V>, LifecycleEventObserver {<!-- -->

    private var binding: V? = null

    override fun getValue(thisRef: F, property: KProperty<*>): V {<!-- -->
        binding?.let {<!-- -->
            return it
        }
        val lifecycle = thisRef.viewLifecycleOwner.lifecycle
        if (lifecycle.currentState == Lifecycle.State.DESTROYED) {<!-- -->
            this.binding = null
            throw IllegalStateException("Can't access ViewBinding after onDestroyView")
        } else {<!-- -->
            lifecycle.addObserver(this)
            val viewBinding = bindingCreator.invoke(thisRef)
            this.binding = viewBinding
            return viewBinding
        }
    }

    override fun onStateChanged(source: LifecycleOwner, event: Lifecycle.Event) {<!-- -->
        if (event == Lifecycle.Event.ON_DESTROY) {<!-- -->
            binding = null
            source.lifecycle.removeObserver(this)
        }
    }
}

Then create a function that returns LifecycleAwareViewBinding

// 1. Through bind function
fun <V : ViewBinding> Fragment.viewBinding(binder: (View) -> V): LifecycleAwareViewBinding<Fragment, V> {<!-- -->
    return LifecycleAwareViewBinding {<!-- --> binder.invoke(it.requireView()) }
}
// use
class TestFragment : Fragment(R.layout.fragment_test) {<!-- -->
    private val binding: FragmentTestBinding by viewBinding(FragmentTestBinding::bind)
}

// 2. Through reflection
inline fun <reified V : ViewBinding> Fragment.viewBinding(): LifecycleAwareViewBinding<Fragment, V> {<!-- -->
    val method = V::class.java.getMethod("inflate", LayoutInflater::class.java, ViewGroup::class.java, Boolean::class.java)
    return LifecycleAwareViewBinding {<!-- --> method.invoke(null, layoutInflater, null, false) as V }
}
// use
class TestFragment : Fragment() {<!-- -->
    private val binding: FragmentTestBinding by viewBinding()
    
    override fun onCreateView(
        inflater: LayoutInflater,
        container: ViewGroup?,
        savedInstanceState: Bundle?
    ): View {<!-- -->
        return binding.root
    }
}

It should be noted that the first method uses the Fragment#requireView method, so the layout ID needs to be passed to the Fragment’s constructor (passing the layout ID to the Fragment actually relies on the Fragment’s default onCreateView implementation, although the layout ID is not passed, Manual implementation is also possible, but this is actually almost the same as the method mentioned above).

Summary

For the template code that appears in ViewBinding to prevent memory leaks, you can extract the template code into the base class Fragment or use the life cycle of Fragment’s viewLifecycleOwner to automatically clean it up; for the template code that appears in onCreateView to create ViewBinding, you can use Fragment# The default implementation of onCreateView and the bind function generated by ViewBinding are created, or the ViewBinding is created by calling the inflate method generated by ViewBinding through reflection.

Other

Author: Xiao Kong in Xiao Kong and Xiao Zhi
Reprint instructions-be sure to indicate the source: https://zhima.blog.csdn.net/
This fellow Taoist, please stay, I think you have extraordinary bearing, and there is a hint of kingly domineering in your conversation, you will definitely do great things in the future! ! ! There is LikeCollect next to it. Send it to you today, click it. If you succeed in the future, I won’t take any money. If you don’t succeed, you can come back and find it. I.

Warm Tips: Click on the card below to get more unexpected resources.
Mr. Empty Name