最近看了些 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
。
接着看真正创建 tempView
的 createViewFromTag()
方法。
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
,然后,Factory2
在 Activity
中主要用于解析 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
的过程,第一步加载布局资源,生 XmlParser
和 AttributeSet
,然后根据不版本和不同标签,选择是通过 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
标签上的 id
和 visibility
属性,如果 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();
}
}
}
第一次看到这个方法时,我在想,我们可以直接通过 ViewStub
的 VISIBLE
GONE
来控制显示和消失啊,为什么还要拿到真实布局来控制呢?后面尝试之后才意识到一个问题,上面的 replaceSelfWithView()
方法已经将自己删除,所以,当我们调用 viewStub.setVisibilty(View.VISIBLE)
之后,viewStub
这个对象已经被置空,不能再次使用。这个想法没法实现,而且更尴尬的是,如果你直接调用viewStub.setVisibilty(View.INVISIBLE)
之后,viewStub
置空,但是你又没有真实 view
引用,你就不能直接让它再次展示出来了。是不是觉得这里有个坑?其实这个时候你可以使用findView查找了,所以这个坑不存在。不过这也解释了 ViewStub
为什么要用弱引用来持有真实 View
。
Factory 拓展
来填一填上文 Factory
的坑,之前说到 Activity
实现了 Factory
接口,但是什么时候,怎么把自己设置到 LayoutInflator
中的呢?我们直接到 Activity
的 attach()
方法中。
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);
...
}
在 Activity
的 attach()
方法中,会调用 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
中,首先是 mFactory2
和 mFactory
,这两个 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
。既然它已经设置过,那么就弄明白两个问题,第一,哪里设置,第二,有什么特别的用途。也不卖关子,第一个问题,在 AppcompatActivity
的 onCreate()
方法中。
@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
类。
直接贴个图,简单理解,Google 官方推出 AppCompat
组件之后,新增一些新特性。出于对我们开发者关照(让你一个个 xml 去替换估计你也不会干),所以就想出通过 LayoutInflator
中 setFactory()
这个方法直接添加自己的转换工厂,这样神不知鬼不觉的就让你的旧控件就能使用新特性(我们就可以偷懒)。所以,在 AppCompatActivity
和 AppCompaDialog
中,不用刻意去写 AppCompatXxxView
,它会自动转换。截图中最后一句有强调,我们只需要注意在自定义 View
时才需要额外设置继承 AppCompatXxxView
,到这里,Android Studio 给你警告的原因也大白。
Fragment View 创建
最后,再补全 Fragment
中 View
的创建过程。 前文分析 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)
方法,完工。