【小學生Android】二、我們來談談ViewPager的刷新機制存在的問題及其解決方案

**

參考文章:

**
https://www.jianshu.com/p/266861496508
http://blog.sina.com.cn/u/2017385987
https://blog.csdn.net/z13759561330/article/details/40737381
https://blog.csdn.net/happylishang/article/details/78961984

**

目錄

**

一.問題復現

1.從ViewPager的3種適配器說起
這裏我們先來盤一下ViewPager的三種適配器

PagerAdapter:
 重寫:getCount()、isViewFromObject()、instantiateItem()、destroyItem()
 對應的item爲View;

FragmentPagerAdapter:
 重寫:getCount()、getItem()、
 對應的item爲Fragment,適用於Fragment較少的情況;
 會緩存所有Fragment,爲每個fragment添加Tag,需要的時候根據Tag查找;
 首次顯示時候add,非首次根據Tag查找然後attach;
 移除的時候detach,注意這裏並未remove;

FragmentStateAdapter:
 重寫:getCount()、getItem()
 對應item爲Fragment,適用於Fragment較多的情況;
 只緩存最近顯示的Fragment ,不會爲fragment添加Tag ,需要的時候如果沒有緩存就重新創建;
 顯示時候如果沒有緩存就add,有緩存直接顯示
 移除的時候remove

2.問題來了,刷新dapter的數據源 Viewpager並不能及時正確地刷新UI頁面
 操作步驟:
  首先,對dapter進行增刪改的操作
  然後調用adapter.notifyDateSetChanged()觀察ViewPager的頁面
 親測結果如下:

PagerAdapter FragmentPagerAdapter FragmentStateAdapter
更新Item 生效但不及時 不生效 生效但不及時
增加Item 生效但不及時 不生效 crash
刪除Item 頁面錯亂 不生效 crash
清空Item 殘留當前頁面 OK 殘留當前頁面

二. 問題分析

1.知其然——問題很嚴重
更新生效但不及時: C級bug
更新新無效: C級+bug
頁面錯亂: B級bug
crash: A級bug
試想一下,如果我們自己的app出現這種bug,是何等的事故喲!但是這種事情就實實在在地發生了,而且還是發生在擁有10億級用戶的Android操作系統的官方api中!

2.知其所以然——Why!
讓我們來分析下這個問題的可能原因:
原因一、我們自己寫的adapter有問題。
原因二、這是bug。

排除法 搞起
case 原因二: 這個bug能發佈出來只有一種情況——Google發佈Android的api連冒煙測試都沒通過。可能性幾乎爲0 排除
case 原因一:是我們的代碼寫得不夠規範。可能性較大
那麼我們的代碼那裏寫的有問題呢?爲什麼按Google的api只重寫幾個PagerAdapter必要的抽象方法,在數據源更新的時候,調用notifyDateSetChanged()不能達到像ListView和RecyclerView那樣的效果呢?
這一切都要從PagerAdapter的刷新機制說起。
下圖是刷新單個Item的流程圖:

Created with Raphaël 2.2.0開始NotifyDateSetChanged調用getItemPosition是否=POSITION_NONEdestroyIteminstantiateItem結束yesno

調用流程:

  1. 調用 PagerAdapter 的 notifyDataSetChanged() 方法
  2. 觸發 getItemPosition() 方法,該方法會爲每個 Item 返回狀態碼POSITION_NONEPOSITION_UNCHANGED
  3. 如果是 POSITION_NONE,那麼該 Item 會被 destroyItem() 方法 remove 掉,然後instantiateItem()重新加載 顯示刷新後的頁面
  4. 如果是 POSITION_UNCHANGED,就不會重新加載,默認是 POSITION_UNCHANGED,無刷新效果

那麼問題來了,這個getItemPosition()是幹什麼的,我跟ta 認識,嗎?
我們回過頭來再來回顧我們寫的PagerAdapter都重寫了哪些方法
PagerAdapter:
 重寫:getCount()、isViewFromObject()、instantiateItem()、destroyItem()

FragmentPagerAdapter:
 重寫:getCount()、getItem()、

FragmentStateAdapter:
 重寫:getCount()、getItem()

並沒有找到這個getItemPosition()啊,別急既然這方法是PagerAdapter的,我們去源碼裏找下。


public int getItemPosition(@NonNull Object object) {
        return PagerAdapter.POSITION_UNCHANGED;//默認返回POSITION_UNCHANGED
    }

...

哈哈,“真相只有一個!”就是因爲父類PagerAdapter的getItemPosition()直接返回POSITION_UNCHANGED,才導致這一連串的刷新問題。
那麼,我們是不是隻要在子類中重寫該方法返回POSITION_NONE就萬事大吉了呢?So,easy?媽媽再也不用擔心我的Viewpager不刷新?

老辦法,我們試一下子!

PagerAdapter FragmentPagerAdapter FragmentStateAdapter
更新Item OK 不生效 OK
增加Item OK 不生效 OK
刪除Item OK 不生效 OK
清空Item OK OK OK

三. 解決方案

1.PagerAdapter 重寫該方法返回POSITION_NONE 可以解決刷新問題
2.FragmentPagerAdapter 效果並不理想 這跟它內部對Fragment的緩存有關
3.FragmentStateAdapter 重寫該方法返回POSITION_NONE 可以解決刷新問題

