Android 源碼分析一 View 創建

最近看了些 View 相關的源碼,相比之前,有一些新的認知。爭取通過一次整理,能系統了解 Android View 加載和顯示的相關過程,記錄下來,共勉。接下來的所有源碼基于 Android API 27 Platform

對于 View 創建,通俗說其實就兩種方式,一種是直接通過 new 關鍵詞直接創建對象,另外就是通過 xml 填充一個 View。第一種方式寫起來最簡易,但是,也有一些代價,比如說所有屬性都要一個個設置,通用 style 也沒辦法使用。第二種方式最傳統,也是接下來重點關注的方式。

構造方法參數

寫過自定義 View 都知道,我們一般需要實現三個構造方法,當然,如果你使用 Kotlin 之后,這種情況可以有一些改善,類似這樣:

class TestView @JvmOverloads constructor(context: Context, attrs: AttributeSet? = null, defStyleAttr: Int = -1)

第一個參數上下文,這個沒啥問題,第二個參數,AttributeSet 屬性集合,第三個參數,defStyleAttr 應該是默認 style 的 id。

反正至少得有這三個參數,而且,一般來說,我們第三個參數也沒怎么使用,默認使用的 -1 來占位,第二個參數一般我們也是使用 null 來默認占位。它們到底有什么用呢?可以不寫對應的構造方法嗎?

如果我們自定義 View,只有上下文那個構造方法時,通過 xml 方式填充時就會出錯:

 Caused by: android.view.InflateException: Binary XML file line #12: Error inflating class com.lovejjfg.circle.TestView
 Caused by: java.lang.NoSuchMethodException: <init> [class android.content.Context, interface android.util.AttributeSet]

簡單說就是找不到兩個參數的那個構造方法,那么這個構造方法到底在哪里被調用呢?

LayoutInflater

使用 xml 填充布局,就必須得使用 LayoutInflater ,等等,Activity 設置布局是通過 setContentView() 更新的,看看它的代碼呢。

@Override
public void setContentView(int resId) {
    ensureSubDecor();
    ViewGroup contentParent = (ViewGroup) mSubDecor.findViewById(android.R.id.content);
    contentParent.removeAllViews();
    LayoutInflater.from(mContext).inflate(resId, contentParent);
    mOriginalWindowCallback.onContentChanged();
}

LayoutInflator 創建

/**
 * Obtains the LayoutInflater from the given context.
 */
public static LayoutInflater from(Context context) {
    LayoutInflater LayoutInflater =
            (LayoutInflater) context.getSystemService(Context.LAYOUT_INFLATER_SERVICE);
    if (LayoutInflater == null) {
        throw new AssertionError("LayoutInflater not found.");
    }
    return LayoutInflater;
}

LayoutInflater 也是一個系統提供的遠程服務。

inflate() 方法

public View inflate(@LayoutRes int resource, @Nullable ViewGroup root) {
    return inflate(resource, root, root != null);
}

這個方法接收三個參數,一路點進去,首先會先通過傳入的 layoutId 構建 XmlParser :

public View inflate(@LayoutRes int resource, @Nullable ViewGroup root, boolean attachToRoot) {
    final Resources res = getContext().getResources();
    final XmlResourceParser parser = res.getLayout(resource);
    try {
        return inflate(parser, root, attachToRoot);
    } finally {
        parser.close();
    }
}

