最近看了些 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)
方法,完工。