1. 程式人生 > >Android開發-從原始碼分析Fragment巢狀PagerAdapter生命週期,解決重建問題

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空指標,因為互動的介面例項為空。

分析原始碼

因為介面例項的注入在FragmentPagerAdaptergetItem中完成,我們發現問題的入口就是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問題,分析情景如下:

  1. 第一次構建FragmentPagerAdapter,FragmetManager中Fragment集合為空,需要getItem執行返回Fragment例項,UI得以顯示。
  2. 使用者離開當前Activity,onSaveInstanceState儲存操作被回撥執行,Fragment被FragmetManager控制器調起儲存操作,儲存內部狀態。
  3. 使用者回到Activity,備忘錄的資料恢復操作開始執行,就是onCreate(Bundle savedInstanceState)方法中Bundle有被儲存下來的資料,FragmetManager被恢復成之前的狀態,Fragment容器中有內容。
  4. 再次構建FragmentPagerAdapter,但是通過FragmetManager例項能夠findFragmentByTag得到Fragment例項。
  5. 整個恢復過程由原始碼配合完成,我們的FragmentPagerAdapter子類的getItem方法沒有被調起執行。
  6. 最後當所有得原始碼都執行完,執行到我們所寫的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的應用。

水平有限,錯誤之處還望大家多多指正