XML 解析不展開說,接下來開始真正的 inflate() 方法:

    public View inflate(XmlPullParser parser, @Nullable ViewGroup root, boolean attachToRoot) {
    synchronized (mConstructorArgs) {
        final Context inflaterContext = mContext;
        //1.AttributeSet 在這里創建出來
        final AttributeSet attrs = Xml.asAttributeSet(parser);
        Context lastContext = (Context) mConstructorArgs[0];
        mConstructorArgs[0] = inflaterContext;
        View result = root;

        try {
            final String name = parser.getName();
            //2.merge 標簽的注意事項
            if (TAG_MERGE.equals(name)) {
                if (root == null || !attachToRoot) {
                    throw new InflateException("<merge /> can be used only with a valid "
                            + "ViewGroup root and attachToRoot=true");
                }
                rInflate(parser, root, inflaterContext, attrs, false);
            } else {
                // Temp is the root view that was found in the xml
                //3.真正的創建方法
                final View temp = createViewFromTag(root, name, inflaterContext, attrs);
                ViewGroup.LayoutParams params = null;
                if (root != null) {
                    // Create layout params that match root, if supplied
                    params = root.generateLayoutParams(attrs);
                    if (!attachToRoot) {
                        // Set the layout params for temp if we are not
                        // attaching. (If we are, we use addView, below)
                        temp.setLayoutParams(params);
                    }
                }
                //4.創建子View
                // Inflate all children under temp against its context.
                rInflateChildren(parser, temp, attrs, true);
                // We are supposed to attach all the views we found (int temp)
                // to root. Do that now.
                //5.attachToRoot 參數作用
                if (root != null && attachToRoot) {
                    root.addView(temp, params);
                }
                // Decide whether to return the root that was passed in or the
                // top view found in xml.
                //5.attachToRoot 參數作用
                if (root == null || !attachToRoot) {
                    result = temp;
                }
            }

        } catch (XmlPullParserException e) {
          ...
        } finally {
          ...
        }
        return result;
    }
}

有五個注意點,已經分別在代碼中加上對應注釋,第一,View 創建的第二個參數 AttributeSet ,在這個方法中被創建出來了。第二,merge 標簽在這里首次現身,詳細放到下面「特殊標簽處理」展開講。第三, createViewFromTag() 該方法才是真正創建 tempView 的方法。第四,rInflateChildren() 方法用于填充子 View 的方法。第五,attachToRoot 參數決定是否把 temp 直接加到 rootView 上,決定是返回 rootView 還是填充出來的 tempView

接著看真正創建 tempViewcreateViewFromTag() 方法。

View createViewFromTag(View parent, String name, Context context, AttributeSet attrs,
        boolean ignoreThemeAttr) {
    ...
    //1.彩蛋
    if (name.equals(TAG_1995)) {
        // Let's party like it's 1995!
        return new BlinkLayout(context, attrs);
    }
    try {
        View view;
        //2. 各種 factory
        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 {
                //3.自定義View的差異
                if (-1 == name.indexOf('.')) {
                    view = onCreateView(parent, name, attrs);
                } else {
                    view = createView(name, null, attrs);
                }
            } finally {
                mConstructorArgs[0] = lastContext;
            }
        }

        return view;
    } catch (Exception e) {
       ...
    }
}

彩蛋分析

三個點,第一,居然看到一個彩蛋, private static final String TAG_1995 = "blink" Google 人 一下注釋,你會看到這個提交地址 戳戳戳,如果解析到這個標簽的話,會直接創建出 BlinkLayout 返回,blink 就是閃爍的意思,看注釋 // Let's party like it's 1995! ,哈哈那種一閃一閃的感覺。那么這個效果到底怎么實現的呢?直接看代碼:

    public BlinkLayout(Context context, AttributeSet attrs) {
        super(context, attrs);
        mHandler = new Handler(new Handler.Callback() {
            @Override
            public boolean handleMessage(Message msg) {
                if (msg.what == MESSAGE_BLINK) {
                    if (mBlink) {
                        mBlinkState = !mBlinkState;
                        makeBlink();
                    }
                    invalidate();
                    return true;
                }
                return false;
            }
        });
    }

    private void makeBlink() {
        Message message = mHandler.obtainMessage(MESSAGE_BLINK);
        mHandler.sendMessageDelayed(message, BLINK_DELAY);
    }
    @Override
    protected void dispatchDraw(Canvas canvas) {
        if (mBlinkState) {
            super.dispatchDraw(canvas);
        }
    }

其實很簡單,就是通過 Handler 來控制是否調用 dispatchDraw() 方法,不調用,就啥都不繪制,調用就會繪制出來,那這就是一閃一閃亮晶晶的效果咯,真是程序員的小巧思啊。

另外注意這里 Handler 的創建方式,使用的是 Callback,并不是創建一個匿名內部類,復寫 handleMessage() 方法。

