Android開發-從原始碼分析Fragment巢狀PagerAdapter生命週期,解決重建問題
介紹
眾所周知在Android開發中Fragment的生命週期非常複雜,複雜得甚至讓Square公司提出了我為什麼主張反對使用Android Fragment轉而提倡使用自定義View組合替代Fragment。但是沒辦法公司專案還是使用了很多Fragment巢狀。遇到問題還是需要自己去處理的。
這裡以Fragment的狀態儲存和恢復(即重建)來討論一些關於Fragment的生命週期問題。
有隱患的程式碼
不知道各位有沒有寫過下面,類似的程式碼。
public class TabActivity extends AppCompatActivity { @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_tab); Toolbar toolbar = (Toolbar) findViewById(R.id.toolbar); setSupportActionBar(toolbar); SectionsPagerAdapter mSectionsPagerAdapter = new SectionsPagerAdapter(getSupportFragmentManager()); ViewPager mViewPager = (ViewPager) findViewById(R.id.container); mViewPager.setAdapter(mSectionsPagerAdapter); TabLayout tabLayout = (TabLayout) findViewById(R.id.tabs); tabLayout.setupWithViewPager(mViewPager); } //互動的介面定義 public interface OnInteractionListener { void action(String action); } public static class PlaceholderFragment extends Fragment { private static final String ARG_SECTION_NUMBER = "section_number"; public PlaceholderFragment() { } private OnInteractionListener listener; public void setListener(OnInteractionListener listener) { this.listener = listener; } public static PlaceholderFragment newInstance(int sectionNumber) { PlaceholderFragment fragment = new PlaceholderFragment(); Bundle args = new Bundle(); args.putInt(ARG_SECTION_NUMBER, sectionNumber); fragment.setArguments(args); return fragment; } @Override public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { View rootView = inflater.inflate(R.layout.fragment_tab, container, false); TextView textView = (TextView) rootView.findViewById(R.id.section_label); textView.setText( getString(R.string.section_format, getArguments().getInt(ARG_SECTION_NUMBER))); //使用介面 和Activity互動 listener.action("fragment response view is build"); return rootView; } } public class SectionsPagerAdapter extends FragmentPagerAdapter { public SectionsPagerAdapter(FragmentManager fm) { super(fm); } /** * 在get方法中 返回Fragment例項 並設定介面回撥 * @param position * @return */ @Override public Fragment getItem(int position) { Logger.d("position = "+position); PlaceholderFragment fragment = PlaceholderFragment.newInstance(position + 1); //注入介面例項 列印輸出 fragment.setListener(new OnInteractionListener() { @Override public void action(String action) { Logger.d(action); } }); return fragment; } @Override public int getCount() { return 3; } @Override public CharSequence getPageTitle(int position) { switch (position) { case 0: return "SECTION 1"; case 1: return "SECTION 2"; case 2: return "SECTION 3"; } return null; } } }
基本思路是使用ViewPager顯示Fragment,使用FragmentPagerAdapter管理Fragment,並在建立的地方加入外部介面例項注入過程。
這段程式碼看起來沒什麼問題,跑起來也沒有問題。但是有很大隱患。
首先程式碼是使用Android Studio自動生成的,操作如下。我在原有基礎上加上了Fragment和Activity的通過介面互動的操作(有註釋的部分程式碼)。
如果按上一篇提到的開發者選項->開啟不保留活動
測試切換後的Activity恢復重建。然後程式就崩潰了!!,原因竟然是NullPointerException空指標,因為互動的介面例項為空。
分析原始碼
因為介面例項的注入在FragmentPagerAdapter
的getItem
中完成,我們發現問題的入口就是FragmentPagerAdapter。
FragmentPagerAdapter:只貼出相關原始碼
public abstract class FragmentPagerAdapter extends PagerAdapter { private final FragmentManager mFragmentManager; public FragmentPagerAdapter(FragmentManager fm) { mFragmentManager = fm; } /** * Return the Fragment associated with a specified position. */ public abstract Fragment getItem(int position); @Override public Object instantiateItem(ViewGroup container, int position) { if (mCurTransaction == null) { mCurTransaction = mFragmentManager.beginTransaction(); } final long itemId = getItemId(position); // Do we already have this fragment? String name = makeFragmentName(container.getId(), itemId); Fragment fragment = mFragmentManager.findFragmentByTag(name); if (fragment != null) { if (DEBUG) Log.v(TAG, "Attaching item #" + itemId + ": f=" + fragment); mCurTransaction.attach(fragment); } else { fragment = getItem(position); if (DEBUG) Log.v(TAG, "Adding item #" + itemId + ": f=" + fragment); mCurTransaction.add(container.getId(), fragment, makeFragmentName(container.getId(), itemId)); } if (fragment != mCurrentPrimaryItem) { fragment.setMenuVisibility(false); fragment.setUserVisibleHint(false); } return fragment; } private static String makeFragmentName(int viewId, long id) { return "android:switcher:" + viewId + ":" + id; } }
從原始碼中發現instantiateItem
中呼叫getItem(int
position)是有條件的。只有當上一步嘗試在FragmentManager
抽象類中查詢不到特定的Fragment時才會呼叫,去子類中獲取Fragment例項。
而FragmentManager
是由外部注入的,注入的是抽象定義沒有實現程式碼,且findFragmentByTag
方法是怎麼樣根據name去查詢Fragment的。
我們得到以下兩個問題:
1:FragmentManager的例項是什麼?
2:findFragmentByTag查詢的是什麼?
FragmentActivity管理Fragment
檢視原始碼我們來到FragmentActivity,它的主要功能就是對巢狀在他內部的Fragment進行管理。
FragmentActivity:
public class FragmentActivity {
final FragmentController mFragments = FragmentController.createController(new HostCallbacks());
//FragmentActivity對內部狀態的儲存操作
@Override
protected void onSaveInstanceState(Bundle outState) {
super.onSaveInstanceState(outState);
Parcelable p = mFragments.saveAllState();
if (p != null) {
outState.putParcelable(FRAGMENTS_TAG, p);
}
}
}
/**
* Return the FragmentManager for interacting with fragments associated
* with this activity.
*/
public FragmentManager getSupportFragmentManager() {
return mFragments.getSupportFragmentManager();
}
由以上這些程式碼,我們可以知道對於Fragment的控制,FragmentActivity其實是通過FragmentController
實現的
FragmentController
public class FragmentController {
/**
* Returns a {@link FragmentManager} for this controller.
*/
public FragmentManager getSupportFragmentManager() {
return mHost.getFragmentManagerImpl();
}
}
通過這行程式碼我們最終來到了目的地
FragmentManagerImpl:它是FragmentManager抽象類的具體實現類
final class FragmentManagerImpl extends FragmentManager implements LayoutInflaterFactory {
ArrayList<Fragment> mActive;
ArrayList<Fragment> mAdded;
@Override
public Fragment findFragmentByTag(String tag) {
if (mAdded != null && tag != null) {
// First look through added fragments.
for (int i=mAdded.size()-1; i>=0; i--) {
Fragment f = mAdded.get(i);
if (f != null && tag.equals(f.mTag)) {
return f;
}
}
}
if (mActive != null && tag != null) {
// Now for any known fragment.
for (int i=mActive.size()-1; i>=0; i--) {
Fragment f = mActive.get(i);
if (f != null && tag.equals(f.mTag)) {
return f;
}
}
}
return null;
}
}
findFragmentByTag:是從內部持有的Fragment集合中根據tag名稱查詢的。
Fragment的備忘錄模式應用
我們通過原始碼回答了前面提出的兩個問題,但是還是沒有弄清楚崩潰的原因。
但是我們發現ArrayList<Fragment> mActive
內部變數。根據對備忘錄模式的理解,肯定存在對該變數的備忘錄封裝操作。
回到前面的部分FragmentActivity對內部狀態的儲存操作
我們看看FragmentManagerImpl關於備忘錄模式的實現,篇幅有限只看儲存操作。
FragmentManagerImpl:
Parcelable saveAllState() {
//省略其他操作 只看關鍵程式碼
// First collect all active fragments.
int N = mActive.size();
FragmentState[] active = new FragmentState[N];
for (int i=0; i<N; i++) {
Fragment f = mActive.get(i);
FragmentState fs = new FragmentState(f);
active[i] = fs;
}
FragmentManagerState fms = new FragmentManagerState();
fms.mActive = active;
fms.mAdded = added;
fms.mBackStack = backStack;
return fms;
}
Fragment的備忘錄物件實現:
final class FragmentState implements Parcelable {
final String mClassName;
final int mIndex;
final boolean mFromLayout;
final int mFragmentId;
final int mContainerId;
final String mTag;
final boolean mRetainInstance;
final boolean mDetached;
final Bundle mArguments;
final boolean mHidden;
Bundle mSavedFragmentState;
Fragment mInstance;
public FragmentState(Fragment frag) {
mClassName = frag.getClass().getName();
mIndex = frag.mIndex;
mFromLayout = frag.mFromLayout;
mFragmentId = frag.mFragmentId;
mContainerId = frag.mContainerId;
mTag = frag.mTag;
mRetainInstance = frag.mRetainInstance;
mDetached = frag.mDetached;
mArguments = frag.mArguments;
mHidden = frag.mHidden;
}
}
FragmentManager的備忘錄實現:
final class FragmentManagerState implements Parcelable {
FragmentState[] mActive;
int[] mAdded;
BackStackState[] mBackStack;
public FragmentManagerState() {
}
}
所以基於以上的Fragment的備忘錄模式的實現,Android系統能夠保證當FragmentActivity被銷燬後,重新返回時的重建。恢復到離開時的狀態。
情景分析
在看了這麼多原始碼之後,重新返回思考剛才的重建後崩潰NPE問題,分析情景如下:
- 第一次構建FragmentPagerAdapter,FragmetManager中Fragment集合為空,需要getItem執行返回Fragment例項,UI得以顯示。
- 使用者離開當前Activity,onSaveInstanceState儲存操作被回撥執行,Fragment被FragmetManager控制器調起儲存操作,儲存內部狀態。
- 使用者回到Activity,備忘錄的資料恢復操作開始執行,就是
onCreate(Bundle savedInstanceState)
方法中Bundle有被儲存下來的資料,FragmetManager被恢復成之前的狀態,Fragment容器中有內容。 - 再次構建FragmentPagerAdapter,但是通過FragmetManager例項能夠
findFragmentByTag
得到Fragment例項。 - 整個恢復過程由原始碼配合完成,我們的FragmentPagerAdapter子類的getItem方法沒有被調起執行。
- 最後當所有得原始碼都執行完,執行到我們所寫的Fragment子類,一個沒有被恢復的物件也就是我們由外部注入的介面例項,肯定為空,然後就發生NullPointerException空指標崩潰。
問題的解決方案
知道了問題發生的原因,解決方案的思路就很清晰,因為資料的重建恢復完全由原始碼完成,我們所能做的就是配合原始碼的執行,在適當的生命週期新增適當的程式碼。
改變外部物件的注入方式
配合Fragment的生命週期,改變外部物件的注入方式,比如這樣
Activity實現介面
public class TabActivity extends AppCompatActivity implements OnInteractionListener {
}
Fragment從生命週期中獲取外部物件
public static class PlaceholderFragment extends Fragment {
@Override public void onAttach(Context context) {
super.onAttach(context);
if (context instanceof OnInteractionListener){
listener= (OnInteractionListener) context;
}
}
優點:可以很簡單的實現解決問題,程式碼量比較少。
缺點:Fragmen和Activity的介面實現方式隱式繫結,不容易理解。且對於ViewPager這樣的,還需要再新增程式碼集中控制Fragment集合。
繼承FragmentPagerAdapter新增新方法
根據FragmentPagerAdapter的原始碼,重新定義建立和繫結過程
import android.support.annotation.Nullable;
import android.support.v4.app.Fragment;
import android.support.v4.app.FragmentManager;
import android.support.v4.app.FragmentPagerAdapter;
import android.support.v4.view.ViewPager;
import android.view.ViewGroup;
import java.util.ArrayList;
import java.util.List;
/**
* Created by LiCola on 2017/6/5. 按照FragmentActivity和FragmentPagerAdapter
* 對子Fragment的生命週期和重建的順序特性抽象的父類,
* 建議專案中所有的有關FragmentPagerAdapter 都直接繼承該抽象類,或按照該思路管理Fragment
*/
public abstract class FragmentPagerRebuildAdapter<T extends Fragment> extends FragmentPagerAdapter {
private final T PLACE_FRAGMENT = null;
protected final int pageSize;
protected List<T> fragments;
public FragmentPagerRebuildAdapter(FragmentManager fm, int pageSize) {
super(fm);
this.pageSize = pageSize;
fragments = loadPlaceFragment(pageSize);
}
/**
* 根據位置引數建立並返回一個Fragment例項 該方法FragmentActivity在新建Fragment時呼叫,銷燬後重建時不會呼叫
*
* @param position 位置引數
* @return 建立好的Fragment例項
*/
protected abstract T createFragment(int position);
/**
* 操作某個Fragment,設定或繫結操作或資料 該方法,新建或重建都呼叫
*
* @param fragment 對某個位置的Fragment
* @param position 某個位置的位置引數
*/
protected abstract void bindFragment(T fragment, int position);
private void unbindFragment(T fragment, int position) {
}
public List<T> getFragmentList() {
return fragments;
}
/**
* 根據傳入位置 得到Fragment
*/
@Nullable
public T getFragmentByPosition(int position) {
if (fragments == null || fragments.size() == 0 || position >= fragments.size()) {
return null;
}
return fragments.get(position);
}
/**
* 得到ViewPager當前頁的Fragment
*/
@Nullable
public T getFragmentByCurrentItem(ViewPager viewPager) {
if (viewPager == null) {
return null;
}
return getFragmentByPosition(viewPager.getCurrentItem());
}
/**
* 獲取例項方式 該方法的position可能會亂序輸入,所以使用set方式
*/
@Override
public Object instantiateItem(ViewGroup container, int position) {
Object object = super.instantiateItem(container, position);
bindFragment((T) object, position);
fragments.set(position, (T) object);
return object;
}
@Override
public Fragment getItem(int position) {
Fragment fragment = createFragment(position);
if (fragment == null) {
throw new UnsupportedOperationException("createFragment(position="
+ position
+ " 沒有返回Fragment例項),檢查程式碼確保createFragment方法覆蓋所有position");
}
return fragment;
}
@Override
public void destroyItem(ViewGroup container, int position, Object object) {
super.destroyItem(container, position, object);
unbindFragment((T) object, position);
}
@Override
public int getCount() {
return pageSize;
}
/**
* 初始化List集合,並使用佔位物件填充,否則無法使用ArrayList的set直接填充指定位置的資料
* @param pageSize
* @return
*/
private List<T> loadPlaceFragment(int pageSize) {
if (pageSize <= 0) {
throw new IllegalArgumentException("FragmentPagerRebuildAdapter pageSize<=0");
}
ArrayList<T> placeList = new ArrayList<>(pageSize);
for (int i = 0; i < pageSize; i++) {
placeList.add(PLACE_FRAGMENT);
}
return placeList;
}
}
優點:邏輯清晰,子類只需要根據提示實現抽象方法,且提供了容器控制。
缺點:需要確定Fragment數量,不能改變數量,對現有程式碼修改比較大。
總結
- 本文從問題出發,一步步探索原始碼,得到程式碼執行的內部邏輯。理解原始碼的後,能夠清晰的情景分析,提出解決方案。
- 對於解決方案,在理解問題發生的原因後,方案有多種,這裡只是簡單的討論兩種解決方案。
- 討論分析了備忘錄模式應用在FragmentActivity和Fragment的應用。
水平有限,錯誤之處還望大家多多指正