Android fragment 標籤載入過程分析

頭條祁同偉發表於2019-03-04
本文概述

在上一篇文章中我們介紹了 AsyncLayoutInflater 使用的注意事項及改進方案。

建議先回顧下之前五篇文章,這個系列的文章從前往後順序看最佳:

本篇文章我們來學習下 layout 中 fragment 標籤的載入過程,本文基於 Android 8.1.0。

1、鋪墊

各位老司機肯定對 Fragment 的使用都非常熟悉,我們簡單回顧下:Fragment 的新增方式有兩種:靜態新增和動態新增。而靜態新增就是在佈局中寫上 Fragment 的相關引用,如下:

<?xml version="1.0" encoding="utf-8"?>
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent">

    <fragment
        android:id="@+id/fragment"
        android:name="com.example.MainFragment"
        android:layout_width="match_parent"
        android:layout_height="match_parent"/>

</FrameLayout>
複製程式碼

這個 layout 檔案是相對特殊的,因為這個 fragment 標籤不是很常見,而且大家回憶下 LayoutInflater 的 inflate 流程,其中 inflate 方法的返回值是 View。而我們看下 Fragment 的定義:

public class Fragment implements ComponentCallbacks, OnCreateContextMenuListener {
}
複製程式碼

可以看到 Fragment 並不是一個 View,那說明 fragment 標籤就不是通過正常的反射來建立的,進一步說就是 fragment 標籤的建立和普通的 view 不是一個流程。

2、思考

問題:既然 fragment 標籤的建立和普通的 view 不是一個流程,那 fragment 標籤是怎麼載入的呢?

首先我們想下前提條件:fragment 標籤仍然是處於佈局檔案中的。就是說 fragment 標籤節點也會被 LayoutInflater 解析,只是被解析之後的流程和別的 view 不一樣了。一路跟蹤流程,我們來到了 LayoutInflater 的 createViewFromTag 方法:

View view;
if (mFactory2 != null) {
    view = mFactory2.onCreateView(parent, name, context, attrs);
} else if (mFactory != null) {
    view = mFactory.onCreateView(name, context, attrs);
} else {
    view = null;
}

if (view == null && mPrivateFactory != null) {
    view = mPrivateFactory.onCreateView(parent, name, context, attrs);
}

if (view == null) {
    final Object lastContext = mConstructorArgs[0];
    mConstructorArgs[0] = context;
    try {
        if (-1 == name.indexOf(`.`)) {
            view = onCreateView(parent, name, attrs);
        } else {
            view = createView(name, null, attrs);
        }
    } finally {
        mConstructorArgs[0] = lastContext;
    }
}
複製程式碼

我貼出來這段程式碼是為了總結下通過 setContentView 這種方式建立出 View 的途徑:

  1. Factory2.onCreateView;
  2. Factory.onCreateView;
  3. mPrivateFactory.onCreateView;
  4. createView;

其中1、2、4方式相信看過前面幾篇文章的小夥伴肯定都很熟悉了:

  • 1、2兩種方式本質上一樣,可以通過我們自己設定的 Factory 來建立View;
  • 4這種方式是通過反射來建立 View物件;
  • 而方式3在之前的幾篇文章中則沒有說到過,不過別急,接下來我們會介紹它;

到了這裡我們知道通過 setContentView 這種方式建立出 View 的途徑有4種,其中第4種我們直接排除掉了,也就只剩下了前三種方式。

3、探究

在我們探索究竟是這三種方式中的哪一種之前,我們先來熟悉下 mPrivateFactory。我們看下它的定義及設值的地方:


    private Factory2 mPrivateFactory;// 定義可以看出 mPrivateFactory 也實現了 Factory2 
    
    protected LayoutInflater(LayoutInflater original, Context newContext) {
        mContext = newContext;
        mFactory = original.mFactory;
        mFactory2 = original.mFactory2;
        mPrivateFactory = original.mPrivateFactory;
        setFilter(original.mFilter);
    }
    
    /**
     * @hide for use by framework
     */
    public void setPrivateFactory(Factory2 factory) {
        if (mPrivateFactory == null) {
            mPrivateFactory = factory;
        } else {
            mPrivateFactory = new FactoryMerger(factory, factory, mPrivateFactory, mPrivateFactory);
        }
    }

