1. 程式人生 > >對於Fragment“懶載入”問題的一點點見解

對於Fragment“懶載入”問題的一點點見解

1. 問題來源

在開發過程中,或多或少會需要捕獲與Fragment生命週期相關的一些事件,去做相關的資料初始化等其他操作,而Fragment的生命週期並不完全像Activity那樣,兩者之間還是有一些區別的。

例如,我們想在使用者第一次看到該Fragment的時候去載入該Fragment中的資料,並非每次使用者看到Fragment都去載入資料,這時候就需要我們非常清楚Fragment的生命週期方法,才能實現理想中的效果。

然而對於初學或者不太瞭解Fragment生命週期的朋友,可能會在這裡產生一些錯誤的認知,比如本人剛開始學習Android的時候,就認為Fragment執行了onResume()方法之後,Fragment就處於可與使用者互動的狀態。

然而實際情況並不是這樣,例如現在大部分APP的設計都是底部幾個Button來控制切換Fragment的顯示與隱藏,在APP啟動的時候會同時建立這些Fragment,並新增到Activity中去,然後利用FragmentTransaction的show()和hide()方法動態的顯示或隱藏指定的Fragment。在Fragment新增到Activity中去的時候,不管Fragment有沒有顯示,它都已經走到onResume()生命週期了。此時實際情況是所有的Fragment都處在onResume()生命週期。

2. Demo示例

這只是一個簡單演示專案,目的為了看起來更加的直觀。

2.1 XML中嵌入Fragment,或使用FragmentManager的replace方法

在這種方式下,Fragment的生命週期onResume()即可表明當前Fragment對使用者可見,且處於可與使用者互動的狀態。

2.2 Activity+Fragment

在Activity中添加了4個Fragment,Fragment中只重寫了生命週期方法,列印log。

Activity介面如圖:

CSDN圖片伺服器異常,請稍後

程式碼如下:


public class MainActivity extends AppCompatActivity implements View.OnClickListener
{
FragmentA fragmentA = new FragmentA(); FragmentB fragmentB = new FragmentB(); FragmentC fragmentC = new FragmentC(); FragmentD fragmentD = new FragmentD(); Fragment currentFragment; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); // 新增Fragment getFragmentManager().beginTransaction() .add(R.id.activity_main, fragmentA, "A") .hide(fragmentA) .add(R.id.activity_main, fragmentB, "B") .hide(fragmentB) .add(R.id.activity_main, fragmentC, "C") .hide(fragmentC) .add(R.id.activity_main, fragmentD, "D") .hide(fragmentD) .commitAllowingStateLoss(); // 預設顯示FragmentA showFragment(fragmentA); findViewById(R.id.a).setOnClickListener(this); findViewById(R.id.b).setOnClickListener(this); findViewById(R.id.c).setOnClickListener(this); findViewById(R.id.d).setOnClickListener(this); } // 顯示Fragment private void showFragment(Fragment fragment) { if (currentFragment != null) { getFragmentManager().beginTransaction() .show(fragment).hide(currentFragment).commitAllowingStateLoss(); } else { getFragmentManager().beginTransaction() .show(fragment).commitAllowingStateLoss(); } currentFragment = fragment; } @Override public void onClick(View v) { switch (v.getId()) { case R.id.a: showFragment(fragmentA); break; case R.id.b: showFragment(fragmentB); break; case R.id.c: showFragment(fragmentC); break; case R.id.d: showFragment(fragmentD); break; } } }

啟動APP,可以看到Log資訊如下:

從日誌中可以看到,新增Fragment到Activity之後,Fragment都已經執行到了onResume()生命週期方法,但是我們只能看的到FragmentA,是因為其他的Fragment全都被hide了,也就對使用者不可見了,但是並不會走onPause()方法。

現在再注意這部分Log

12-26 09:30:34.720 12327-12327/cn.manchester.fragmentlazyload E/FragmentA: onHiddenChanged: true
12-26 09:30:34.720 12327-12327/cn.manchester.fragmentlazyload E/FragmentA: onHiddenChanged: false

