本文已参加「新人创作礼」活动,一起敞开创作之路。
防止内存走漏
假如不同的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 }
}
}