@Override
    public int getItemPosition(@NonNull Object object) {
        return POSITION_NONE;//返回POSITION_NONE 強制刷新
    }

關於1和3 的【可以解決】這裏有幾點說明一下:
First…我們這裏所謂的刷新數據源包括:增加,刪除、更新、清空
Second…這裏的刷新雖然能達到效果,但是所有Item都會刷新,不能實現類似RecyclerView只更新單個tem的效果;
Third…如果只是更新數據源 不涉及動態增加、刪除、清空操作,那麼 可以這樣優化:
 通過爲每個Item設置Tag,然後重建的時候根據Tag決定是否更新
注意 這種Tag方式只能保證更新操作不會出問題

關於2FragmentPagerAdapter的【效果並不理想
嘗試的解決方案是這樣的,先講下思路:
 在Adapter內維護一個所有fragment的集合fragments,注意這個集合需要在adapter內通過new創建,不能由外部指定,加載數據需要先clear 再addAll();
  每次外部數據源list有刷新操作的時候:
  然後通過fragmentManager將所有fragment移除
  先將內部維護的fragemnt清空,
  接着內部集合fragments 再addAll()
  最後調用notifyDataSetChanged()
完整代碼如下:

package com.darcy.hellotest.ui.viewpager.adapter;

import android.support.annotation.NonNull;
import android.support.v4.app.Fragment;
import android.support.v4.app.FragmentManager;
import android.support.v4.app.FragmentPagerAdapter;
import android.support.v4.app.FragmentTransaction;
import android.view.ViewGroup;

import java.util.ArrayList;
import java.util.List;

public class FragmentsAdapter extends FragmentPagerAdapter {

    private List<Fragment> fragments;//內維護一個所有fragment的集合fragments

    private FragmentManager fragmentManager;

    public FragmentsAdapter(FragmentManager fm, List<Fragment> list) {
        super(fm);
        this.fragmentManager = fm;
        fragments = new ArrayList<>();//集合需要在adapter內通過new創建
        setFragments(list);
    }

    /**
     * 刷新 通過修改adapter數據源實現
     * 每次更新刷新所有fragment
     *
     * @param fragments 新數據源
     */
    public void setFragments(List<Fragment> fragments) {
        if (this.fragments != null) {
            FragmentTransaction transaction = fragmentManager.beginTransaction();
            for (Fragment f : this.fragments) {//通過fragmentManager將所有fragment移除
                transaction.remove(f);
            }
            transaction.commit();
            fragmentManager.executePendingTransactions();
        }
        assert this.fragments != null;
        this.fragments.clear();//清空
        this.fragments.addAll(fragments);//addAll操作
        notifyDataSetChanged();//調用notifyDataSetChanged()
    }

    @Override
    public Fragment getItem(int i) {
        return fragments.get(i);
    }

    @Override
    public int getCount() {
        return fragments.size();
    }

    @NonNull
    @Override
    public Object instantiateItem(@NonNull ViewGroup container, int position) {
        return super.instantiateItem(container, position);
    }

    @Override
    public void destroyItem(@NonNull ViewGroup container, int position, @NonNull Object object) {
        super.destroyItem(container, position, object);
    }

    @Override
    public int getItemPosition(@NonNull Object object) {
        return POSITION_NONE;
    }

}

Activity中的刷新調用代碼如下:

    //真更新 通過修改適配器數據源實現
    private void refresh2() {
        fragments.set(0, PagerFragment.newInstance("更新測試"));
        mPagerAdapter.setFragments(fragments);
    }

    private void add() {
        fragments.add(0, PagerFragment.newInstance("添加Item測試"));
        mPagerAdapter.setFragments(fragments); 
    }

    private void delete() {
        fragments.remove(0);
        mPagerAdapter.setFragments(fragments);
    }

    private void clean() {
        fragments.clear();
        mPagerAdapter.setFragments(fragments);
    }

這個方法的實驗結果如下:

FragmentPagerAdapter
更新Item 有效 但是會導致下一個頁面首次加載時空白
增加Item 有效 但是會導致下一個頁面首次加載時空白
刪除Item 有效 但是會導致下一個頁面首次加載時空白
清空Item OK

很顯然 目前這個方案還不完善,哪位大佬知道,還望不吝賜教,這裏先行謝過了(教我,讓我做你的舔狗~~~)
所以我現在的做法是向FragmentStatePagerAdapter靠攏,既然FragmentPagerAdapter效果不理想,那就先拿FragmentStatePagerAdapter救急。

四. 總結

1.以上我們復現了ViewPager的刷新機制存在的問題——不能及時正確地更新頁面
2.接着我們分析了這種情況出現的原因——PagerAdapter的getItemPosition()直接返回POSITION_UNCHANGED 導致頁面不刷新
3.然後我們提出瞭解決方案:在子類中重寫該方法返回POSITION_NONE
  這個方案針對PagerAdapter和FragmentStatePagerAdapter效果理想
  對FragmentPagerAdapter由於其內部的緩存機制 效果並不理想
4.最後我們還嘗試針對FragmentPagerAdapter的緩存機制進行優化——強制清除緩存,但是效果也有瑕疵。期待大佬出來指點江山~~

此致:
我是漩渦小學生

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章