最近看了些 View
相關的源碼,相比以前,有一些新的認知。爭取經過一次整理,能系統瞭解 Android View
加載和顯示的相關過程,記錄下來,共勉。接下來的全部源碼基於 Android API 27 Platform
。java
對於 View
建立,通俗說其實就兩種方式,一種是直接經過 new
關鍵詞直接建立對象,另外就是經過 xml
填充一個 View
。第一種方式寫起來最簡易,可是,也有一些代價,好比說全部屬性都要一個個設置,通用 style 也沒辦法使用。第二種方式最傳統,也是接下來重點關注的方式。android
寫過自定義 View
都知道,咱們通常須要實現三個構造方法,固然,若是你使用 Kotlin
以後,這種狀況能夠有一些改善,相似這樣:程序員
class TestView @JvmOverloads constructor(context: Context, attrs: AttributeSet? = null, defStyleAttr: Int = -1)
canvas
第一個參數上下文,這個沒啥問題,第二個參數,AttributeSet
屬性集合,第三個參數,defStyleAttr
應該是默認 style 的 id。緩存
反正至少得有這三個參數,並且,通常來講,咱們第三個參數也沒怎麼使用,默認使用的 -1 來佔位,第二個參數通常咱們也是使用 null 來默認佔位。它們到底有什麼用呢?能夠不寫對應的構造方法嗎?app
若是咱們自定義 View
,只有上下文那個構造方法時,經過 xml 方式填充時就會出錯:async
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]
複製代碼
簡單說就是找不到兩個參數的那個構造方法,那麼這個構造方法到底在哪裏被調用呢?ide
###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();
}
複製代碼
/**
* 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
也是一個系統提供的遠程服務。ui
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()
方法。
彩蛋說完,迴歸總體,第二,出現了 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 中的呢?這個問題也放下面單獨講)。
這麼說下來,若是不是 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
的兩個參數的構造方法建立。
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()
方法。
接着,再說說前面的這些 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
標籤用於減小層級,必須是頂級標籤,從上面代碼就能夠看到對頂級標籤的檢測。減小層級的話,就又要回到 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
確定不能爲空。
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
標籤是用來佔位,實現 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
的坑,以前說到 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
的建立過程。 前文分析 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)
方法,完工。