複製程式碼

我們就知道了 mPrivateFactory 實現了 Factory2 介面,設值方式有兩種,一種是 framework 呼叫,還有一種是建立 LayoutInflater 的時候傳入。

這三種方式有一個共同特點就是都和 Factory 相關。而使用 Factory 都會通過 LayoutInflater setFactory,既然我們沒有做事情就完成了對 fragment 標籤的解析,那有理由相信是系統處理了。使用 Fragment 的時候需要繼承 FragmentActivity 或者是 AppCompatActivity,這裡就以 FragmentActivity 為例來分析,來搜下哪裡呼叫了 setFactory 函式。

FragmentActivity繼承關係

但是在 FragmentActivity 的繼承鏈上的各個類我們並沒有搜到 setFactory 或 setFactory2。這兩個常規的設定沒有找到,我們再來找第三種方式 setPrivateFactory,最終在 Activity 搜到了,attach 方法中:

    mWindow.getLayoutInflater().setPrivateFactory(this);
複製程式碼

然後我們看下 Activity 的定義,實現了 LayoutInflater.Factory2 介面

    public class Activity extends ContextThemeWrapper
            implements LayoutInflater.Factory2,
            Window.Callback, KeyEvent.Callback,
            OnCreateContextMenuListener, ComponentCallbacks2,
            Window.OnWindowDismissedCallback, WindowControllerCallback,
            AutofillManager.AutofillClient {
            
                @Nullable
                public View onCreateView(String name, Context context, AttributeSet attrs) {
                    return null;
                }
            
                public View onCreateView(View parent, String name, Context context, AttributeSet attrs) {
                    if (!"fragment".equals(name)) {
                        return onCreateView(name, context, attrs);
                    }
            
                    return mFragments.onCreateView(parent, name, context, attrs);
                }
                
            }
複製程式碼

可以看出來在 onCreateView 方法中會判斷標籤名字如果是 fragment 的話則會呼叫 mFragments.onCreateView 來建立 View。

接下來總結下流程:

  1. 在 Activity 的 attach 方法中會為當前 Activity 的設定 mPrivateFactory;
  2. 在 LayoutInflater 的 createViewFromTag 方法中會先使用 Factory2 或 Factory 來建立view;
  3. 針對 fragment 的場景下預設獲取到的 view 是null;
  4. 如果是 null 則通過 mPrivateFactory 建立 view,這裡就會走到 Activity 的onCreateView 方法;
  5. 通過 mFragments(也就是 FragmentController)的 onCreateView 方法來建立 View;

4、mFragments.onCreateView

mFragments 其實是 FragmentController,然後細跟程式碼會走到 FragmentManager 的 onCreateView 方法:

    @Override
    public View onCreateView(View parent, String name, Context context, AttributeSet attrs) {
        if (!"fragment".equals(name)) {
            return null;
        }

        moveToState(fragment, Fragment.CREATED, 0, 0, false);

        if (fragment.mView == null) {
            throw new IllegalStateException("Fragment " + fname
                    + " did not create a view.");
        }
        return fragment.mView;
    }
複製程式碼