LayoutInflater Factory

彩蛋說完,回歸整體,第二,出現了 factory. onCreateView() 方法。而且吧,這個factory還不止一個。那這是什么操作呢?仔細看下 public interface Factory2 extends Factory private static class FactoryMerger implements Factory2 它們是這么定義,Factory中只有一個方法:

    public View onCreateView(String name, Context context, AttributeSet attrs);

Factory2 其實重載了一個新的方法:

    public View onCreateView(View parent, String name, Context context, AttributeSet attrs);

至于 FactoryMerger 其實就是用于我們添加我們指定的 Factory 去創建對應 View

那么問題來了,為什么要整兩個 Factory 呢?

看看 Factory 的具體實現類,首先有兩個需要重點關注,一個是 Activity,一個是FragmentManager

Activity 中,看到有這兩個方法的實現:

/**
 * Standard implementation of
 * {@link android.view.LayoutInflater.Factory#onCreateView} used when
 * inflating with the LayoutInflater returned by {@link #getSystemService}.
 * This implementation does nothing and is for
 * pre-{@link android.os.Build.VERSION_CODES#HONEYCOMB} apps.  Newer apps
 * should use {@link #onCreateView(View, String, Context, AttributeSet)}.
 *
 * @see android.view.LayoutInflater#createView
 * @see android.view.Window#getLayoutInflater
 */
@Nullable
public View onCreateView(String name, Context context, AttributeSet attrs) {
    return null;
}

/**
 * Standard implementation of
 * {@link android.view.LayoutInflater.Factory2#onCreateView(View, String, Context, AttributeSet)}
 * used when inflating with the LayoutInflater returned by {@link #getSystemService}.
 * This implementation handles <fragment> tags to embed fragments inside
 * of the activity.
 *
 * @see android.view.LayoutInflater#createView
 * @see android.view.Window#getLayoutInflater
 */
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);
}

簡單理解就是,Factory 是用于低版本,高版本是 Factory2 ,然后,Factory2Activity中主要用于解析 fragment 標簽,其他它不 care(到這里,你可能有個疑問,Activity 實現了這個接口,但是是啥時候設置直接到 LayoutInflater 中的呢?這個問題也放下面單獨講)。

View 真正的創建

這么說下來,如果不是 fragment 標簽 ,那就會到剛剛的第三點,額,戰線有點兒長了,如果都已經忘記第三點就往上面翻再看下。在第三點之前,還有一個 mPrivateFactory 攔路虎,它還可以再浪一把,這個我們也先跳過,假定到這里都沒創建 View,開始第三點。

if (-1 == name.indexOf('.')) {
    view = onCreateView(parent, name, attrs);
} else {
    view = createView(name, null, attrs);
}

protected View onCreateView(String name, AttributeSet attrs)
        throws ClassNotFoundException {
    return createView(name, "android.view.", attrs);
}

如果不包含 . ,就使用 onCreateView(),這個方法其實就是給它把對應路徑補全。使用系統控件時,我們并沒有寫出全路徑,例如 TextView ,而我們自定義 View 時都是寫的全路徑,所以就直接執行 createView(name, null, attrs) 方法。

public final View createView(String name, String prefix, AttributeSet attrs)
        throws ClassNotFoundException, InflateException {
    //1.緩存中取 Constructor        
    Constructor<? extends View> constructor = sConstructorMap.get(name);
    if (constructor != null && !verifyClassLoader(constructor)) {
        constructor = null;
        sConstructorMap.remove(name);
    }
    Class<? extends View> clazz = null;
    try {
        if (constructor == null) {
            // Class not found in the cache, see if it's real, and try to add it
            //2. 加載對應的 Class
            clazz = mContext.getClassLoader().loadClass(
                    prefix != null ? (prefix + name) : name).asSubclass(View.class);
            ...
            constructor = clazz.getConstructor(mConstructorSignature);
            constructor.setAccessible(true);
            //3.加入緩存
            sConstructorMap.put(name, constructor);
        } 
        
        ...

        Object lastContext = mConstructorArgs[0];
        if (mConstructorArgs[0] == null) {
            // Fill in the context if not already within inflation.
            mConstructorArgs[0] = mContext;
        }
        //4.指定參數
        Object[] args = mConstructorArgs;
        args[1] = attrs;
        //5.反射創建
        final View view = constructor.newInstance(args);
        //6.ViewStub處理
        if (view instanceof ViewStub) {
            // Use the same context when inflating ViewStub later.
            final ViewStub viewStub = (ViewStub) view;
            viewStub.setLayoutInflater(cloneInContext((Context) args[0]));
        }
        mConstructorArgs[0] = lastContext;
        return view;

    } catch (Exception e) {
        ...
    } 
}

