View,2018android 面试题
我们可以看到最基础的就是 AppCompatDelegateImplV9 这个版本,其他的实现类最终都是继承自这个 AppCompatDelegateImplV9 类的。我们后面要查看的方法都在 AppCompatDelegateImplV9 这个类实现里。
所以我们在 AppCompatActivity 中调用 setContentView()
方法,实际最终实现都是 AppCompatDelegateImplV9 里。
3.2.2 AppCompatDelegateImplV9.setContentView() 方法。
// 代理类的具体实现类 AppCompatDelegateImplV9 中 setContentView() 方法 @Overridepublic void setContentView(View v, ViewGroup.LayoutParams lp) {ensureSubDecor();ViewGroup contentParent = (ViewGroup) mSubDecor.findViewById(android.R.id.content);contentParent.removeAllViews();contentParent.addView(v, lp);mOriginalWindowCallback.onContentChanged();}
从代码第 5 - 7 行,从 mSubDector
(类型 ViewGroup )中取出个 android.R.id.content
标识的 contentParent
,然后重新添加 view
。第 8 行回调通知。
那第 4 行代码从名字上可以看出是确保这个 mSubDector
初始化的方法。我们进去看下:
private void ensureSubDecor() {if (!mSubDecorInstalled) {mSubDecor = createSubDecor();
//...省略...}}
private ViewGroup createSubDecor() {//...省略... 这部分主要针对 AppCompat 样式检查和适配
// Now let's make sure that the Window has installed its decor by retrieving itmWindow.getDecorView();
final LayoutInflater inflater = LayoutInflater.from(mContext);ViewGroup subDecor = null;
//...省略... 这部分主要针对不同的样式设置来初始化不同的 subDecor(inflater 不同的布局 xml )
if (subDecor == null) {throw new IllegalArgumentException("AppCompat does not support the current theme features: { "
"windowActionBar: " + mHasActionBar
", windowActionBarOverlay: "+ mOverlayActionBar
", android:windowIsFloating: " + mIsFloating
", windowActionModeOverlay: " + mOverlayActionMode
", windowNoTitle: " + mWindowNoTitle
" }");}
//...省略...
// Make the decor optionally fit system windows, like the window's decorViewUtils.makeOptionalFitsSystemWindows(subDecor);
final ContentFrameLayout contentView = (ContentFrameLayout) subDecor.findViewById(R.id.action_bar_activity_content);
final ViewGroup windowContentView = (ViewGroup) mWindow.findViewById(android.R.id.content);if (windowContentView != null) {// There might be Views already added to the Window's content view so we need to// migrate them to our content viewwhile (windowContentView.getChildCount() > 0) {final View child = windowContentView.getChildAt(0);windowContentView.removeViewAt(0);contentView.addView(child);}
// Change our content FrameLayout to use the android.R.id.content id.// Useful for fragments.windowContentView.setId(View.NO_ID);contentView.setId(android.R.id.content);
// The decorContent may have a foreground drawable set (windowContentOverlay).// Remove this as we handle it ourselvesif (windowContentView instanceof FrameLayout) {((FrameLayout) windowContentView).setForeground(null);}}
// Now set the Window's content view with the decormWindow.setContentView(subDecor);
//...省略...
return subDecor;}
下面我们重点看一下代码 28 - 31 行,从 subDecor
中取出了 R.id.action_bar_activity_content
标示的 FrameLayout ,从 window
中取出我们熟悉的 android.R.id.content
标示 view
。这个 view
呢其实就是 PhoneWindow 中 DecorView 里的 contentView
了。
代码 35 - 38 行,就是将 window
里取出的 windowContentView
里已有的 childview 依次挪到这个 subDector
取出的 contentView
中去,并清空这个 windowContentView
。这里就达到狸猫换太子的第一步。
代码 43 - 44 行,接下来将原来 window
里的 windowContentView
的 id( android.R.id.content )
替换给我们 subDecor
里的 contentView
。
代码 54 行,狸猫换太子的最后一步,将狸猫 subDecor
设置给 mWindow
。
分析完上述代码,我们再回过来看一下 setContentView() 方法的代码第 4 行,就不难理解为什么可以通过 android.R.id.content
来取到 “根 View ” 了。
@Overridepublic void setContentView(View v, ViewGroup.LayoutParams lp) {ensureSubDecor();ViewGroup contentParent = (ViewGroup) mSubDecor.findViewById(android.R.id.content);contentParent.removeAllViews();contentParent.addView(v, lp);mOriginalWindowCallback.onContentChanged();}
四、如何从 XML 里读取并构建一个 View?
刚才我们讨论了一类参数为 View 的 setContentView() 方法,现在我们来看下另一个参数为布局 id 的 setContentView() 方法。
4.1 LayoutInflater.inflate() 方法
当我们在 Activity 的 onCreate() 方法里调用 setContentView(R.layout.xxx) 来设置一个页面时,最终都会走到类似如下的方法:
LayoutInflater.from(mContext).inflate(resId, contentParent);
所以下面我们来看下怎么 inflate 一个页面出来。
// LayoutInflater 代码 public View inflate(@LayoutRes int resource, @Nullable ViewGroup root) {return inflate(resource, root, root != null);}
public View inflate(@LayoutRes int resource, @Nullable ViewGroup root, boolean attachToRoot) {final Resources res = getContext().getResources();if (DEBUG) {Log.d(TAG, "INFLATING from resource: "" + res.getResourceName(resource) + "" ("
Integer.toHexString(resource) + ")");}
final XmlResourceParser parser = res.getLayout(resource);try {return inflate(parser, root, attachToRoot);} finally {parser.close();}}
看代码第 13 行,通过 XML 解析器 XmlResourceParser 来解析我们传进来的布局文件的。下面我们贴下第 14 行代码方法的详细。
public View inflate(XmlPullParser parser, @Nullable ViewGroup root, boolean attachToRoot) {synchronized (mConstructorArgs) {Trace.traceBegin(Trace.TRACE_TAG_VIEW, "inflate");
final Context inflaterContext = mContext;final AttributeSet attrs = Xml.asAttributeSet(parser);Context lastContext = (Context) mConstructorArgs[0];mConstructorArgs[0] = inflaterContext;View result = root;
try {// Look for the root node.int type;while ((type = parser.next()) != XmlPullParser.START_TAG &&type != XmlPullParser.END_DOCUMENT) {// Empty}
if (type != XmlPullParser.START_TAG) {throw new InflateException(parser.getPositionDescription()
": No start tag found!");}
final String name = parser.getName();
if (DEBUG) {System.out.println("**************************");System.out.println("Creating root view: "
name);System.out.println("**************************");}
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 xmlfinal View temp = createViewFromTag(root, name, inflaterContext, attrs);
ViewGroup.LayoutParams params = null;
if (root != null) {if (DEBUG) {System.out.println("Creating params from root: " +root);}// Create layout params that match root, if suppliedparams = 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);}}
if (DEBUG) {System.out.println("-----> start inflating children");}
// Inflate all children under temp against its context.rInflateChildren(parser, temp, attrs, true);
if (DEBUG) {System.out.println("-----> done inflating children");}
// We are supposed to attach all the views we found (int temp)// to root. Do that now.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.if (root == null || !attachToRoot) {result = temp;}}
} catch (XmlPullParserException e) {final InflateException ie = new InflateException(e.getMessage(), e);ie.setStackTrace(EMPTY_STACK_TRACE);throw ie;} catch (Exception e) {final InflateException ie = new InflateException(parser.getPositionDescription()
": " + e.getMessage(), e);ie.setStackTrace(EMPTY_STACK_TRACE);throw ie;} finally {// Don't retain static reference on context.mConstructorArgs[0] = lastContext;mConstructorArgs[1] = null;
Trace.traceEnd(Trace.TRACE_TAG_VIEW);}
return result;}}
可以看到上面的代码不是特别多,主要就是根据一个个 XML 中的标签( </> 封装的内容),用 parser
来解析并做相应处理。
代码第 74 行将 view 添加到 root
中去。而这个 root
就是一开始传下来的 contentParent
(类型 ViewGroup )。
那就有疑问了,读取到标签,知道是什么标签了,比如是个 TextView ,那在什么地方创建一个 View 呢?
代码第 41 - 42 行,调用 createViewFromTag()
方法来创建 View 的。
// Temp is the root view that was found in the xml
final View temp = createViewFromTag(root, name, inflaterContext, attrs);
4.2 createViewFromTag() 方法
我们简化掉一部分代码。
// LayoutInflater 代码 View createViewFromTag(View parent, String name, Context context, AttributeSet attrs, boolean ignoreThemeAttr) {//...省略...try {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;}}
return view;} catch//...省略捕获异常...}
其中 Factory
、 Factory2
都是接口,都提供了 onCreateView()
方法,其中 Factory2
继承自 Factory
,扩展了个字段。
public interface Factory {/**
Hook you can supply that is called when inflating from a LayoutInflater.
You can use this to customize the tag names available in your XML
layout files.
<p>
Note that it is good practice to prefix these custom names with your
package (i.e., com.coolcompany.apps) to avoid conflicts with system
names.
@param name Tag name to be inflated.
@param context The context the view is being created in.
@param attrs Inflation attributes as specified in XML file.
@return View Newly created view. Return null for the default
*/public View onCreateView(String name, Context context, AttributeSet attrs);}
public interface Factory2 extends Factory {/**
Version of {@link #onCreateView(String, Context, AttributeSet)}
that also supplies the parent that the view created view will be
placed in.
@param parent The parent that the created view will be placed
in; <em>note that this may be null</em>.
@param name Tag name to be inflated.
@param context The context the view is being created in.
@param attrs Inflation attributes as specified in XML file.
@return View Newly created view. Return null for the default
*/public View onCreateView(View parent, String name, Context context, AttributeSet attrs);}
如果所有 factory 都为空或者 factory 构建的 view 为空,则最终调用 CreareView()
方法了,关于此方法代码就不贴了,就是通过控件名字( XML 中标签名)反射生成个对象,贴一段注释就明白了。
Low-level function for instantiating a view by name. This attempts to instantiate a view class of the given name found in this LayoutInflater's ClassLoader.
最后的疑问就是这个 Factory(或 Factory2 )接口类型的成员变量什么时候会赋值了?请往下看。
4.3 Activity 中 Factory 赋值
我们先看看 Activity 是实现了 LayoutInflater.Factory2 接口的。
public class Activity extends ContextThemeWrapperimplements LayoutInflater.Factory2,Window.Callback, KeyEvent.Callback,OnCreateContextMenuListener, ComponentCallbacks2,Window.OnWindowDismissedCallback, WindowControllerCallback {//...省略
/**
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*/@Nullablepublic 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);}
}
这里我们有了一个额外的收获,就是这个 “fragment”
。如果我们的 XML 中用 fragment 标签来嵌入一个 Fragment ,在解析 XML 时候,会在 Activity 中调用 mFragments
的 onCreateView()
方法来返回一个 View ,最后加入到 contentParent
中。
4.3.1 Activity 与 LayoutInflater 关联
// Activity 代码 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) {//...省略
mWindow = new PhoneWindow(this, window);mWindow.setWindowControllerCallback(this);mWindow.setCallback(this);mWindow.setOnWindowDismissedCallback(this);mWindow.getLayoutInflater().setPrivateFactory(this);
还是这个 attach()
方法( Internal API ),在代码第 15 行调用了 PhoneWindow 的 getLayoutInflater()
方法,设置了 privateFactory
。
public PhoneWindow(Context context) {super(context);mLayoutInflater = LayoutInflater.from(context);}
/**
Return a LayoutInflater instance that can be used to inflate XML view layout
resources for use in this Window.
@return LayoutInflater The shared LayoutInflater.*/@Overridepublic LayoutInflater getLayoutInflater() {return mLayoutInflater;}
代码已经说明了一切,注释也很清楚了。
4.4 AppCompatActivity 中 Factory 赋值
请往下看
五、AppCompatActivity
我们之前的内容都是一些准备知识,我们最初的问题是 ImageView 里 getContext() 的类型为什么在 5.0 以下会是 TintContextWrapper ?什么时候以及是替换掉的?还没有解答,下面会陆续给出答案。小伙伴们坚持下!
5.1 AppCompatActivity.onCreate() 方法分析
@Overrideprotected void onCreate(@Nullable Bundle savedInstanceState) {final AppCompatDelegate delegate = getDelegate();delegate.installViewFactory();delegate.onCreate(savedInstanceState);if (delegate.applyDayNight() && mThemeId != 0) {// If DayNight has been applied, we need to re-apply the theme for// the changes to take effect. On API 23+, we should bypass// setTheme(), which will no-op if the theme ID is identical to the// current theme ID.if (Build.VERSION.SDK_INT >= 23) {onApplyThemeResource(getTheme(), mThemeId, false);} else {setTheme(mThemeId);}}super.onCreate(savedInstanceState);}
怎么样第 3 行代码是不是很熟悉,代理加兼容模式,这个 AppCompatDelegate 具体实现类我们再看一遍。
// AppCompatActivity 代码,代码 8 行的 this 就是这个 Activity 本身。/**
@return The {@link AppCompatDelegate} being used by this Activity.*/@NonNullpublic AppCompatDelegate getDelegate() {if (mDelegate == null) {mDelegate = AppCompatDelegate.create(this, this);}return mDelegate;}
// AppCompatDelegate 代码 private static AppCompatDelegate create(Context context, Window window,AppCompatCallback callback) {final int sdk = Build.VERSION.SDK_INT;if (BuildCompat.isAtLeastN()) {return new AppCompatDelegateImplN(context, window, callback);} else if (sdk >= 23) {return new AppCompatDelegateImplV23(context, window, callback);} else if (sdk >= 14) {return new AppCompatDelegateImplV14(context, window, callback);} else if (sdk >= 11) {return new AppCompatDelegateImplV11(context, window, callback);} else {return new AppCompatDelegateImplV9(context, window, callback);}}
AppCompatActivity.onCreate() 代码里,第 4 行 delegate.installViewFactory()
。具体的实现是在 AppCompatDelegateImplV9 里。看如下代码:
@Overridepublic void installViewFactory() {LayoutInflater layoutInflater = LayoutInflater.from(mContext);if (layoutInflater.getFactory() == null) {LayoutInflaterCompat.setFactory(layoutInflater, this);} else {if (!(LayoutInflaterCompat.getFactory(layoutInflater)instanceof AppCompatDelegateImplV9)) {Log.i(TAG, "The Activity's LayoutInflater already has a Factory installed"
" so we can not install AppCompat's");}}}
代码第 3 - 5 行,如果 layoutInflater
的factory
为空,则将自身设置给layoutInflater
,达到设置 factory 的效果( 4.3 章节问题解决),也达到了自定义 contentView 的效果。
对比下之前的 setContentView(View view) 代码,有区别就是在下面的第 6 行。
@Overridepublic void setContentView(int resId) {ensureSubDecor();ViewGroup contentParent = (ViewGroup) mSubDecor.findViewById(android.R.id.content);contentParent.removeAllViews();LayoutInflater.from(mContext).inflate(resId, contentParent);mOriginalWindowCallback.onContentChanged();}
还不明白 AppCompatActivity 如何自定义 contentView 的小伙伴,可以回去看看第四章,看看 4.2 createViewFromTag() 方法 章节。对 contentParent
有疑问的看看第三章。
联系下我们最初的问题,在这里传给 LayoutInflater 的 mContext
已经替换 TintContextWrapper 了么?当然不是,从 AppCompatActivity.onCreate() 方法里一路传下来的 context
都是 AppCompatActivity 自身。我们还得往下看。
5.2 AppCompatDelegateImplV9.onCreateView() 方法分析
从 5.1 的代码我们已经可以看到在 AppCompatActivity 中通过 AppCompatDelegateImplV9 将自己与 LayoutInflater 的 setFactory 系列方法关联。具体实现 Factory 接口方法也自然在 AppCompatDelegateImplV9 中了。
这里我们先将 support-v4 包里 LayoutInflaterFactory 接口等同与 LayoutInflater 的 Factory2 接口,具体如何等效我们后面第 6 章节会讲述。
class AppCompatDelegateImplV9 extends AppCompatDelegateImplBaseimplements MenuBuilder.Callback, LayoutInflaterFactory {
//...省略...
/**
From {@link android.support.v4.view.LayoutInflaterFactory}*/@Overridepublic final View onCreateView(View parent, String name, Context context, AttributeSet attrs) {// First let the Activity's Factory try and inflate the viewfinal View view = callActivityOnCreateView(parent, name, context, attrs);if (view != null) {return view;}
// If the Factory didn't handle it, let our createView() method tryreturn createView(parent, name, context, attrs);}
//...省略...
@Overridepublic View createView(View parent, final String name, @NonNull Context context,@NonNull AttributeSet attrs) {if (mAppCompatViewInflater == null) {mAppCompatViewInflater = new AppCompatViewInflater();}
//...省略...
return mAppCompatViewInflater.createView(parent, name, context, attrs, inheritContext,IS_PRE_LOLLIPOP, /* Only read android:theme pre-L (L+ handles this anyway) /true, / Read read app:theme as a fallback at all times for legacy reasons /VectorEnabledTintResources.shouldBeUsed() / Only tint wrap the context if enabled */);}
//...省略...}
从上面的代码可以看到,LayoutInflate
里 Factory2 接口 onCreateView()
方法的实现,是在 AppCompatDelegateImplV9 ( AppCompatActivity 中代理实现类)中并且使用的是 AppCompatViewInflater
。忘记了可以回去看看第四章。
我们再进去看看这个 AppCompatViewInflater 的 createView()
是做了什么事情。
5.3 AppCompatViewInflater
“duang duang duang”!
public 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 contextif (inheritContext && parent != null) {context = parent.getContext();}if (readAndroidTheme || readAppTheme) {// We then apply the theme on the context, if specifiedcontext = 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 versionsswitch (name) {case "TextView":view = new AppCompatTextView(context, attrs);break;case "ImageView":view = new AppCompatImageView(context, attrs);break;case "Button":view = new AppCompatButton(context, attrs);break;case "EditText":view = new AppCompatEditText(context, attrs);break;case "Spinner":view = new AppCompatSpinner(context, attrs);break;case "ImageButton":view = new AppCompatImageButton(context, attrs);break;case "CheckBox":view = new AppCompatCheckBox(context, attrs);break;case "RadioButton":view = new AppCompatRadioButton(context, attrs);break;case "CheckedTextView":view = new AppCompatCheckedTextView(context, attrs);break;case "AutoCompleteTextView":view = new AppCompatAutoCompleteTextView(context, attrs);break;case "MultiAutoCompleteTextView":view = new AppCompatMultiAutoCompleteTextView(context, attrs);break;case "RatingBar":view = new AppCompatRatingBar(context, attrs);break;case "SeekBar":view = new AppCompatSeekBar(context, attrs);break;}
if (view == null && originalContext != context) {// If the original context does not equal our themed context, then we need to manually// inflate it using the name so that android:theme takes effect.view = createViewFromTag(context, name, attrs);}
if (view != null) {// If we have created a view, check it's android:onClickcheckOnClickListener(view, attrs);}
return view;}
代码 15 - 17 行,如果 wrapContext
为 true ,将 context
用 TintContextWrapper
包了一次。我们终于第一次看到这个 TintContextWrapper
了!!!下面我们再详细看。
代码 23 - 61 行,将一些常见的基础 View 转变为 AppCompatXXX 了。终于知道在 AppCompatActivity 中哪些基础控件会被替换了,具体参见上面的 case 。
代码 23 - 61 行,将一些常见的基础 View 转变为 AppCompatXXX 了。终于知道在 AppCompatActivity 中哪些基础控件会被替换了,具体参见上面的 case 。
这里我们只看下 AppCompatImageView 的构造函数(其他类似),也将 context
用 TintContextWrapper
包下。
public AppCompatImageView(Context context, AttributeSet attrs) {this(context, attrs, 0);}
public AppCompatImageView(Context context, AttributeSet attrs, int defStyleAttr) {super(TintContextWrapper.wrap(context), attrs, defStyleAttr);//...省略...}
5.4 TintContextWrapper
代码直接告诉我们 SDK 版本低于 21 ( android 5.0 ),将 Context 包装成 TintContextWrapper 类型。 这就是为什么 XML 中的 ImageView 获取到的 Context 可能是 TintContextWrapper 类型了。
public static Context wrap(@NonNull final Context context) {if (shouldWrap(context)) {synchronized (CACHE_LOCK) {//...省略...
// If we reach here then the cache didn't have a hit, so create a new instance// and add it to the cachefinal TintContextWrapper wrapper = new TintContextWrapper(context);
//...省略...
return wrapper;}}return context;}
private static boolean shouldWrap(@NonNull final Context context) {if (context instanceof TintContextWrapper|| context.getResources() instanceof TintResources|| context.getResources() instanceof VectorEnabledTintResources) {// If the Context already has a TintResources[Experimental] impl, no need to wrap again// If the Context is already a TintContextWrapper, no need to wrap againreturn false;}return Build.VERSION.SDK_INT < 21 || VectorEnabledTintResources.shouldBeUsed();}
5.5 VectorEnabledTintResources.shouldBeUsed()
无论是在 5.2 章节里 mAppCompatViewInflater.createView()
方法里还是 TintContextWrapper.shouldWrap()
方法里都有这句 VectorEnabledTintResources.shouldBeUsed() 。我们继续看下代码:
@RestrictTo(LIBRARY_GROUP)public class VectorEnabledTintResources extends Resources {
public static boolean shouldBeUsed() {return AppCompatDelegate.isCompatVectorFromResourcesEnabled()&& Build.VERSION.SDK_INT <= MAX_SDK_WHERE_REQUIRED;}
/**
评论