最近看了些 View
相關(guān)的源碼,相比之前,有一些新的認(rèn)知。爭取通過一次整理,能系統(tǒng)了解 Android View
加載和顯示的相關(guān)過程,記錄下來,共勉。接下來的所有源碼基于 Android API 27 Platform
。
對于 View
創(chuàng)建,通俗說其實(shí)就兩種方式,一種是直接通過 new
關(guān)鍵詞直接創(chuàng)建對象,另外就是通過 xml
填充一個(gè) View
。第一種方式寫起來最簡易,但是,也有一些代價(jià),比如說所有屬性都要一個(gè)個(gè)設(shè)置,通用 style 也沒辦法使用。第二種方式最傳統(tǒng),也是接下來重點(diǎn)關(guān)注的方式。
構(gòu)造方法參數(shù)
寫過自定義 View
都知道,我們一般需要實(shí)現(xiàn)三個(gè)構(gòu)造方法,當(dāng)然,如果你使用 Kotlin
之后,這種情況可以有一些改善,類似這樣:
class TestView @JvmOverloads constructor(context: Context, attrs: AttributeSet? = null, defStyleAttr: Int = -1)
第一個(gè)參數(shù)上下文,這個(gè)沒啥問題,第二個(gè)參數(shù),AttributeSet
屬性集合,第三個(gè)參數(shù),defStyleAttr
應(yīng)該是默認(rèn) style 的 id。
反正至少得有這三個(gè)參數(shù),而且,一般來說,我們第三個(gè)參數(shù)也沒怎么使用,默認(rèn)使用的 -1 來占位,第二個(gè)參數(shù)一般我們也是使用 null 來默認(rèn)占位。它們到底有什么用呢?可以不寫對應(yīng)的構(gòu)造方法嗎?
如果我們自定義 View
,只有上下文那個(gè)構(gòu)造方法時(shí),通過 xml 方式填充時(shí)就會(huì)出錯(cuò):
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]
簡單說就是找不到兩個(gè)參數(shù)的那個(gè)構(gòu)造方法,那么這個(gè)構(gòu)造方法到底在哪里被調(diào)用呢?
LayoutInflater
使用 xml 填充布局,就必須得使用 LayoutInflater
,等等,Activity
設(shè)置布局是通過 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 創(chuàng)建
/**
* 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
也是一個(gè)系統(tǒng)提供的遠(yuǎn)程服務(wù)。
inflate() 方法
public View inflate(@LayoutRes int resource, @Nullable ViewGroup root) {
return inflate(resource, root, root != null);
}
這個(gè)方法接收三個(gè)參數(shù),一路點(diǎn)進(jìn)去,首先會(huì)先通過傳入的 layoutId 構(gòu)建 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 在這里創(chuàng)建出來
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 標(biāo)簽的注意事項(xiàng)
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.真正的創(chuàng)建方法
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.創(chuàng)建子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 參數(shù)作用
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 參數(shù)作用
if (root == null || !attachToRoot) {
result = temp;
}
}
} catch (XmlPullParserException e) {
...
} finally {
...
}
return result;
}
}
有五個(gè)注意點(diǎn),已經(jīng)分別在代碼中加上對應(yīng)注釋,第一,View
創(chuàng)建的第二個(gè)參數(shù) AttributeSet
,在這個(gè)方法中被創(chuàng)建出來了。第二,merge
標(biāo)簽在這里首次現(xiàn)身,詳細(xì)放到下面「特殊標(biāo)簽處理」展開講。第三, createViewFromTag()
該方法才是真正創(chuàng)建 tempView
的方法。第四,rInflateChildren()
方法用于填充子 View
的方法。第五,attachToRoot
參數(shù)決定是否把 temp 直接加到 rootView
上,決定是返回 rootView
還是填充出來的 tempView
。
接著看真正創(chuàng)建 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) {
...
}
}
彩蛋分析
三個(gè)點(diǎn),第一,居然看到一個(gè)彩蛋, private static final String TAG_1995 = "blink"
Google 人 一下注釋,你會(huì)看到這個(gè)提交地址 戳戳戳,如果解析到這個(gè)標(biāo)簽的話,會(huì)直接創(chuàng)建出 BlinkLayout
返回,blink
就是閃爍的意思,看注釋 // Let's party like it's 1995!
,哈哈那種一閃一閃的感覺。那么這個(gè)效果到底怎么實(shí)現(xiàn)的呢?直接看代碼:
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);
}
}
其實(shí)很簡單,就是通過 Handler
來控制是否調(diào)用 dispatchDraw()
方法,不調(diào)用,就啥都不繪制,調(diào)用就會(huì)繪制出來,那這就是一閃一閃亮晶晶的效果咯,真是程序員的小巧思啊。
另外注意這里 Handler
的創(chuàng)建方式,使用的是 Callback
,并不是創(chuàng)建一個(gè)匿名內(nèi)部類,復(fù)寫 handleMessage()
方法。
LayoutInflater Factory
彩蛋說完,回歸整體,第二,出現(xiàn)了 factory. onCreateView()
方法。而且吧,這個(gè)factory還不止一個(gè)。那這是什么操作呢?仔細(xì)看下 public interface Factory2 extends Factory
private static class FactoryMerger implements Factory2
它們是這么定義,Factory
中只有一個(gè)方法:
public View onCreateView(String name, Context context, AttributeSet attrs);
Factory2
其實(shí)重載了一個(gè)新的方法:
public View onCreateView(View parent, String name, Context context, AttributeSet attrs);
至于 FactoryMerger
其實(shí)就是用于我們添加我們指定的 Factory
去創(chuàng)建對應(yīng) View
。
那么問題來了,為什么要整兩個(gè) Factory
呢?
看看 Factory
的具體實(shí)現(xiàn)類,首先有兩個(gè)需要重點(diǎn)關(guān)注,一個(gè)是 Activity
,一個(gè)是FragmentManager
。
在 Activity
中,看到有這兩個(gè)方法的實(shí)現(xiàn):
/**
* 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
標(biāo)簽,其他它不 care(到這里,你可能有個(gè)疑問,Activity 實(shí)現(xiàn)了這個(gè)接口,但是是啥時(shí)候設(shè)置直接到 LayoutInflater 中的呢?這個(gè)問題也放下面單獨(dú)講)。
View 真正的創(chuàng)建
這么說下來,如果不是 fragment
標(biāo)簽 ,那就會(huì)到剛剛的第三點(diǎn),額,戰(zhàn)線有點(diǎn)兒長了,如果都已經(jīng)忘記第三點(diǎn)就往上面翻再看下。在第三點(diǎn)之前,還有一個(gè) mPrivateFactory
攔路虎,它還可以再浪一把,這個(gè)我們也先跳過,假定到這里都沒創(chuàng)建 View
,開始第三點(diǎn)。
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()
,這個(gè)方法其實(shí)就是給它把對應(yīng)路徑補(bǔ)全。使用系統(tǒng)控件時(shí),我們并沒有寫出全路徑,例如 TextView
,而我們自定義 View
時(shí)都是寫的全路徑,所以就直接執(zhí)行 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. 加載對應(yīng)的 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.指定參數(shù)
Object[] args = mConstructorArgs;
args[1] = attrs;
//5.反射創(chuàng)建
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
時(shí),隱約就覺得應(yīng)該找到真正創(chuàng)建的方法。總的來說就是通過 ClassLoader
拿到字節(jié)碼,然后得到構(gòu)造方法 Constructor
對象,因?yàn)榉瓷涫怯蓄~外成本消耗,所以這里有做緩存。接下來就是真正的反射創(chuàng)建,注意,反射創(chuàng)建時(shí),使用的是兩個(gè)參數(shù)的構(gòu)建方法,第一個(gè)是 Context
上下文,第二個(gè)就是第一步就創(chuàng)建出來的 AttributeSet
,這個(gè)老將在這里終于派上用場。這也解釋了開頭提出那個(gè)問題,如果不指定帶有 Context
AttributeSet
兩個(gè)參數(shù)的構(gòu)造方法,LayoutInflator
是無法創(chuàng)建出對應(yīng)的 View
,反射創(chuàng)建會(huì)在這里拋出上文提到那個(gè)異常。
到這里,tempView
終于創(chuàng)建成功。可以先簡單總結(jié)下:LayoutInflator
填充 View
的過程,第一步加載布局資源,生 XmlParser
和 AttributeSet
,然后根據(jù)不版本和不同標(biāo)簽,選擇是通過 Factory
的實(shí)現(xiàn)類去創(chuàng)建(fragment標(biāo)簽就是讓Activity去創(chuàng)建)還是自己創(chuàng)建。自己創(chuàng)建的話,就是通過反射,調(diào)用View
的兩個(gè)參數(shù)的構(gòu)造方法創(chuàng)建。
子 View 創(chuàng)建
tempView
創(chuàng)建后,還要解析它的子 View
,過程當(dāng)然重復(fù)類似,我們知道在 View
創(chuàng)建填充完畢后中,有一個(gè) onFinishInflate()
回調(diào),看看它啥時(shí)候被調(diào)用。回到 inflate()
方法中的第四點(diǎn),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);
}
這個(gè)方法最后調(diào)用 rInflate()
,接下來再看看這個(gè)方法的實(shí)現(xiàn)。
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 標(biāo)簽
if (TAG_REQUEST_FOCUS.equals(name)) {
pendingRequestFocus = true;
consumeChildElements(parser);
//2. tag 標(biāo)簽
} else if (TAG_TAG.equals(name)) {
parseViewTag(parser, parent, attrs);
//3. include 標(biāo)簽
} 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 標(biāo)簽
} else if (TAG_MERGE.equals(name)) {
throw new InflateException("<merge /> must be the root element");
} else {
//5. 創(chuàng)建 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.回調(diào) onFinishInflate
if (finishInflate) {
parent.onFinishInflate();
}
}
特殊標(biāo)簽
額,先忽略那些 if 條件,直接先看 else,之前的套路創(chuàng)建 View
后再遞歸調(diào)用 rInflateChildren()
,不過需要注意再重新調(diào)用 rInflateChildren()
時(shí),parent
參數(shù)已經(jīng)是剛剛新創(chuàng)建的 view
啦。最后回調(diào)onFinishInflate()
方法。
tag requestFocus 標(biāo)簽
接著,再說說前面的這些 if 語句,除了我們熟悉的 include
merge
標(biāo)簽檢查,這里居然還有什么 tag
requestFocus
等冷門標(biāo)簽, 我反正有點(diǎn)兒震驚,層度不低于那個(gè)彩蛋。
<tag
android:id="@id/test1"
android:value="testTagValue"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"/>
然后嘗試了下 tag
標(biāo)簽,結(jié)果是 OK 的,我可以直接在父布局中使用 getTag(R.id.test1)
拿到我在 xml 中設(shè)置的 value
。 不過具體使用場景我著實(shí)沒有想到,requestFocus
也是如此。
merge 標(biāo)簽
我們知道,merge
標(biāo)簽用于減少層級(jí),必須是頂級(jí)標(biāo)簽,從上面代碼就可以看到對頂級(jí)標(biāo)簽的檢測。減少層級(jí)的話,就又要回到 inflate()
方法中第二點(diǎn)。
//2.merge 標(biāo)簽的注意事項(xiàng)
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
標(biāo)簽,會(huì)直接調(diào)用 rInflate()
方法填充下一層級(jí),parent
參數(shù)也不會(huì)變,所以,merge
標(biāo)簽下面的內(nèi)容直接就加到了 rootView
中。所以,這種情況,上一層肯定不能為空,傳入的 parent
肯定不能為空。
include 標(biāo)簽
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 標(biāo)簽直接填充
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 標(biāo)簽上的 id
final int id = a.getResourceId(R.styleable.Include_id, View.NO_ID);
//3.include 標(biāo)簽上的 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.設(shè)置可見性
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
標(biāo)簽,直接再次解析,然后會(huì)取出 include
標(biāo)簽上的 id
和 visibility
屬性,如果 include
標(biāo)簽上面有 id
,那么會(huì)重新設(shè)置給 View
,那么之前設(shè)置的 id
就會(huì)失效,然后更新 visibility
屬性。
ViewStub 標(biāo)簽
我們知道,ViewStub
標(biāo)簽是用來占位,實(shí)現(xiàn) View
懶加載。那么到底實(shí)現(xiàn)的呢?先看代碼。
...
//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]));
}
根據(jù)這個(gè)代碼,明顯看出 ViewStub
標(biāo)簽和 include
或者 merge
不一樣,它是 View
的子類,是一個(gè)真實(shí) 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
默認(rèn)寬高都是 0 ,draw()
(注意是 draw()
而不是 onDraw()
方法)等方法都是空實(shí)現(xiàn),真就是一個(gè)殼。接著看它的 inflate ()
方法實(shí)現(xiàn)。
public View inflate() {
final ViewParent viewParent = getParent();
...
if (mLayoutResource != 0) {
final ViewGroup parent = (ViewGroup) viewParent;
//1.填充真實(shí)布局
final View view = inflateViewNoAdd(parent);
//2.替換自己
replaceSelfWithView(view, parent);
//3.創(chuàng)建弱引用
mInflatedViewRef = new WeakReference<>(view);
...
return view;
}
...
}
private View inflateViewNoAdd(ViewGroup parent) {
final LayoutInflater factory;
...
//1.填充真實(shí)布局
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.添加真實(shí)布局
if (layoutParams != null) {
parent.addView(view, index, layoutParams);
} else {
parent.addView(view, index);
}
}
看完還是那話,ViewStub
就是一個(gè)殼,先占一個(gè)坑,在調(diào)用 inflate()
之后才加載真實(shí)布局,然后替換掉自己,從而實(shí)現(xiàn)懶加載。說到這里,還要看一下它的 setVisibility()
方法。
@Override
@android.view.RemotableViewMethod(asyncImpl = "setVisibilityAsync")
public void setVisibility(int visibility) {
//1.調(diào)用 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.第一次設(shè)置可見時(shí)觸發(fā) inflate()
if (visibility == VISIBLE || visibility == INVISIBLE) {
inflate();
}
}
}
第一次看到這個(gè)方法時(shí),我在想,我們可以直接通過 ViewStub
的 VISIBLE
GONE
來控制顯示和消失啊,為什么還要拿到真實(shí)布局來控制呢?后面嘗試之后才意識(shí)到一個(gè)問題,上面的 replaceSelfWithView()
方法已經(jīng)將自己刪除,所以,當(dāng)我們調(diào)用 viewStub.setVisibilty(View.VISIBLE)
之后,viewStub
這個(gè)對象已經(jīng)被置空,不能再次使用。這個(gè)想法沒法實(shí)現(xiàn),而且更尷尬的是,如果你直接調(diào)用viewStub.setVisibilty(View.INVISIBLE)
之后,viewStub
置空,但是你又沒有真實(shí) view
引用,你就不能直接讓它再次展示出來了。是不是覺得這里有個(gè)坑?其實(shí)這個(gè)時(shí)候你可以使用findView查找了,所以這個(gè)坑不存在。不過這也解釋了 ViewStub
為什么要用弱引用來持有真實(shí) View
。
Factory 拓展
來填一填上文 Factory
的坑,之前說到 Activity
實(shí)現(xiàn)了 Factory
接口,但是什么時(shí)候,怎么把自己設(shè)置到 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()
方法中,會(huì)調(diào)用 setPrivateFactory(this)
方法把自己設(shè)置到 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);
}
}
看這個(gè)代碼,它是設(shè)置的 mPrivateFactory
,這個(gè)優(yōu)先級(jí)是最低的,前面介紹時(shí)第二點(diǎn)各種 factory
中,首先是 mFactory2
和 mFactory
,這兩個(gè) factory
是提供方法讓我們我們設(shè)置更改的,不過需要注意只能設(shè)置一次,所以,先打印看看 Activity
中設(shè)置情況。
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
中,默認(rèn)都沒有設(shè)置,所以你完全可以調(diào)用 setFactory()
方法設(shè)置我們指定的Factory
來解析對應(yīng) View
。注意:上面演示時(shí)使用的是 Activity,但我們一般不會(huì)直接繼承 Activity
,因?yàn)樾碌?appcompat
包中的那些新控件例 Toolbar
等等,都需要使用 AppCompatActivity
搭配上 appcompat
主題。這種情況下,再看看相關(guān)日志輸出。
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
已經(jīng)都設(shè)置了,而且這個(gè)變量只能設(shè)置一次,設(shè)置時(shí)會(huì)有檢查,所以在這種情況下,我們基本上沒辦法再去設(shè)置新的 Factory
。既然它已經(jīng)設(shè)置過,那么就弄明白兩個(gè)問題,第一,哪里設(shè)置,第二,有什么特別的用途。也不賣關(guān)子,第一個(gè)問題,在 AppcompatActivity
的 onCreate()
方法中。
@Override
protected void onCreate(@Nullable Bundle savedInstanceState) {
final AppCompatDelegate delegate = getDelegate();
delegate.installViewFactory();
delegate.onCreate(savedInstanceState);
...
}
再貼一個(gè) LayoutInflaterCompat
代碼片段,這里強(qiáng)調(diào)有 framework
bug 修復(fù),已經(jīng)通過 反射 強(qiáng)制更新 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);
}
}
}
第二點(diǎn),有什么用呢,前面提過,Activity
中,其實(shí)就判斷是否是 fragment
標(biāo)簽,不是的話,就返回空,不操作。在 AppcompatActivity
中,createView()
會(huì)執(zhí)行到 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
標(biāo)簽本來應(yīng)該創(chuàng)建的 TextView
換成了 AppCompatTextView
類。
直接貼個(gè)圖,簡單理解,Google 官方推出 AppCompat
組件之后,新增一些新特性。出于對我們開發(fā)者關(guān)照(讓你一個(gè)個(gè) xml 去替換估計(jì)你也不會(huì)干),所以就想出通過 LayoutInflator
中 setFactory()
這個(gè)方法直接添加自己的轉(zhuǎn)換工廠,這樣神不知鬼不覺的就讓你的舊控件就能使用新特性(我們就可以偷懶)。所以,在 AppCompatActivity
和 AppCompaDialog
中,不用刻意去寫 AppCompatXxxView
,它會(huì)自動(dòng)轉(zhuǎn)換。截圖中最后一句有強(qiáng)調(diào),我們只需要注意在自定義 View
時(shí)才需要額外設(shè)置繼承 AppCompatXxxView
,到這里,Android Studio 給你警告的原因也大白。
Fragment View 創(chuàng)建
最后,再補(bǔ)全 Fragment
中 View
的創(chuàng)建過程。 前文分析 Activity
中只解析 fragment
標(biāo)簽。最后會(huì)調(diào)用到 FragmentManager
中的 onCreateView()
方法。
@Override
public View onCreateView(View parent, String name, Context context, AttributeSet attrs) {
if (!"fragment".equals(name)) {
return null;
}
...
//創(chuàng)建 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
創(chuàng)建不展開說了,用了反射,以后篇章有空再細(xì)聊。下面調(diào)用 moveToState()
方法,state
設(shè)置的是 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);
}
到這里,就回調(diào)我們熟悉的 onCreateView(inflater, container, savedInstanceState)
方法,完工。