看到 final 時,隱約就覺得應該找到真正創建的方法。總的來說就是通過 ClassLoader 拿到字節碼,然后得到構造方法 Constructor 對象,因為反射是有額外成本消耗,所以這里有做緩存。接下來就是真正的反射創建,注意,反射創建時,使用的是兩個參數的構建方法,第一個是 Context 上下文,第二個就是第一步就創建出來的 AttributeSet ,這個老將在這里終于派上用場。這也解釋了開頭提出那個問題,如果不指定帶有 Context AttributeSet 兩個參數的構造方法,LayoutInflator 是無法創建出對應的 View,反射創建會在這里拋出上文提到那個異常。

到這里,tempView 終于創建成功。可以先簡單總結下:LayoutInflator 填充 View 的過程,第一步加載布局資源,生 XmlParserAttributeSet,然后根據不版本和不同標簽,選擇是通過 Factory 的實現類去創建(fragment標簽就是讓Activity去創建)還是自己創建。自己創建的話,就是通過反射,調用View 的兩個參數的構造方法創建。

子 View 創建

tempView 創建后,還要解析它的子 View,過程當然重復類似,我們知道在 View 創建填充完畢后中,有一個 onFinishInflate() 回調,看看它啥時候被調用。回到 inflate() 方法中的第四點,rInflateChildren(parser, temp, attrs, true)

final void rInflateChildren(XmlPullParser parser, View parent, AttributeSet attrs,
        boolean finishInflate) throws XmlPullParserException, IOException {
    rInflate(parser, parent, parent.getContext(), attrs, finishInflate);
}

這個方法最后調用 rInflate() ,接下來再看看這個方法的實現。

void rInflate(XmlPullParser parser, View parent, Context context,
        AttributeSet attrs, boolean finishInflate) throws XmlPullParserException, IOException {

    final int depth = parser.getDepth();
    int type;
    boolean pendingRequestFocus = false;

    while (((type = parser.next()) != XmlPullParser.END_TAG ||
            parser.getDepth() > depth) && type != XmlPullParser.END_DOCUMENT) {

        ...

        final String name = parser.getName();
        //1. focus 標簽
        if (TAG_REQUEST_FOCUS.equals(name)) {
            pendingRequestFocus = true;
            consumeChildElements(parser);
        //2. tag 標簽
        } else if (TAG_TAG.equals(name)) {
            parseViewTag(parser, parent, attrs);
        //3. include 標簽
        } else if (TAG_INCLUDE.equals(name)) {
            if (parser.getDepth() == 0) {
                throw new InflateException("<include /> cannot be the root element");
            }
            parseInclude(parser, context, parent, attrs);
        //4. merge 標簽
        } else if (TAG_MERGE.equals(name)) {
            throw new InflateException("<merge /> must be the root element");
        } else {
            //5. 創建 view 遞歸解析
            final View view = createViewFromTag(parent, name, context, attrs);
            final ViewGroup viewGroup = (ViewGroup) parent;
            final ViewGroup.LayoutParams params = viewGroup.generateLayoutParams(attrs);
            rInflateChildren(parser, view, attrs, true);
            viewGroup.addView(view, params);
        }
    }

    if (pendingRequestFocus) {
        parent.restoreDefaultFocus();
    }
    // 6.回調 onFinishInflate
    if (finishInflate) {
        parent.onFinishInflate();
    }
}

特殊標簽