然後到了 moveToState 方法,注意傳入的 newState 是 Fragment.CREATED。

    void moveToState(Fragment f, int newState, int transit, int transitionStyle,
            boolean keepActive) {
         
         case Fragment.CREATED:

         if (newState > Fragment.CREATED) {
             if (DEBUG) Log.v(TAG, "moveto ACTIVITY_CREATED: " + f);
             if (!f.mFromLayout) {
                 ViewGroup container = null;
                 if (f.mContainerId != 0) {
                     if (f.mContainerId == View.NO_ID) {
                         throwException(new IllegalArgumentException(
                                 "Cannot create fragment "
                                         + f
                                         + " for a container view with no id"));
                     }
                     container = mContainer.onFindViewById(f.mContainerId);
                     if (container == null && !f.mRestored) {
                         String resName;
                         try {
                             resName = f.getResources().getResourceName(f.mContainerId);
                         } catch (NotFoundException e) {
                             resName = "unknown";
                         }
                         throwException(new IllegalArgumentException(
                                 "No view found for id 0x"
                                 + Integer.toHexString(f.mContainerId) + " ("
                                 + resName
                                 + ") for fragment " + f));
                     }
                 }
                 f.mContainer = container;
                 
                 // 重點:最關鍵的方法在這裡
                 f.mView = f.performCreateView(f.performGetLayoutInflater(
                         f.mSavedFragmentState), container, f.mSavedFragmentState);
                         
                 if (f.mView != null) {
                     f.mView.setSaveFromParentEnabled(false);
                     if (container != null) {
                         container.addView(f.mView);
                     }
                     if (f.mHidden) {
                         f.mView.setVisibility(View.GONE);
                     }
                     f.onViewCreated(f.mView, f.mSavedFragmentState);
                     dispatchOnFragmentViewCreated(f, f.mView, f.mSavedFragmentState,
                             false);
                     // Only animate the view if it is visible. This is done after
                     // dispatchOnFragmentViewCreated in case visibility is changed
                     f.mIsNewlyAdded = (f.mView.getVisibility() == View.VISIBLE)
                             && f.mContainer != null;
                 }
             }

             f.performActivityCreated(f.mSavedFragmentState);
             dispatchOnFragmentActivityCreated(f, f.mSavedFragmentState, false);
             if (f.mView != null) {
                 f.restoreViewState(f.mSavedFragmentState);
             }
             f.mSavedFragmentState = null;
         }
    }
複製程式碼

然後到了 Fragment 的 performCreateView 方法:

    View performCreateView(LayoutInflater inflater, ViewGroup container,
            Bundle savedInstanceState) {
        if (mChildFragmentManager != null) {
            mChildFragmentManager.noteStateNotSaved();
        }
        mPerformedCreateView = true;
        return onCreateView(inflater, container, savedInstanceState);
    }
複製程式碼

在最後一行我們再次看到了 onCreateView 方法,這個 onCreateView 就是 Fragment 的一個方法,我們在開發中需要覆寫的那個。Fragment 的 performCreateView() 方法的返回值是一個 View ,這個View 被返回給了 Activity 中的 onCreateView 方法;這樣就實現了遇到 fragment 標籤特殊處理並返回 view。

5、總結

本文主要學習 layout 中 fragment 標籤的建立過程,並且將思考、分析的過程也寫了下來,希望對大家閱讀原始碼、思考問題有所幫助。

我們再來回顧下 fragment 標籤的建立過程:

  1. FragmentActivity 實現了 Factory2介面,並在 attach 方法設定了mPrivateFactory;
  2. LayoutInflater 使用 factory 對 fragment 標籤預設建立出來的 view 為null;
  3. 走到了 mPrivateFactory 的 onCreateView 方法;
  4. 呼叫 Activity 的 onCreateView 方法;
  5. 呼叫 FragmentController 的 onCreateView 方法;
  6. 呼叫 FragmentManager 的 onCreateView 方法;
  7. 呼叫 moveToState 方法,其中會呼叫 Fragment 的 performGetLayoutInflater 方法;
  8. 呼叫 Fragment 的 performCreateView 方法,就建立了 fragment 標籤對應的 view;

廣告時間

今日頭條各Android客戶端團隊招人火爆進行中,各個級別和應屆實習生都需要,業務增長快、日活高、挑戰大、待遇給力,各位大佬走過路過千萬不要錯過!

本科以上學歷、對技術有熱情,歡迎加我的微信詳聊:KOBE8242011

歡迎關注

相關文章