前言
View的繪制流程這一篇文章其實十分不好寫,因為在網(wǎng)上已經(jīng)有千篇一律的文章,導致我一直不太想寫這一篇文章。不過既然是Android重學系列,還是一步一腳印來分析分析里面的細節(jié)。如果對這個流程很熟悉的人來說,本文就沒必要閱讀了。如果不是很熟悉的朋友可以閱讀本文,看看系統(tǒng)上設計的優(yōu)點以及可以優(yōu)化的地方。
正文
在整個View的繪制流程中,從大的方向看來,大致上分為兩部分:
- 在Activity的onCreate生命周期,實例化所有的View。
- Activity的onResume生命周期,測量,布局,繪制所有的View。
暫時不去看Activity如何聯(lián)通SurfaceFlinger,之后會有專門的專題再來聊聊。
那么Activity是怎么管理View的繪制的呢?接下來我們會以上面兩點為線索來分析一下源碼。
不過內容很多,本文集中重點聊聊view的實例化是怎么回事。
總覽
- Activity的初始化與分層
- LayoutInflater原理,以及思考
- AsyncLayoutInflater的原理以及缺陷
Activity onCreate的綁定
其實Activity的生命周期僅僅只是管理著Activity這個對象的活躍狀態(tài),并沒有真的去管理View,那么Activity是怎么通過管理View的繪制的呢?我們來看看在ActivityThread調用performLaunchActivity:
文件:/frameworks/base/core/java/android/app/ActivityThread.java
Window window = null;
if (r.mPendingRemoveWindow != null && r.mPreserveWindow) {
window = r.mPendingRemoveWindow;
r.mPendingRemoveWindow = null;
r.mPendingRemoveWindowManager = null;
}
appContext.setOuterContext(activity);
activity.attach(appContext, this, getInstrumentation(), r.token,
r.ident, app, r.intent, r.activityInfo, title, r.parent,
r.embeddedID, r.lastNonConfigurationInstances, config,
r.referrer, r.voiceInteractor, window, r.configCallback);
能看到在這個步驟中,對著實例化的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, ActivityConfigCallback activityConfigCallback) {
attachBaseContext(context);
mFragments.attachHost(null /*parent*/);
mWindow = new PhoneWindow(this, window, activityConfigCallback);
mWindow.setWindowControllerCallback(this);
mWindow.setCallback(this);
mWindow.setOnWindowDismissedCallback(this);
mWindow.getLayoutInflater().setPrivateFactory(this);
if (info.softInputMode != WindowManager.LayoutParams.SOFT_INPUT_STATE_UNSPECIFIED) {
mWindow.setSoftInputMode(info.softInputMode);
}
...
mUiThread = Thread.currentThread();
mMainThread = aThread;
mInstrumentation = instr;
mToken = token;
mIdent = ident;
mApplication = application;
mIntent = intent;
mReferrer = referrer;
mComponent = intent.getComponent();
mActivityInfo = info;
....
mWindow.setWindowManager(
(WindowManager)context.getSystemService(Context.WINDOW_SERVICE),
mToken, mComponent.flattenToString(),
(info.flags & ActivityInfo.FLAG_HARDWARE_ACCELERATED) != 0);
if (mParent != null) {
mWindow.setContainer(mParent.getWindow());
}
mWindowManager = mWindow.getWindowManager();
mCurrentConfig = config;
mWindow.setColorMode(info.colorMode);
...
}
能看到在attach中實際上最為重要的工作就是實例化一個PhoneWindow對象,并且把當前Phone相關的監(jiān)聽,如點擊事件的回調,窗體消失的回調等等。
并且把ActivityThread,ActivityInfo,Application等重要的信息綁定到當前的Activity。
從上一個專欄WMS,就能知道,實際上承載視圖真正的對象實際上是Window窗口。那么這個Window對象又是什么做第一次的視圖加載呢?
其實是調用了我們及其熟悉的api:
public void setContentView(@LayoutRes int layoutResID) {
getWindow().setContentView(layoutResID);
initWindowDecorActionBar();
}
在這個api中設置了PhoneWindow的內容視圖區(qū)域。這也是每一個Android開發(fā)的接觸到的第一個api。因為其至關重要承載了Android接下來要顯示什么內容。
接下來我們很容易想到setContentView究竟做了什么事情,來初始化所有的View對象。
PhoneWindow.setContentView
文件:/frameworks/base/core/java/com/android/internal/policy/PhoneWindow.java
@Override
public void setContentView(int layoutResID) {
if (mContentParent == null) {
installDecor();
} else if (!hasFeature(FEATURE_CONTENT_TRANSITIONS)) {
...
}
if (hasFeature(FEATURE_CONTENT_TRANSITIONS)) {
....
} else {
mLayoutInflater.inflate(layoutResID, mContentParent);
}
mContentParent.requestApplyInsets();
final Callback cb = getCallback();
if (cb != null && !isDestroyed()) {
cb.onContentChanged();
}
mContentParentExplicitlySet = true;
}
我們這里只關注核心邏輯。能看到當mContentParent為空的時候,會調用installDecor生成一個父容器,最終會通過我們另一個熟悉的函數(shù)LayoutInflater.inflate把所有的View都實例化出來。
那么同理,我們把整個步驟分為2部分:
- 1.installDecor生成DecorView安裝在FrameLayout作為所有View的頂層View
- 2.LayoutInflater.inflate 實例化傳進來的內容。
installDecor生成DecorView作為所有View的頂層View
private void installDecor() {
mForceDecorInstall = false;
if (mDecor == null) {
//核心事件1
mDecor = generateDecor(-1);
mDecor.setDescendantFocusability(ViewGroup.FOCUS_AFTER_DESCENDANTS);
mDecor.setIsRootNamespace(true);
if (!mInvalidatePanelMenuPosted && mInvalidatePanelMenuFeatures != 0) {
mDecor.postOnAnimation(mInvalidatePanelMenuRunnable);
}
} else {
mDecor.setWindow(this);
}
if (mContentParent == null) {
//核心事件二
mContentParent = generateLayout(mDecor);
// Set up decor part of UI to ignore fitsSystemWindows if appropriate.
mDecor.makeOptionalFitsSystemWindows();
final DecorContentParent decorContentParent = (DecorContentParent) mDecor.findViewById(
R.id.decor_content_parent);
if (decorContentParent != null) {
...
} else {
...
}
if (mDecor.getBackground() == null && mBackgroundFallbackResource != 0) {
mDecor.setBackgroundFallback(mBackgroundFallbackResource);
}
// Only inflate or create a new TransitionManager if the caller hasn't
// already set a custom one.
if (hasFeature(FEATURE_ACTIVITY_TRANSITIONS)) {
...
}
}
}
我們抽出核心邏輯看看這個installDecor做的事情實際上很簡單:
- 1.generateDecor生成DecorView
- 2.generateLayout 獲取DecorView中的內容區(qū)域
- 3.尋找DecorView中的DecorContentParent處理PanelMenu等系統(tǒng)內置的掛在view。
- 4.處理專場動畫。
我們只需要把關注點放在頭兩項。這兩個才是本文的重點。第四項的處理實際上是處理
generateDecor生成DecorView
protected DecorView generateDecor(int featureId) {
// System process doesn't have application context and in that case we need to directly use
// the context we have. Otherwise we want the application context, so we don't cling to the
// activity.
Context context;
if (mUseDecorContext) {
Context applicationContext = getContext().getApplicationContext();
if (applicationContext == null) {
context = getContext();
} else {
context = new DecorContext(applicationContext, getContext().getResources());
if (mTheme != -1) {
context.setTheme(mTheme);
}
}
} else {
context = getContext();
}
return new DecorView(context, featureId, this, getAttributes());
}
能看到里面十分簡單,聲明DecorContext,注入到DecorView。如果處理著名的鍵盤內存泄漏的時候,把打印打開,當切換到另一個Activity的時候,就會看到這個這個Context。
在Android系統(tǒng)看來DecorView必須擁有自己的Context的原因是,DecorView是系統(tǒng)自己的服務,因此需要做Context的隔離。不過雖然是系統(tǒng)服務,但是還是添加到我們的View當中。
generateLayout 獲取DecorView中的內容區(qū)域
protected ViewGroup generateLayout(DecorView decor) {
// Apply data from current theme.
TypedArray a = getWindowStyle();
//獲取當前窗體所有的標志位
...
//根據(jù)標志位做初步處理,如背景
....
...
// 根據(jù)標志位設置資源id
int layoutResource;
int features = getLocalFeatures();
// System.out.println("Features: 0x" + Integer.toHexString(features));
if ((features & (1 << FEATURE_SWIPE_TO_DISMISS)) != 0) {
...
} else if ((features & ((1 << FEATURE_LEFT_ICON) | (1 << FEATURE_RIGHT_ICON))) != 0) {
...
} else if ((features & ((1 << FEATURE_PROGRESS) | (1 << FEATURE_INDETERMINATE_PROGRESS))) != 0
&& (features & (1 << FEATURE_ACTION_BAR)) == 0) {
...
} else if ((features & (1 << FEATURE_CUSTOM_TITLE)) != 0) {
...
} else if ((features & (1 << FEATURE_NO_TITLE)) == 0) {
...
} else if ((features & (1 << FEATURE_ACTION_MODE_OVERLAY)) != 0) {
layoutResource = R.layout.screen_simple_overlay_action_mode;
} else {
// Embedded, so no decoration is needed.
layoutResource = R.layout.screen_simple;
// System.out.println("Simple!");
}
mDecor.startChanging();
mDecor.onResourcesLoaded(mLayoutInflater, layoutResource);
ViewGroup contentParent = (ViewGroup)findViewById(ID_ANDROID_CONTENT);
if (contentParent == null) {
throw new RuntimeException("Window couldn't find content container view");
}
...
return contentParent;
}
這里做的事情如下:
- 1.獲取設置在xml中的窗體style,設置相應的標志位如mFloat等
- 2.獲取DecorView窗體的屬性,進一步處理一些背景,根據(jù)當前的Android版本,style重新設置窗體的屬性,以供后面使用。
- 3.根據(jù)上面設置的標志位設置合適的窗體資源
- 4.獲取ID_ANDROID_CONTENT中的內容區(qū)域。
假如,我們當前使用的是最普通的狀態(tài),將會加載R.layout.screen_simple;資源文件到DecorView中,當實例化好當前的xml資源之后,將會從DecorView找到我們的內容區(qū)域部分。
我們看看screen_simple是什么東西:
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:fitsSystemWindows="true"
android:orientation="vertical">
<ViewStub android:id="@+id/action_mode_bar_stub"
android:inflatedId="@+id/action_mode_bar"
android:layout="@layout/action_mode_bar"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:theme="?attr/actionBarTheme" />
<FrameLayout
android:id="@android:id/content"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:foregroundInsidePadding="false"
android:foregroundGravity="fill_horizontal|top"
android:foreground="?android:attr/windowContentOverlay" />
</LinearLayout>
能看到這是一個LinearLayout包裹著的一個FrameLayout。把當前的View設置一個windowContentOverlay屬性。這個屬性可以在Activity生成之后,渲染速度太慢可以設置成白色透明或者圖片。一個ViewStub用來優(yōu)化顯示actionbar
和startingWindow有本質上的區(qū)別,startingWindow的出現(xiàn)是為了處理還沒有進入Activity,繪制在屏幕的窗體,同時還能獲取之前保留下來的屏幕像素渲染在上面。是一個優(yōu)化顯示體驗的設計。
最后找到這個id為content的內容區(qū)域返回回去。
這樣我們就知道網(wǎng)上那個Android的顯示區(qū)域劃分圖是怎么來的。
如果我們把對象考慮進來大致上是如此:
LayoutInflater.inflate實例化所有內容視圖
核心代碼如下:
mLayoutInflater.inflate(layoutResID, mContentParent);
實際上對于Android開發(fā)來說,這個api也熟悉的不能再熟悉了。我們看看LayoutInflater常用的用法。
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;
}
可以去結合我上一個專欄的WMS的getSystemService分析。實際上在這里面LayoutInflater是一個全局單例。為什么一定要設計為單例這是有原因的。看到后面就知道為什么了。
LayoutInflater.inflate原理
接下來我們把注意力轉移到inflater如何實例化view上面:
public View inflate(@LayoutRes int resource, @Nullable ViewGroup root) {
return inflate(resource, root, root != null);
}
public View inflate(XmlPullParser parser, @Nullable ViewGroup root) {
return inflate(parser, 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();
}
}
我們常用的LayoutInflater其實有三種方式,最后都會調用三個參數(shù)的inflate方法。
分為2個步驟
- 1.首先先通過Resource獲取XmlResourceParser的解析器
- 2.inflate按照解析器實例化View。
換句話說,為了弄清楚View怎么實例化這個流程,我們必須看Android是怎么獲取資源的。
Resource解析xml
public XmlResourceParser getLayout(@LayoutRes int id) throws NotFoundException {
return loadXmlResourceParser(id, "layout");
}
XmlResourceParser loadXmlResourceParser(@AnyRes int id, @NonNull String type)
throws NotFoundException {
final TypedValue value = obtainTempTypedValue();
try {
final ResourcesImpl impl = mResourcesImpl;
impl.getValue(id, value, true);
if (value.type == TypedValue.TYPE_STRING) {
return impl.loadXmlResourceParser(value.string.toString(), id,
value.assetCookie, type);
}
throw new NotFoundException("Resource ID #0x" + Integer.toHexString(id)
+ " type #0x" + Integer.toHexString(value.type) + " is not valid");
} finally {
releaseTempTypedValue(value);
}
}
能看到此時是通過loadXmlResourceParser來告訴底層解析的是Layout資源。能看到此時會把工作交給ResourcesImpl和AssetManager去完成。這個類如果看過我的文章的朋友就很熟悉了,這兩個類就是Java層加載資源的核心類,當我們做插件化的時候,是不可避免的接觸這個類。
經(jīng)過的方法,大致上我們把資源讀取的步驟分為3部分:
- 1.獲取TypedValue
- 2.讀取資源文件,生成保存著xml解析內容的對象
- 3.釋放掉TypedValue
這個步驟和我們平時開發(fā)自定義View設置自定義屬性的時候何其相似,都是通過obtainStyledAttributes打開TypeArray,讀取其中的數(shù)據(jù),最后關閉TypeArray。
這背后隱藏這什么玄機呢?我們之后會有文章專門探索,我們不要打斷當前的思緒。
inflate解析Xml解析器中的數(shù)據(jù)
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 (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
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) {
temp.setLayoutParams(params);
}
}
rInflateChildren(parser, temp, attrs, true);
// 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);
}
if (root == null || !attachToRoot) {
result = temp;
}
}
} catch (XmlPullParserException e) {
...
} catch (Exception e) {
...
} finally {
// Don't retain static reference on context.
mConstructorArgs[0] = lastContext;
mConstructorArgs[1] = null;
Trace.traceEnd(Trace.TRACE_TAG_VIEW);
}
return result;
}
}
從這我們就能看到是否添加父布局以及是否綁定父布局之間的差別了。
首先不斷的從xml的頭部開始查找(第一個“<”),直到找到第一個view的進行解析。接下來就分為兩個路線:
- 1.此時xml布局使用了merge優(yōu)化,調用rInflate。請注意直接使用LayoutInflate實例化merge標簽,請設置根布局不然會報錯。
- 2.xml是普通的布局,調用rInflateChildren
先來看看第二種不普通情況,在沒有merge布局的情況:
- 1.先通過createViewFromTag實例化對應的view。
- 2.接著根據(jù)查看當前有沒有root需要綁定的父布局,如果有,則獲取根布局的generLayout:
public LayoutParams generateLayoutParams(AttributeSet attrs) {
return new LayoutParams(getContext(), attrs);
}
根據(jù)xml當前標簽的屬性生成適配根布局的LayoutParams。
attachToRoot如果為true,則會直接添加到根布局中。
- 3.rInflateChildren繼續(xù)解析當前布局下的根布局,進入遞歸。
這也解釋了三個inflate方法之間的區(qū)別,帶著根部布局參數(shù)的inflate能夠將當前根部的標簽的參數(shù)生成一個適配根部LayoutParams,也就保留了根部布局的屬性。而最后一個bool僅僅是代表用不用系統(tǒng)自動幫你添加到根布局中
在這里面有一個核心的函數(shù)createViewFromTag,這是就是如何創(chuàng)建View的核心。
createViewFromTag創(chuàng)建View
private View createViewFromTag(View parent, String name, Context context, AttributeSet attrs) {
return createViewFromTag(parent, name, context, attrs, false);
}
View createViewFromTag(View parent, String name, Context context, AttributeSet attrs,
boolean ignoreThemeAttr) {
if (name.equals("view")) {
name = attrs.getAttributeValue(null, "class");
}
// Apply a theme wrapper, if allowed and one is specified.
if (!ignoreThemeAttr) {
final TypedArray ta = context.obtainStyledAttributes(attrs, ATTRS_THEME);
final int themeResId = ta.getResourceId(0, 0);
if (themeResId != 0) {
context = new ContextThemeWrapper(context, themeResId);
}
ta.recycle();
}
if (name.equals(TAG_1995)) {
// Let's party like it's 1995!
return new BlinkLayout(context, attrs);
}
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 (InflateException e) {
...
} catch (ClassNotFoundException e) {
...
} catch (Exception e) {
...
}
}
實際上這里面很簡單和很巧妙,也能發(fā)現(xiàn)Android 中的新特性。
1.如果標簽直接是view,則直接獲取xml標簽中系統(tǒng)中class的屬性,這樣就能找到view對應的類名。
2.如果name是blink,則創(chuàng)建一個深度鏈接的布局
3.通過三層的Factory攔截view的創(chuàng)建,分別是Factory,F(xiàn)actory2,privateFactory。
4.如果經(jīng)過上層由用戶或者系統(tǒng)定義的特殊view的生成攔截沒有生成,則會判斷當前的標簽名又沒有"."。有“.”說明是自定義view,沒有說明是系統(tǒng)控件。
1.系統(tǒng)控件調用onCreateView創(chuàng)建View。
2.自定義View調用createView創(chuàng)建View。
在繼續(xù)下一步之前,讓我們把目光放回系統(tǒng)生成LayoutInflater的方法中,看看生成LayoutInflater有什么貓膩?
文件:/frameworks/base/core/java/android/app/SystemServiceRegistry.java
registerService(Context.LAYOUT_INFLATER_SERVICE, LayoutInflater.class,
new CachedServiceFetcher<LayoutInflater>() {
@Override
public LayoutInflater createService(ContextImpl ctx) {
return new PhoneLayoutInflater(ctx.getOuterContext());
}});
能看到系統(tǒng)初期實例化的是一個PhoneLayoutInflater,并非是一個普通的LayoutInflater,而這個類重載了一個很重要的方法:
文件:http://androidxref.com/9.0.0_r3/xref/frameworks/base/core/java/com/android/internal/policy/PhoneLayoutInflater.java
private static final String[] sClassPrefixList = {
//常用控件
"android.widget.",
//一般指WebView
"android.webkit.",
//一般指Fragment
"android.app."
};
@Override protected View onCreateView(String name, AttributeSet attrs) throws ClassNotFoundException {
for (String prefix : sClassPrefixList) {
try {
View view = createView(name, prefix, attrs);
if (view != null) {
return view;
}
} catch (ClassNotFoundException e) {
// In this case we want to let the base class take a crack
// at it.
}
}
return super.onCreateView(name, attrs);
}
能看到這樣就手動為View添加了前綴,還是調用了createView創(chuàng)建View。舉個例子,如果是一個Linearlayout,就會為這個標簽添加android.widget.前綴,稱為android.widget.Linearlayout。這樣就能找到相對完整的類名。
createView創(chuàng)建View的核心動作
public final View createView(String name, String prefix, AttributeSet attrs)
throws ClassNotFoundException, InflateException {
//所有view對應構造函數(shù)
Constructor<? extends View> constructor = sConstructorMap.get(name);
if (constructor != null && !verifyClassLoader(constructor)) {
constructor = null;
sConstructorMap.remove(name);
}
Class<? extends View> clazz = null;
try {
Trace.traceBegin(Trace.TRACE_TAG_VIEW, name);
if (constructor == null) {
// Class not found in the cache, see if it's real, and try to add it
clazz = mContext.getClassLoader().loadClass(
prefix != null ? (prefix + name) : name).asSubclass(View.class);
if (mFilter != null && clazz != null) {
boolean allowed = mFilter.onLoadClass(clazz);
if (!allowed) {
failNotAllowed(name, prefix, attrs);
}
}
constructor = clazz.getConstructor(mConstructorSignature);
constructor.setAccessible(true);
sConstructorMap.put(name, constructor);
} else {
// If we have a filter, apply it to cached constructor
if (mFilter != null) {
// Have we seen this name before?
Boolean allowedState = mFilterMap.get(name);
if (allowedState == null) {
// New class -- remember whether it is allowed
clazz = mContext.getClassLoader().loadClass(
prefix != null ? (prefix + name) : name).asSubclass(View.class);
boolean allowed = clazz != null && mFilter.onLoadClass(clazz);
mFilterMap.put(name, allowed);
if (!allowed) {
failNotAllowed(name, prefix, attrs);
}
} else if (allowedState.equals(Boolean.FALSE)) {
failNotAllowed(name, prefix, attrs);
}
}
}
Object lastContext = mConstructorArgs[0];
if (mConstructorArgs[0] == null) {
// Fill in the context if not already within inflation.
mConstructorArgs[0] = mContext;
}
Object[] args = mConstructorArgs;
args[1] = attrs;
final View view = constructor.newInstance(args);
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 (NoSuchMethodException e) {
...
} catch (ClassCastException e) {
...
} catch (ClassNotFoundException e) {
...
} catch (Exception e) {
...
} finally {
Trace.traceEnd(Trace.TRACE_TAG_VIEW);
}
}
實際上我們能夠看到在實例化View有一個關鍵的數(shù)據(jù)結構:sConstructorMap.
這個數(shù)據(jù)結構保存著app應用內所有實例化過的View對應的構造函數(shù)。主要經(jīng)過存儲一次,就以name為key存儲對應的構造函數(shù),就不用一直反射獲取了。
實際上這段代碼就是做這個事情:
- 1.當從name找構造函數(shù),找的到,就直接通過構造函數(shù)實例化
- 2.沒有找到構造函數(shù),則通過prefix+name的方式查找類的構造函數(shù),接著再實例化。
這也解釋了為什么LayoutInflater一定要做成全局單例的方式,原因很簡單就是為了加速view實例化的過程,共用反射的構造函數(shù)的緩存。
View布局優(yōu)化細節(jié)
- 當然也能看到ViewStub實際上在這里面沒有做太多事情,把當前的Layoutflater復制一份進去,延后實例化里面的View。
- merge優(yōu)化原理,也能明白了,實際上在正常的分支會直接實例化一個View之后再添加,接著遞歸子view繼續(xù)添加當前的View。而merge則是跳出第一次實例化View步驟,直接進入遞歸。
merge能做到什么事情呢?我當然知道是壓縮層級?一般是壓縮include標簽的層級。
這里就解釋了怎么壓縮層級。換句話說,我們使用merge的時候完全不用添加父布局,直接用merge標簽包裹子view即可,當我們不能預覽,在merge上添加parentTag即可。
舉個例子:
<FrameLayout>
<include layout="@layout/layout2"/>
</FrameLayout>
layout2.xml:
<merge>
<TextView />
</merge>
合并之后就是如下:
<FrameLayout>
<TextView />
</FrameLayout>
如果把include作為根布局呢?很明顯會找不到android.view.include這個文件。
rInflate遞歸解析View
當我們生成View之后,就需要繼續(xù)遞歸子View,讓我們看看其核心邏輯是什么。
能看到無論是是走merge分支還是正常解析分支也好,本質上都會調用到rInflate這個方法。
final void rInflateChildren(XmlPullParser parser, View parent, AttributeSet attrs,
boolean finishInflate) throws XmlPullParserException, IOException {
rInflate(parser, parent, parent.getContext(), attrs, finishInflate);
}
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) {
if (type != XmlPullParser.START_TAG) {
continue;
}
final String name = parser.getName();
if (TAG_REQUEST_FOCUS.equals(name)) {
pendingRequestFocus = true;
consumeChildElements(parser);
} else if (TAG_TAG.equals(name)) {
parseViewTag(parser, parent, attrs);
} else if (TAG_INCLUDE.equals(name)) {
if (parser.getDepth() == 0) {
throw new InflateException("<include /> cannot be the root element");
}
parseInclude(parser, context, parent, attrs);
} else if (TAG_MERGE.equals(name)) {
throw new InflateException("<merge /> must be the root element");
} else {
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();
}
if (finishInflate) {
parent.onFinishInflate();
}
}
在一段代碼就是不斷循環(huán)當前的當前xml的節(jié)點解析內部的標簽,直到到了標簽的末尾("/>")
分為如下幾個分支:
- 1.如果發(fā)現(xiàn)需要requestFoucs,則設置當前view標志為聚焦
- 2.如果發(fā)現(xiàn)標簽是include,則調用parseInclude,解開include內部的layout進行解析。
- 3.標簽是tag,則保存tag中的內容
- 4.發(fā)現(xiàn)標簽是merge則報錯
- 5.其他情況,如正常一般,會正常生成View,并且添加當前的父布局中。
最后會回調onFinishInflate監(jiān)聽。
LayoutInflater Factory的妙用
或許有人會覺得LayoutInflater中設置Factory干什么的?實際上這個Factory到處有在用,只是我們沒有注意到過而已。
舉一個例子,肯定有人注意過吧。當我們使用AppCompatActivity的時候,如果打印或者打斷點,會發(fā)現(xiàn)內部的ImageView這些view,會被替換掉,變成AppCompat開頭的view,如AppCompatImageView。
我們來看看AppCompatActivity的onCreate方法:
@Override
protected void onCreate(@Nullable Bundle savedInstanceState) {
final AppCompatDelegate delegate = getDelegate();
delegate.installViewFactory();
delegate.onCreate(savedInstanceState);
}
實際上AppCompatDelegate本質上是AppCompatDelegateImpl,在這個類中調用了installViewFactory的方法。
public void installViewFactory() {
LayoutInflater layoutInflater = LayoutInflater.from(mContext);
if (layoutInflater.getFactory() == null) {
LayoutInflaterCompat.setFactory2(layoutInflater, this);
} else {
...
}
}
既然我們直到Factory需要重寫onCreateView的方法,我們直接看看這個類中的方法:
public View createView(View parent, final String name, @NonNull Context context,
@NonNull AttributeSet attrs) {
if (mAppCompatViewInflater == null) {
TypedArray a = mContext.obtainStyledAttributes(R.styleable.AppCompatTheme);
String viewInflaterClassName =
a.getString(R.styleable.AppCompatTheme_viewInflaterClass);
if ((viewInflaterClassName == null)
|| AppCompatViewInflater.class.getName().equals(viewInflaterClassName)) {
...
mAppCompatViewInflater = new AppCompatViewInflater();
} else {
try {
Class viewInflaterClass = Class.forName(viewInflaterClassName);
mAppCompatViewInflater =
(AppCompatViewInflater) viewInflaterClass.getDeclaredConstructor()
.newInstance();
} catch (Throwable t) {
..
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 */
);
}
能看到此時,會實例化AppCompatViewInflater,把實例化View交給它來處理。
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;
case "ImageView":
view = createImageView(context, attrs);
verifyNotNull(view, name);
break;
case "Button":
view = createButton(context, attrs);
verifyNotNull(view, name);
break;
case "EditText":
view = createEditText(context, attrs);
verifyNotNull(view, name);
break;
case "Spinner":
view = createSpinner(context, attrs);
verifyNotNull(view, name);
break;
case "ImageButton":
view = createImageButton(context, attrs);
verifyNotNull(view, name);
break;
case "CheckBox":
view = createCheckBox(context, attrs);
verifyNotNull(view, name);
break;
case "RadioButton":
view = createRadioButton(context, attrs);
verifyNotNull(view, name);
break;
case "CheckedTextView":
view = createCheckedTextView(context, attrs);
verifyNotNull(view, name);
break;
case "AutoCompleteTextView":
view = createAutoCompleteTextView(context, attrs);
verifyNotNull(view, name);
break;
case "MultiAutoCompleteTextView":
view = createMultiAutoCompleteTextView(context, attrs);
verifyNotNull(view, name);
break;
case "RatingBar":
view = createRatingBar(context, attrs);
verifyNotNull(view, name);
break;
case "SeekBar":
view = createSeekBar(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);
}
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 its android:onClick
checkOnClickListener(view, attrs);
}
return view;
}
能看到吧,此時把根據(jù)name去生成不同的View,這樣就替換原來的View加入到布局中。
從官方的App包我們能夠得到什么啟發(fā)?我們可以通過在這里做一個Factory,做一次View的生成攔截。實際上這種思路,已經(jīng)被用于換膚框架中,其中一種,且個人認為最好的流派。就是這樣設計的
當然除了App包有這種操作,實際上在Activity里面也有一樣的設計,不過是專門針對Fragment的。
Fragment標簽的實例化
還記得開篇的attach方法嗎?其中一行設置了當前LayoutInflater的PrivateFactory
mWindow.getLayoutInflater().setPrivateFactory(this);
這個Factory也是有一個onCreateView的接口,我們直接看看做了什么:
public View onCreateView(@Nullable View parent, @NonNull String name,
@NonNull Context context, @NonNull AttributeSet attrs) {
if (!"fragment".equals(name)) {
return onCreateView(name, context, attrs);
}
return mFragments.onCreateView(parent, name, context, attrs);
}
能看到在PrivateFactory攔截的正是fragment標簽。接著就通過mFragments.onCreateView創(chuàng)建Fragment。關于Fragment,我之后專門用一篇文章聊聊。
正是因為Fragment不是一個View,因此才需要做這種特殊處理。
AsyncLayoutInflater的性能優(yōu)化
在Android的渲染中,其實大部分的事情都在ui線程中完成。我們稍微思考其中的工作,我們暫時只考慮Java可以輕易看見的地方。做了反射,做了測量布局,渲染,都在一個線程中。除此之外還有很多業(yè)務邏輯,這樣會導致ui線程十分重量級。
為了解決這個問題,官方也好,各大廠商也好,都做十分巨大的努力去優(yōu)化這個ui渲染速度。
接下來AsyncLayoutInflater就是官方提供優(yōu)化工具,實際上這是一個封裝好的異步LayoutInflater,這樣就能降低ui線程的壓力。想法是好的,實際上不過這個api設計上有點缺陷,導致有點雞肋。
我們看看怎么使用
new AsyncLayoutInflater(Activity.this)
.inflate(R.layout.async_layout, null, new AsyncLayoutInflater.OnInflateFinishedListener() {
@Override
public void onInflateFinished(View view, int resid, ViewGroup parent) {
setContentView(view);
}
});
能看到當異步實例化好View之后,再去setContentView。
我們直接看看構造函數(shù),以及實例化的方法:
public AsyncLayoutInflater(@NonNull Context context) {
mInflater = new BasicInflater(context);
mHandler = new Handler(mHandlerCallback);
mInflateThread = InflateThread.getInstance();
}
@UiThread
public void inflate(@LayoutRes int resid, @Nullable ViewGroup parent,
@NonNull OnInflateFinishedListener callback) {
if (callback == null) {
throw new NullPointerException("callback argument may not be null!");
}
InflateRequest request = mInflateThread.obtainRequest();
request.inflater = this;
request.resid = resid;
request.parent = parent;
request.callback = callback;
mInflateThread.enqueue(request);
}
能看到實際上每一次調用,都會把一個所有需要實例化的request封裝起來,丟進mInflateThread實例化隊列中。
private static class InflateThread extends Thread {
private static final InflateThread sInstance;
static {
sInstance = new InflateThread();
sInstance.start();
}
public static InflateThread getInstance() {
return sInstance;
}
private ArrayBlockingQueue<InflateRequest> mQueue = new ArrayBlockingQueue<>(10);
private SynchronizedPool<InflateRequest> mRequestPool = new SynchronizedPool<>(10);
// Extracted to its own method to ensure locals have a constrained liveness
// scope by the GC. This is needed to avoid keeping previous request references
// alive for an indeterminate amount of time, see b/33158143 for details
public void runInner() {
InflateRequest request;
try {
request = mQueue.take();
} catch (InterruptedException ex) {
// Odd, just continue
Log.w(TAG, ex);
return;
}
try {
request.view = request.inflater.mInflater.inflate(
request.resid, request.parent, false);
} catch (RuntimeException ex) {
// Probably a Looper failure, retry on the UI thread
Log.w(TAG, "Failed to inflate resource in the background! Retrying on the UI"
+ " thread", ex);
}
Message.obtain(request.inflater.mHandler, 0, request)
.sendToTarget();
}
@Override
public void run() {
while (true) {
runInner();
}
}
public InflateRequest obtainRequest() {
InflateRequest obj = mRequestPool.acquire();
if (obj == null) {
obj = new InflateRequest();
}
return obj;
}
public void releaseRequest(InflateRequest obj) {
obj.callback = null;
obj.inflater = null;
obj.parent = null;
obj.resid = 0;
obj.view = null;
mRequestPool.release(obj);
}
public void enqueue(InflateRequest request) {
try {
mQueue.put(request);
} catch (InterruptedException e) {
throw new RuntimeException(
"Failed to enqueue async inflate request", e);
}
}
}
能看到這個線程的run方法中是一個死循環(huán),在這個死循環(huán)里面會不斷讀取mQueue的的請求,進行一次次的實例化。一旦實例化完成之后,將會通過Handler通知回調完成。
通過AsyncLayoutInflater和正常的LayoutInflater比較就能清楚,雙方的差異是什么?
我們稍微瀏覽一下用于實例化操作真正的LayoutInflater
private static class BasicInflater extends LayoutInflater {
private static final String[] sClassPrefixList = {
"android.widget.",
"android.webkit.",
"android.app."
};
BasicInflater(Context context) {
super(context);
}
@Override
public LayoutInflater cloneInContext(Context newContext) {
return new BasicInflater(newContext);
}
@Override
protected View onCreateView(String name, AttributeSet attrs) throws ClassNotFoundException {
for (String prefix : sClassPrefixList) {
try {
View view = createView(name, prefix, attrs);
if (view != null) {
return view;
}
} catch (ClassNotFoundException e) {
// In this case we want to let the base class take a crack
// at it.
}
}
return super.onCreateView(name, attrs);
}
}
- 首先AsyncLayoutInflater不能設置Factory,那么也就沒辦法創(chuàng)建AppCompat系列的View,也沒有辦法創(chuàng)建Fragment。
- 其次AsyncLayoutInflater包含的阻塞隊列居然只有10個,如果遇到RecyclerView這種多個子布局,超過了10個需要實例化的View,反而需要主線程的阻塞。可以使用線程池子處理
- 如果是setContentView的話,本身就是處于ui線程的第一步,也就沒有必要異步。
- 甚至如果線程工作滿了,可以把部分任務丟給主線程處理。
- 而且在run中寫一個死循環(huán)進行讀取實例化任務,不合理。完全有更好的異步等到做法,如生產(chǎn)者消費者模式。
處理這幾個問題確實不難,閱讀過源碼當然知道怎么處理,這里就不繼續(xù)贅述了。
總結
本文總結了Activity的分層,本質上android從視圖上來看,是一個DecorView包裹所有的View,我們繪制的內容區(qū)域一般在R.id.content中。
LayoutInflater本質上是通過一個構造函數(shù)的緩存map來加速反射View的速度,同時merge壓縮層級原理就是越過本次View的生成,將內部的view生成出來直接添加到父布局,因此如果include需要的父布局和外層一直,就沒有必要在內部也添加一個一模一樣的布局。
AsyncLayoutInflater本質上就是把反射的工作丟給一個專門的線程去處理。但是其雞肋的設計導致使用場景不廣泛。
下一篇將和大家聊聊資源是怎么加載到我們的App的,把本篇遺留的問題解決了。