額,先忽略那些 if 條件,直接先看 else,之前的套路創建 View 后再遞歸調用 rInflateChildren() ,不過需要注意再重新調用 rInflateChildren() 時,parent 參數已經是剛剛新創建的 view 啦。最后回調onFinishInflate() 方法。

tag requestFocus 標簽

接著,再說說前面的這些 if 語句,除了我們熟悉的 include merge 標簽檢查,這里居然還有什么 tag requestFocus 等冷門標簽, 我反正有點兒震驚,層度不低于那個彩蛋。

<tag
    android:id="@id/test1"
    android:value="testTagValue"
    app:layout_constraintBottom_toBottomOf="parent"
    app:layout_constraintStart_toStartOf="parent"
    app:layout_constraintTop_toTopOf="parent"/>

然后嘗試了下 tag 標簽,結果是 OK 的,我可以直接在父布局中使用 getTag(R.id.test1) 拿到我在 xml 中設置的 value 。 不過具體使用場景我著實沒有想到,requestFocus 也是如此。

merge 標簽

我們知道,merge 標簽用于減少層級,必須是頂級標簽,從上面代碼就可以看到對頂級標簽的檢測。減少層級的話,就又要回到 inflate() 方法中第二點。

        //2.merge 標簽的注意事項
        if (TAG_MERGE.equals(name)) {
            if (root == null || !attachToRoot) {
                throw new InflateException("<merge /> can be used only with a valid "
                        + "ViewGroup root and attachToRoot=true");
            }
            rInflate(parser, root, inflaterContext, attrs, false);
        }

如果解析到 merge 標簽,會直接調用 rInflate() 方法填充下一層級,parent 參數也不會變,所以,merge 標簽下面的內容直接就加到了 rootView 中。所以,這種情況,上一層肯定不能為空,傳入的 parent 肯定不能為空。

include 標簽

private void parseInclude(XmlPullParser parser, Context context, View parent,
        AttributeSet attrs) throws XmlPullParserException, IOException {
    int type;

    if (parent instanceof ViewGroup) {
        ...
        final XmlResourceParser childParser = context.getResources().getLayout(layout);

        try {
            final AttributeSet childAttrs = Xml.asAttributeSet(childParser);
            ...

            final String childName = childParser.getName();
            //1. merge 標簽直接填充
            if (TAG_MERGE.equals(childName)) {
                // The <merge> tag doesn't support android:theme, so
                // nothing special to do here.
                rInflate(childParser, parent, context, childAttrs, false);
            } else {
                final View view = createViewFromTag(parent, childName,
                        context, childAttrs, hasThemeOverride);
                final ViewGroup group = (ViewGroup) parent;

                final TypedArray a = context.obtainStyledAttributes(
                        attrs, R.styleable.Include);
                //2.include 標簽上的 id
                final int id = a.getResourceId(R.styleable.Include_id, View.NO_ID);
                //3.include 標簽上的 visibility
                final int visibility = a.getInt(R.styleable.Include_visibility, -1);
                a.recycle();
                ViewGroup.LayoutParams params = null;
                try {
                    params = group.generateLayoutParams(attrs);
                } catch (RuntimeException e) {
                    // Ignore, just fail over to child attrs.
                }
                if (params == null) {
                    params = group.generateLayoutParams(childAttrs);
                }
                view.setLayoutParams(params);

                // Inflate all children.
                rInflateChildren(childParser, view, childAttrs, true);
                //4.覆蓋 id
                if (id != View.NO_ID) {
                    view.setId(id);
                }
                //5.設置可見性
                switch (visibility) {
                    case 0:
                        view.setVisibility(View.VISIBLE);
                        break;
                    case 1:
                        view.setVisibility(View.INVISIBLE);
                        break;
                    case 2:
                        view.setVisibility(View.GONE);
                        break;
                }

                group.addView(view);
                
            } finally {
                childParser.close();
            }
        }
        } else {
            throw new InflateException("<include /> can only be used inside of a ViewGroup");
        }
    ...
}

parseInclude() 方法中,如果是 merge 標簽,直接再次解析,然后會取出 include 標簽上的 idvisibility 屬性,如果 include 標簽上面有 id,那么會重新設置給 View,那么之前設置的 id 就會失效,然后更新 visibility 屬性。