這裡的onHiddenChanged()方法是在我們呼叫show()方法的時候,Fragment會回撥的方法,其中引數為true的時候,表示該Fragment被隱藏,否則即顯示。有些人可能會想,那我用這個方法不就可以判斷當前Fragment是否對使用者可見了嗎。當然如果僅僅是這種情況下,的確是可以這樣做。

如果此時按下Home鍵返回到主螢幕,列印的Log資訊如下:

12-26 09:50:48.490 12327-12327/cn.manchester.fragmentlazyload E/FragmentA: onPause: 
12-26 09:50:48.490 12327-12327/cn.manchester.fragmentlazyload E/FragmentB: onPause: 
12-26 09:50:48.490 12327-12327/cn.manchester.fragmentlazyload E/FragmentC: onPause: 
12-26 09:50:48.490 12327-12327/cn.manchester.fragmentlazyload E/FragmentD: onPause: 
12-26 09:50:48.960 12327-12327/cn.manchester.fragmentlazyload E/FragmentA: onStop: 
12-26 09:50:48.960 12327-12327/cn.manchester.fragmentlazyload E/FragmentB: onStop: 
12-26 09:50:48.960 12327-12327/cn.manchester.fragmentlazyload E/FragmentC: onStop: 
12-26 09:50:48.960 12327-12327/cn.manchester.fragmentlazyload E/FragmentD: onStop: 

可見雖然所有的Fragment對於使用者不可見,但是卻沒有回撥onHiddenChanged()方法,由此可知,只有我們在手動呼叫show()或hide()的時候才會回撥onHiddenChanged()方法,僅僅靠這個方法是無法確定Fragment當前的狀態的。

這時,我們又會想,那給Fragment一個boolean值,在onPause()的時候,設定為false,表明當前Fragment對使用者不可見,不就可以解決這個問題了嗎。這時候就需要了解下ViewPager+Fragment的工作機制。

2.3 ViewPager+Fragment

ViewPager+Fragment的“預載入”問題,ViewPager會預先載入當前顯示的Fragment的左右兩個Fragment,即A,B,C,D 4個Fragment,ViewPager當前顯示的是Fragment C的話,它也會預先載入B和D,這樣是為了ViewPager在滑動的時候更加的流暢,預先載入B和D的時候並不會回撥onHiddenChanged()方法。

Activity介面如圖:

這裡寫圖片描述
Activity程式碼如下:

public class Main2Activity extends AppCompatActivity
{
    ViewPager viewPager;

    @Override
    protected void onCreate(Bundle savedInstanceState)
    {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main2);
        viewPager = (ViewPager) findViewById(R.id.viewPager);

