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) 方法,完工。

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 206,126评论 6 481
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 88,254评论 2 382
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 152,445评论 0 341
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 55,185评论 1 278
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 64,178评论 5 371
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,970评论 1 284
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,276评论 3 399
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,927评论 0 259
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 43,400评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,883评论 2 323
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,997评论 1 333
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,646评论 4 322
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,213评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,204评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,423评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,423评论 2 352
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,722评论 2 345

推荐阅读更多精彩内容