對於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介面如圖:
程式碼如下:
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例項。這樣也算是目前的一種解決方案吧,這些目前還不最完美的解決方案,待日後瞭解更加深入之後,再仔細探討這個問題。
–