likes
comments
collection
share

Epoxy - 在RecyclerView中构建复杂界面 - 10

作者站长头像
站长
· 阅读数 35

本文已参与「新人创作礼」活动,一起开启掘金创作之路。

避免内存泄露

如果不同的RecyclerView使用相同的 Adapter 的话, 有 2 种可能的内存泄露. 一个常见场景是在Fragment的onCreate方法中创建和保存 Adapter 字段, 并且在跨越多个视图创建/销毁周期内重用它, 如果 Fragment 放入了backstack或者它的实例经过屏幕旋转残留了.

子视图

为了允许状态保存, Epoxy持有了每一个绑定视图. 为了防止这些视图泄露, 只是确保RecyclerView在用完视图之后将它们完全回收. 一种方式是通过recyclerView.setAdapter(null)方法将 Adapter 从RecyclerView中解绑(很可能是在Fragment的onDestroyView方法里面).

这种方式的缺陷是视图会马上清理掉, 所以如果要离开屏幕上做动画, 在动画完成之前屏幕会出现空白. 要避免空白的更好的选项是在RecyclerView从窗口中解绑时, 用LayoutManager回收子视图. 如果启用了setRecycleChildrenOnDetach(true)的话, LinearLayoutManagerGridLayoutManager会自动地进行回调.

要达到自动回收的目的, 可以在项目中创建一个继承自EpoxyAdapterBaseAdapter.

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发生变化, 都会重新绑定). 这是个可选的参数, 用于注解EpoxyAttributeModelProp.

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
评论
请登录