        List<Fragment> list = new ArrayList<>();
        list.add(new FragmentA());
        list.add(new FragmentB());
        list.add(new FragmentC());
        list.add(new FragmentD());
        viewPager.setAdapter(new MyPagerAdapter(getSupportFragmentManager(), list));
    }

    public static class MyPagerAdapter extends FragmentPagerAdapter
    {
        private List<Fragment> fragmentList;

        public MyPagerAdapter(FragmentManager fm, List<Fragment> fragmentList)
        {
            super(fm);
            this.fragmentList = fragmentList;
        }

        @Override
        public Fragment getItem(int position)
        {
            return fragmentList.get(position);
        }

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

啟動APP,觀察Log:

12-26 10:08:17.055 32679-32679/cn.manchester.fragmentlazyload E/FragmentA: setUserVisibleHint: false
12-26 10:08:17.055 32679-32679/cn.manchester.fragmentlazyload E/FragmentA: getUserVisibleHint: false
12-26 10:08:17.055 32679-32679/cn.manchester.fragmentlazyload E/FragmentB: setUserVisibleHint: false
12-26 10:08:17.055 32679-32679/cn.manchester.fragmentlazyload E/FragmentB: getUserVisibleHint: false
12-26 10:08:17.055 32679-32679/cn.manchester.fragmentlazyload E/FragmentA: setUserVisibleHint: true
12-26 10:08:17.055 32679-32679/cn.manchester.fragmentlazyload E/FragmentA: getUserVisibleHint: true
12-26 10:08:17.055 32679-32679/cn.manchester.fragmentlazyload E/FragmentA: onAttach: 
12-26 10:08:17.056 32679-32679/cn.manchester.fragmentlazyload E/FragmentA: onCreate: 
12-26 10:08:17.056 32679-32679/cn.manchester.fragmentlazyload E/FragmentA: onCreateView: 
12-26 10:08:17.057 32679-32679/cn.manchester.fragmentlazyload E/FragmentA: onStart: 
12-26 10:08:17.057 32679-32679/cn.manchester.fragmentlazyload E/FragmentB: onAttach: 
12-26 10:08:17.057 32679-32679/cn.manchester.fragmentlazyload E/FragmentB: onCreate: 
12-26 10:08:17.057 32679-32679/cn.manchester.fragmentlazyload E/FragmentB: onCreateView: 
12-26 10:08:17.069 32679-32679/cn.manchester.fragmentlazyload E/FragmentA: onResume: 
12-26 10:08:17.069 32679-32679/cn.manchester.fragmentlazyload E/FragmentB: onStart: 
12-26 10:08:17.069 32679-32679/cn.manchester.fragmentlazyload E/FragmentB: onResume: 

可以看到,預先載入了Fragment B,沒有回撥onHiddenChanged()方法,而是呼叫了setUserVisibleHint()方法,該方法的引數是一個boolean值,這個值表明了,當前fragment是否對使用者可見,繼續滑動到下一個頁面的話,又會預先載入Fragment C。

綜上所述,若Fragment處於onPause生命週期,此Fragment不可與使用者互動,即沒有處在foreground。若Fragment處於onResume生命週期,此Fragment也不一定能與使用者進行互動,需要結合onResume(),onHiddenChanged(),setUserVisibleHint()方法來確定Fragment實際所處位置。即如下“懶載入”Fragment:

public class LazyLoadFragment extends Fragment
{
    // 第一次載入
    private boolean isFirstLoad = true;
    private boolean isVisibleToUser;

    @Nullable
    @Override
    public View onCreateView(LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState)
    {
        return super.onCreateView(inflater, container, savedInstanceState);
    }

    /**
     * Activity+Fragment,isVisibleToUser總是為true
     * @param isVisibleToUser
     */
    @Override
    public void setUserVisibleHint(boolean isVisibleToUser)
    {
        super.setUserVisibleHint(isVisibleToUser);
        this.isVisibleToUser = isVisibleToUser && !isHidden();
    }

    /**
     *
     * ViewPager+Fragment,hidden總是為false
     * @param hidden
     */
    @Override
    public void onHiddenChanged(boolean hidden)
    {
        super.onHiddenChanged(hidden);
        isVisibleToUser = !hidden && getUserVisibleHint();
    }

    @Override
    public void onResume()
    {
        super.onResume();
        if (isVisibleToUser)
        {
            if (isFirstLoad)
            {
                lazyLoad();
                isFirstLoad = false;
            }
            onShow();
        }
    }

    @Override
    public void onPause()
    {
        super.onPause();
        isVisibleToUser = false;
    }

    protected void onShow()
    {

    }

    protected void lazyLoad()
    {

    }
}

上面的程式碼還不能適應所有情況,比如當activity重建之後,所有新增到activity的中fragment也隨之銷燬,重建,此時就會導致回撥所有fragment的onShow()方法,也可以通過呼叫fragment的setRetainInstance(true)方法解決這個問題,在activity重建的時候儲存fragment例項。這樣也算是目前的一種解決方案吧,這些目前還不最完美的解決方案,待日後瞭解更加深入之後,再仔細探討這個問題。

謝謝各位的耐心閱讀以及提出的寶貴意見與建議。