ViewStub 標簽

我們知道,ViewStub 標簽是用來占位,實現 View 懶加載。那么到底實現的呢?先看代碼。

     ...
        //6.ViewStub處理
        if (view instanceof ViewStub) {
            // Use the same context when inflating ViewStub later.
            final ViewStub viewStub = (ViewStub) view;
            viewStub.setLayoutInflater(cloneInContext((Context) args[0]));
        }

根據這個代碼,明顯看出 ViewStub 標簽和 include 或者 merge 不一樣,它是 View 的子類,是一個真實 View

@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
    setMeasuredDimension(0, 0);
}

@Override
public void draw(Canvas canvas) {
}

@Override
protected void dispatchDraw(Canvas canvas) {
}

ViewStub 默認寬高都是 0 ,draw() (注意是 draw() 而不是 onDraw() 方法)等方法都是空實現,真就是一個殼。接著看它的 inflate () 方法實現。

public View inflate() {
    final ViewParent viewParent = getParent();
    ...
        if (mLayoutResource != 0) {
            final ViewGroup parent = (ViewGroup) viewParent;
            //1.填充真實布局
            final View view = inflateViewNoAdd(parent);
            //2.替換自己
            replaceSelfWithView(view, parent);
            //3.創建弱引用
            mInflatedViewRef = new WeakReference<>(view);
            ...
            return view;
        } 
    ...
}

private View inflateViewNoAdd(ViewGroup parent) {
    final LayoutInflater factory;
    ...
    //1.填充真實布局
    final View view = factory.inflate(mLayoutResource, parent, false);
    if (mInflatedId != NO_ID) {
        view.setId(mInflatedId);
    }
    return view;
}

private void replaceSelfWithView(View view, ViewGroup parent) {
    final int index = parent.indexOfChild(this);
    //1.移除自己
    parent.removeViewInLayout(this);

    final ViewGroup.LayoutParams layoutParams = getLayoutParams();
    //2.添加真實布局
    if (layoutParams != null) {
        parent.addView(view, index, layoutParams);
    } else {
        parent.addView(view, index);
    }
}

看完還是那話,ViewStub 就是一個殼,先占一個坑,在調用 inflate() 之后才加載真實布局,然后替換掉自己,從而實現懶加載。說到這里,還要看一下它的 setVisibility() 方法。

@Override
@android.view.RemotableViewMethod(asyncImpl = "setVisibilityAsync")
public void setVisibility(int visibility) {
    //1.調用 inflate() 之后 mInflatedViewRef 不為空
    if (mInflatedViewRef != null) {
        View view = mInflatedViewRef.get();
        if (view != null) {
            view.setVisibility(visibility);
        } else {
            throw new IllegalStateException("setVisibility called on un-referenced view");
        }
    } else {
        super.setVisibility(visibility);
        //2.第一次設置可見時觸發 inflate()
        if (visibility == VISIBLE || visibility == INVISIBLE) {
            inflate();
        }
    }
}

第一次看到這個方法時,我在想,我們可以直接通過 ViewStubVISIBLE GONE 來控制顯示和消失啊,為什么還要拿到真實布局來控制呢?后面嘗試之后才意識到一個問題,上面的 replaceSelfWithView() 方法已經將自己刪除,所以,當我們調用 viewStub.setVisibilty(View.VISIBLE) 之后,viewStub 這個對象已經被置空,不能再次使用。這個想法沒法實現,而且更尷尬的是,如果你直接調用viewStub.setVisibilty(View.INVISIBLE) 之后,viewStub 置空,但是你又沒有真實 view 引用,你就不能直接讓它再次展示出來了。是不是覺得這里有個坑?其實這個時候你可以使用findView查找了,所以這個坑不存在。不過這也解釋了 ViewStub 為什么要用弱引用來持有真實 View

Factory 拓展

來填一填上文 Factory 的坑,之前說到 Activity 實現了 Factory 接口,但是什么時候,怎么把自己設置到 LayoutInflator 中的呢?我們直接到 Activityattach() 方法中。

