前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
圈层
工具
发布
首页
学习
活动
专区
圈层
工具
MCP广场
社区首页 >专栏 >PagerAdapter 正确地移除 Item

PagerAdapter 正确地移除 Item

作者头像
bennyhuo
发布于 2020-02-20 05:18:29
发布于 2020-02-20 05:18:29
1.7K00
代码可运行
举报
文章被收录于专栏:BennyhuoBennyhuo
运行总次数:0
代码可运行

引子

很久没有写 UI 相关的程序,感觉都生疏了。最近用 FragmentPagerAdapter,配合 TabLayout,感觉还不错。不过很快就遇到了一个问题,我把 Adapter 里面的数据清空之后,再换一批进去,发现展示的 fragment总是有问题。为什么呢?

PagerAdapter 的 getItemPosition 方法

我们做 Android 这么多年,大家肯定比较熟悉这个套路,就是返回 item 的位置嘛。如果没有这个 item 呢?我们通常也习惯性的返回一个 -1,表示没有,这样做,也与很多 api 的设计的习惯一致:

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
public int indexOf(Object o) {
    ...
    return -1;
}

例如 ArrayList.indexOf 这个方法,如果找不到这个元素,那么就返回一个 -1

所以我们在实现 PagerAdaptergetItemPosition 时很自然的想到这么写:

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
override fun getItemPosition(item : Any?): Int {
    return items.indexOf(item)
}

看上去没有什么,一副人畜无害的样子。如果是在 ListView 或者 RecyclerView 当中这么写,应该没有什么问题,可偏偏这里是 PagerAdapter

我们来看下这个方法的注释:

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
/**
 * @return object's new position index from [0, {@link #getCount()}),
 *         {@link #POSITION_UNCHANGED} if the object's position has not changed,
 *         or {@link #POSITION_NONE} if the item is no longer present.
 */
public int getItemPosition(Object object) {
    return POSITION_UNCHANGED;
}

好像有点儿不一样的地方,注释说如果这个元素没有发生变化,那么就返回 POSITION_UNCHANGED,没有的话就返回 POSITION_NONE —— 聪明的我们一下就能想到这二位是俩整数常量,对应的值依次是 -1-2。是的,你没看错, -1 表示的是位置没有发生变化,而在我们的 indexOf 中, -1 恰恰表示没有。

所以这个方法如果按照上面的写法,就算你清空了 PagerAdapter 的数据,就算你调用了 notifyDataSetChanged,你看到的绝对跟你想要的不一样。

所以这个方法一定要正确实现,哪怕我们不去理会位置是不是发生了变化,至少像下面这样:

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
override fun getItemPosition(item : Any?): Int {
    val index = items.indexOf(item)
    return if(index == -1) 
            PagerAdapter.POSITION_NONE
        else index
}

就是这么尴尬,也不知道设计 PagerAdapter 的人到底是怎么想的,非要给我们添加点儿麻烦。

PagerAdapter 的 getItemId 方法

解决了一个问题,只是一个解决问题的开始罢了。

我在项目中的 adapter 实际上继承自 FragmentPagerAdapter,修改之后我发现清空数据也好、移除页面也好,有了较为正常的效果。不过,很快我就发现如果我清空了数据,并且重新添加了一个上去,显示出来的 Fragment 总是清空之前正在显示的那一个。

进过一番排查,原来在 FragmentPageAdapter 当中实例化 Fragment 的时候,会先通过 findFragmentByTag 查找当前 FragmentManager 当中是否已经存在了与之 name 相同的 Fragment,这个 name 的生成规则如下:

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
private static String makeFragmentName(int viewId, long id) {
    return "android:switcher:" + viewId + ":" + id;
}

具体调用如下:

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
final long itemId = getItemId(position);
String name = makeFragmentName(container.getId(), itemId);

也就是说,只要 itemId 相同的 fragmentFragmentManager 当中,下次显示的时候总是会被复用。

那么问题来了, getItemId 这个方法返回的是什么呢?

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
public long getItemId(int position) {
    return position;
}

它的默认实现就直接把位置返回了,所以如果你在清空数据之前所在的页面的位置和你添加新数据之后的位置如果恰好相同,那么这个新页面就不会被加载。

所以这个默认实现也是要小心的。其实最简单的做法就是返回一个与该 fragment 实例一一对应的 id,跟别人重了的怎么能叫 id 呢。

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
override fun getItemId(position: Int): Long {
    return items.getOrNull(position)
        ?.hashCode()?.toLong()?: -1L
}

FragmentPagerAdapter 为什么用 detach 而不是 remove Fragment

其实前面说了这么多,如果 FragmentPagerAdapter 能在 destroyItem 的时候把对应的 fragment 移除掉,那么也就没有这么多事儿了。

不巧的是,它老人家用的是 detach,而不是 remove

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
public void destroyItem(ViewGroup container, int position, Object object) {
    if (mCurTransaction == null) {
        mCurTransaction = mFragmentManager.beginTransaction();
    }
    mCurTransaction.detach((Fragment)object);
}

这意味着,这个 fragment 虽然从 UI 上被移除了,但生命周期还没有结束,如果需要的话,还是可以立马回来。

通过阅读代码不难发现, FragmentManagerImpl 当中有两个数组:

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
ArrayList<Fragment> mAdded;
SparseArray<Fragment> mActive;

前者存放的是添加到 UI 当中的 fragment,后者则还会保存 UI 已经销毁(调用 onDestroyView 之后)或者尚未初始化(尚未调用 onCreateView 之前)的 fragment。 detach 了之后这个 fragment 就会从 mAdded 中移除,但还会保留在 mActive 当中,这时候我们如果通过 findFragmentByTag 查找的话还是会把它给揪出来的:

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
public Fragment findFragmentByTag(String tag) {
    if (mAdded != null && tag != null) {
        for (int i=mAdded.size()-1; i>=0; i--) {
            ...
        }
    }
    if (mActive != null && tag != null) {
        for (int i=mActive.size()-1; i>=0; i--) {
            ...
        }
    }
    return null;
}

到现在为止,我们其实已经把所有存在的问题都搞清楚了,不过有个细节还需要交代一下,为什么用 detach而不是 remove

detach 对于后续召回这个页面比较有用,这种情况出现的概率也比较高,应该说绝大多数情况下使用 ViewPager 时,页面基本上是固定的,需要彻底 remove 的情况较少。

那么只用 detach 对于需要彻底 remove 的情况会造成内存泄露吗?

其实基本不会,毕竟 detach 的时候,UI 已经销毁, fragment 这时候也只会保留最基本的数据而已。


本文参与 腾讯云自媒体同步曝光计划,分享自微信公众号。
原始发表:2018-01-26,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 Kotlin 微信公众号,前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体同步曝光计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 引子
  • PagerAdapter 的 getItemPosition 方法
  • PagerAdapter 的 getItemId 方法
  • FragmentPagerAdapter 为什么用 detach 而不是 remove Fragment
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档