Epoxy - 在RecyclerView中构建复杂界面 - 10
本文已参与「新人创作礼」活动,一起开启掘金创作之路。
避免内存泄露
如果不同的RecyclerView使用相同的 Adapter 的话, 有 2 种可能的内存泄露. 一个常见场景是在Fragment的onCreate
方法中创建和保存 Adapter 字段, 并且在跨越多个视图创建/销毁周期内重用它, 如果 Fragment 放入了backstack或者它的实例经过屏幕旋转残留了.
子视图
为了允许状态保存, Epoxy持有了每一个绑定视图. 为了防止这些视图泄露, 只是确保RecyclerView在用完视图之后将它们完全回收. 一种方式是通过recyclerView.setAdapter(null)
方法将 Adapter 从RecyclerView中解绑(很可能是在Fragment的onDestroyView
方法里面).
这种方式的缺陷是视图会马上清理掉, 所以如果要离开屏幕上做动画, 在动画完成之前屏幕会出现空白. 要避免空白的更好的选项是在RecyclerView从窗口中解绑时, 用LayoutManager
回收子视图. 如果启用了setRecycleChildrenOnDetach(true)
的话, LinearLayoutManager
和GridLayoutManager
会自动地进行回调.
要达到自动回收的目的, 可以在项目中创建一个继承自EpoxyAdapter
的BaseAdapter
.
public class BaseAdapter extends EpoxyAdapter {
@Override
public void onAttachedToRecyclerView(RecyclerView recyclerView) {
// This will force all models to be unbound and their views recycled once the RecyclerView is no longer in use. We need this so resources
// are properly released, listeners are detached, and views can be returned to view pools (if applicable).
if (recyclerView.getLayoutManager() instanceof LinearLayoutManager) {
((LinearLayoutManager) recyclerView.getLayoutManager()).setRecycleChildrenOnDetach(true);
}
}
}
父视图
另外, 和子视图很相似, 指向RecyclerView本身的引用的泄露. 这个场景对于所有的RecyclerView Adapter 是固有的, 而不仅是Epoxy.
产生的场景与上面的相同, 即在RecyclerView销毁之后, Adapter 被保留了下来. 当RecyclerView设置 Adapter 的时候, RecyclerView注册了个Observer 监听数据项的变化(adapter.registerAdapterDataObserver(...)
). 对于RecyclerView而言, 很有必要知道 Adapter 数据项发生变化的时间.
Observer 只有在 Adapter 从RecyclerView上解绑的时候被移除(例如recyclerView.setAdapter(null)
). 有了 Fragment 中重建视图的常见模型, 要想不这么做很容易.
一个避免 RecyclerView 泄露的选项是在RecyclerView销毁的时候解绑它的 Adapter. 但它有上面提到的缺陷, 就是会立即清理视图.
另一个选项是清除Adapter的引用并且每次创建新的RecyclerView的时候重新创建一个Adapter的引用.
最后的一个选项是自定义RecyclerView子类, 当从窗口上解绑时移除自己的Adapter. 而对于RecyclerView内部嵌套的RecyclerView(例如轮播), 就不要解绑Adapter了. 示例app中的轮播代码展示了一种管理绑定和解绑嵌套轮播的更好的方式.
class MyRecyclerView extends RecyclerView {
@Override protected void onDetachedFromWindow() {
super.onDetachedFromWindow();
setAdapter(null);
// Or use swapAdapter(null, true) so that the existing views are recycled to the view pool
}
}
DoNotHash
Epoxy有Do Not Hash的概念, 即即使hashcode改变了, 也不要更新属性(与常规属性相反, 它们无论任何时候hash发生变化, 都会重新绑定). 这是个可选的参数, 用于注解EpoxyAttribute
和ModelProp
.
DoNotHash是性能优化. 它的预期使用是回调(例如点击监听器), 回调通常是匿名的, 每一个Model构建, 都会有不同的hashcode. 没有“Do Not Hash”的话, 每一次Model进行构建, 差异器都会将拥有点击监听器的Model识别为发生变化, 这将产生很多很多Model. 之后这些Model会全部重绑到Recyclerview.
通常这工作得很好, 但也有个很大的问题. 如果回调在它的闭包内捕获了任何变量, 那么这些变量在回调触发时可能会过期.
举个例子, 想要对象Animal用于创建一个EpoxyModel, 之后Model上的点击监听器持有了Animal的引用用于回调内. 如果Animal对象发生了变更, Model发生了重建, 新的点击监听器(持有新的Animal对象的引用)将不会绑定到视图上, 当点击触发的时候, 老的(且不正确的)列表将会用在回调内.
通常这种情况只有在数据是可变的情况下是个问题.
若不需要, 就不要使用DoNotHash
如果你不担心重新绑定点击监听器影响性能, 那就不要添加DoNotHash
选项👍 这也意味着避免使用@CallbackProp
, 因为它内部使用了DoNotHash. 这也许是个完全合适的方案, 也是最容易的方案.
注意: 如果在使用数据绑定, DoNotHash
对于未实现equals和hashcode类型的变量是默认开启的. 查看数据绑定文件获取更多信息来修改默认设置.
Epoxy的解决方案
要保持DoNotHash的好处的同时也避免操作的复杂性, Epoxy提供了OnModelClickListener
接口取代常规的View.OnClickListener. 这个监听器提供了EpoxyModel, View, 和点击的适配器的位置. Epoxy生成的代码特别处理了点击监听, 保证了点击时提供最新的EpoxyModel.
如果全部数据保存在Model中, 那么数据能够在onClick中检索到, 并且总是最新的. 你也许需要在Model中存储候选数据以用于点击回调.
这种方式的缺陷是它只对点击监听器有用, 其它任何类型的回调不能充分使用它.
重要: 如果在点击监听器中捕获的数据发生了变化, 但是Model中的其它属性没有变化, Model依然不会重新绑定, 因为Epoxy并不知道什么东西发生了变化. 请确保所有表示状态的数据在Model中捕获, 并且所有的属性正确在实现了equals和hashcode.
候选方案 #1
候选情况下, 可以避免在回调闭包中捕获任何状态. 举个例子, 只是捕获对象的ID, 并回调中央Controller报告拥有该ID的项被点击. 交给中央存储通过ID查找最新版本的项并采用正确的行动.
候选方案 #2
如果上面的方案对于你的场景并不适合, 那么可以使用KeyedListener. 它将监听器回调和值类型进行配对, 任何时候值发生变化, 回调就会被更新. 这是通用的解决方案, 可以进行裁剪以匹配自己的需求.
class KeyedListener<Key, Listener> private constructor(val identifier: Key, val callback: Listener) {
companion object {
@JvmStatic
fun <Key, Listener> create(identifier: Key, callback: Listener): KeyedListener<Key, Listener> {
return KeyedListener(identifier, callback)
}
}
// Only include the key, and not the listener, in equals/hashcode
override fun equals(other: Any?): Boolean {
if (this === other) return true
if (other !is KeyedListener<*, *>) return false
return identifier == other.identifier
}
override fun hashCode() = identifier?.hashCode() ?: 0
}
在Model/View中的用法像这样子:
@ModelProp(Option.NullOnRecycle)
fun setKeyedOnClickListener(listener: KeyedListener<*, OnClickListener>?) {
setOnClickListener(keyedListener?.callback);
}
创建Model时的用法像这样子:
animals.forEach {
animalModel {
id(it.id)
keyedOnClickListener(KeyedListener.create(it) { v: View -> // do something with the animal }
}
}
转载自:https://juejin.cn/post/7144527276523126815