final void attach(Context context, ActivityThread aThread,
        Instrumentation instr, IBinder token, int ident,
        Application application, Intent intent, ActivityInfo info,
        CharSequence title, Activity parent, String id,
        NonConfigurationInstances lastNonConfigurationInstances,
        Configuration config, String referrer, IVoiceInteractor voiceInteractor,
        Window window, ActivityConfigCallback activityConfigCallback) {
    
    ...
    mWindow.getLayoutInflater().setPrivateFactory(this);
    ...
}

Activityattach() 方法中,會調用 setPrivateFactory(this) 方法把自己設置到 Layout Inflator 中。

/**
 * @hide for use by framework
 */
public void setPrivateFactory(Factory2 factory) {
    if (mPrivateFactory == null) {
        mPrivateFactory = factory;
    } else {
        mPrivateFactory = new FactoryMerger(factory, factory, mPrivateFactory, mPrivateFactory);
    }
}

看這個代碼,它是設置的 mPrivateFactory ,這個優先級是最低的,前面介紹時第二點各種 factory 中,首先是 mFactory2mFactory ,這兩個 factory 是提供方法讓我們我們設置更改的,不過需要注意只能設置一次,所以,先打印看看 Activity 中設置情況。

    println("factory2:${LayoutInflater.from(this).factory2}")
    println("factory:${LayoutInflater.from(this).factory}")

com.lovejjfg.circle I/System.out: factory2:null
com.lovejjfg.circle I/System.out: factory:null

Activity 中,默認都沒有設置,所以你完全可以調用 setFactory() 方法設置我們指定的Factory 來解析對應 View注意:上面演示時使用的是 Activity,但我們一般不會直接繼承 Activity ,因為新的 appcompat 包中的那些新控件例 Toolbar 等等,都需要使用 AppCompatActivity 搭配上 appcompat 主題。這種情況下,再看看相關日志輸出。

com.lovejjfg.circle I/System.out: factory2:android.support.v7.app.AppCompatDelegateImplN@5307686
com.lovejjfg.circle I/System.out: factory:android.support.v7.app.AppCompatDelegateImplN@5307686

已經都設置了,而且這個變量只能設置一次,設置時會有檢查,所以在這種情況下,我們基本上沒辦法再去設置新的 Factory 。既然它已經設置過,那么就弄明白兩個問題,第一,哪里設置,第二,有什么特別的用途。也不賣關子,第一個問題,在 AppcompatActivityonCreate() 方法中。

@Override
protected void onCreate(@Nullable Bundle savedInstanceState) {
    final AppCompatDelegate delegate = getDelegate();
    delegate.installViewFactory();
    delegate.onCreate(savedInstanceState);
   ...
}

再貼一個 LayoutInflaterCompat 代碼片段,這里強調有 framework bug 修復,已經通過 反射 強制更新 Factory

/**
 * For APIs < 21, there was a framework bug that prevented a LayoutInflater's
 * Factory2 from being merged properly if set after a cloneInContext from a LayoutInflater
 * that already had a Factory2 registered. We work around that bug here. If we can't we
 * log an error.
 */
static void forceSetFactory2(LayoutInflater inflater, LayoutInflater.Factory2 factory) {
    if (!sCheckedField) {
        try {
            sLayoutInflaterFactory2Field = LayoutInflater.class.getDeclaredField("mFactory2");
            sLayoutInflaterFactory2Field.setAccessible(true);
        } catch (NoSuchFieldException e) {
            Log.e(TAG, "forceSetFactory2 Could not find field 'mFactory2' on class "
                    + LayoutInflater.class.getName()
                    + "; inflation may have unexpected results.", e);
        }
        sCheckedField = true;
    }
    if (sLayoutInflaterFactory2Field != null) {
        try {
            sLayoutInflaterFactory2Field.set(inflater, factory);
        } catch (IllegalAccessException e) {
            Log.e(TAG, "forceSetFactory2 could not set the Factory2 on LayoutInflater "
                    + inflater + "; inflation may have unexpected results.", e);
        }
    }
}

第二點,有什么用呢,前面提過,Activity 中,其實就判斷是否是 fragment 標簽,不是的話,就返回空,不操作。在 AppcompatActivity 中,createView() 會執行到 AppCompatViewInflater 中的 createView() 方法。

final View createView(View parent, final String name, @NonNull Context context,
        @NonNull AttributeSet attrs, boolean inheritContext,
        boolean readAndroidTheme, boolean readAppTheme, boolean wrapContext) {
    final Context originalContext = context;

    // We can emulate Lollipop's android:theme attribute propagating down the view hierarchy
    // by using the parent's context
    if (inheritContext && parent != null) {
        context = parent.getContext();
    }
    if (readAndroidTheme || readAppTheme) {
        // We then apply the theme on the context, if specified
        context = themifyContext(context, attrs, readAndroidTheme, readAppTheme);
    }
    if (wrapContext) {
        context = TintContextWrapper.wrap(context);
    }

    View view = null;

    // We need to 'inject' our tint aware Views in place of the standard framework versions
    switch (name) {
        case "TextView":
            view = createTextView(context, attrs);
            verifyNotNull(view, name);
            break;
        ...
        default:
            // The fallback that allows extending class to take over view inflation
            // for other tags. Note that we don't check that the result is not-null.
            // That allows the custom inflater path to fall back on the default one
            // later in this method.
            view = createView(context, name, attrs);
    }

    ...

    return view;
}

@NonNull
protected AppCompatTextView createTextView(Context context, AttributeSet attrs) {
    return new AppCompatTextView(context, attrs);
}

可以看到,這里把 TextView 標簽本來應該創建的 TextView 換成了 AppCompatTextView 類。

appcompat.png

直接貼個圖,簡單理解,Google 官方推出 AppCompat 組件之后,新增一些新特性。出于對我們開發者關照(讓你一個個 xml 去替換估計你也不會干),所以就想出通過 LayoutInflatorsetFactory() 這個方法直接添加自己的轉換工廠,這樣神不知鬼不覺的就讓你的舊控件就能使用新特性(我們就可以偷懶)。所以,在 AppCompatActivityAppCompaDialog 中,不用刻意去寫 AppCompatXxxView ,它會自動轉換。截圖中最后一句有強調,我們只需要注意在自定義 View時才需要額外設置繼承 AppCompatXxxView ,到這里,Android Studio 給你警告的原因也大白。

Fragment View 創建

最后,再補全 FragmentView 的創建過程。 前文分析 Activity 中只解析 fragment 標簽。最后會調用到 FragmentManager 中的 onCreateView() 方法。

@Override
public View onCreateView(View parent, String name, Context context, AttributeSet attrs) {
    if (!"fragment".equals(name)) {
        return null;
    }
    ...
    //創建 Fragment
    if (fragment == null) {
        fragment = mContainer.instantiate(context, fname, null);
        ...
        fragment.onInflate(mHost.getContext(), attrs, fragment.mSavedFragmentState);
        addFragment(fragment, true);

    }
  ...
    if (mCurState < Fragment.CREATED && fragment.mFromLayout) {
        moveToState(fragment, Fragment.CREATED, 0, 0, false);
    } else {
        moveToState(fragment);
    }
   ...
}

Fragment 創建不展開說了,用了反射,以后篇章有空再細聊。下面調用 moveToState() 方法,state 設置的是 Fragment.CREATED

//FragmentManager
void moveToState(Fragment f, int newState, int transit, int transitionStyle,
        boolean keepActive) {
    ...
     f.mView = f.performCreateView(f.performGetLayoutInflater(
                                f.mSavedFragmentState), container, f.mSavedFragmentState);
    ....
  }

//Fragment
View performCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container,
        @Nullable Bundle savedInstanceState) {
    if (mChildFragmentManager != null) {
        mChildFragmentManager.noteStateNotSaved();
    }
    mPerformedCreateView = true;
    return onCreateView(inflater, container, savedInstanceState);
}

到這里,就回調我們熟悉的 onCreateView(inflater, container, savedInstanceState) 方法,完工。

?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。

推薦閱讀更多精彩內容