xml 佈局文件是如何變成 View 並填入 View 樹的?帶着這個問題,閱讀源碼,竟然發現了一個優化佈局構建時間的方案。android
這是 Android 性能優化系列文章的第三篇,文章列表以下:性能優化
佈局構建耗時是優化 Activity 啓動速度中不可缺乏的一個環節。bash
欲優化,先度量。有啥辦法能夠精確地度量佈局耗時?cookie
以熟悉的setContentView()
爲切入點,看看有沒有突破口:app
public class AppCompatActivity
@Override
public void setContentView(View view) {
getDelegate().setContentView(view);
}
}
複製代碼
點開setContentView()
源碼,它的實現交給了一個代理,沿着調用鏈往下追查,最終的實現代碼在AppCompatDelegateImpl
中:ide
class AppCompatDelegateImpl{
@Override
public void setContentView(int resId) {
ensureSubDecor();
//'1.從頂層視圖得到content視圖'
ViewGroup contentParent = mSubDecor.findViewById(android.R.id.content);
//'2.移除全部子視圖'
contentParent.removeAllViews();
//'3.解析佈局文件並填充到content視圖中'
LayoutInflater.from(mContext).inflate(resId, contentParent);
mAppCompatWindowCallback.getWrapped().onContentChanged();
}
}
複製代碼
這三部中,最耗時操做應該是「解析佈局文件」,點進去看看:工具
public abstract class LayoutInflater {
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();
}
}
}
複製代碼
先調用了getLayout()
獲取了和佈局文件對應的解析器,沿着調用鏈繼續追查:佈局
public class ResourcesImpl {
XmlResourceParser loadXmlResourceParser(@NonNull String file, @AnyRes int id, int assetCookie,@NonNull String type) throws NotFoundException {
if (id != 0) {
try {
synchronized (mCachedXmlBlocks) {
...
//'經過AssetManager獲取佈局文件對象'
final XmlBlock block = mAssets.openXmlBlockAsset(assetCookie, file);
if (block != null) {
final int pos = (mLastCachedXmlBlockIndex + 1) % num;
mLastCachedXmlBlockIndex = pos;
final XmlBlock oldBlock = cachedXmlBlocks[pos];
if (oldBlock != null) {
oldBlock.close();
}
cachedXmlBlockCookies[pos] = assetCookie;
cachedXmlBlockFiles[pos] = file;
cachedXmlBlocks[pos] = block;
return block.newParser();
}
}
} catch (Exception e) {
...
}
}
...
}
}
複製代碼
沿着調用鏈,最終走到了ResourcesImpl.loadXmlResourceParser()
,它經過AssetManager.openXmlBlockAsset()
將 xml 佈局文件轉化成 Java 對象XmlBlock
:post
public final class AssetManager implements AutoCloseable {
@NonNull XmlBlock openXmlBlockAsset(int cookie, @NonNull String fileName) throws IOException {
Preconditions.checkNotNull(fileName, 」fileName「);
synchronized (this) {
ensureOpenLocked();
//'打開 xml 佈局文件'
final long xmlBlock = nativeOpenXmlAsset(mObject, cookie, fileName);
if (xmlBlock == 0) {
//'若打開失敗則拋文件未找到異常'
throw new FileNotFoundException(「Asset XML file: 」 + fileName);
}
final XmlBlock block = new XmlBlock(this, xmlBlock);
incRefsLocked(block.hashCode());
return block;
}
}
}
複製代碼
經過一個 native 方法,將佈局文件讀取到內存。走查到這裏,有一件事能夠肯定,即 「解析 xml 佈局文件前須要進行 IO 操做,將其讀取至內存中」。性能
讀原碼就好像「遞歸」,剛纔經過不斷地「遞」,如今經過「歸」回到那個關鍵方法:
public abstract class LayoutInflater {
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();
}
}
}
複製代碼
經過 IO 操做將佈局文件讀到內存後,調用了inflate()
:
public abstract class LayoutInflater {
public View inflate(XmlPullParser parser, @Nullable ViewGroup root, boolean attachToRoot) {
synchronized (mConstructorArgs) {
...
try {
//'根據佈局文件的聲明控件的標籤構建 View'
final View temp = createViewFromTag(root, name, inflaterContext, attrs);
ViewGroup.LayoutParams params = null;
//'構建 View 對應的佈局參數'
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);
}
}
...
//'將 View 填充到 View 樹'
if (root != null && attachToRoot) {
root.addView(temp, params);
}
...
} catch (XmlPullParserException e) {
...
} finally {
...
}
return result;
}
}
複製代碼
這個方法解析佈局文件並根據其中聲明控件的標籤構建 View實例,而後將其填充到 View 樹中。解析佈局文件的細節在createViewFromTag()
中:
public abstract class LayoutInflater {
View createViewFromTag(View parent, String name, Context context, AttributeSet attrs,boolean ignoreThemeAttr) {
...
try {
View view;
//'經過Factory2.onCreateView()構建 View'
if (mFactory2 != null) {
view = mFactory2.onCreateView(parent, name, context, attrs);
}
...
return view;
} catch (InflateException e) {
throw e;
}
...
}
}
複製代碼
onCreateView()
的具體實如今AppCompatDelegateImpl
中:
class AppCompatDelegateImpl{
@Override
public final View onCreateView(View parent, String name, Context context, AttributeSet attrs) {
return createView(parent, name, context, attrs);
}
@Override
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){
...
} else {
try {
//'經過反射獲取AppCompatViewInflater實例'
Class<?> viewInflaterClass = Class.forName(viewInflaterClassName);
mAppCompatViewInflater =
(AppCompatViewInflater) viewInflaterClass.getDeclaredConstructor()
.newInstance();
} catch (Throwable t) {
...
}
}
}
boolean inheritContext = false;
if (IS_PRE_LOLLIPOP) {
inheritContext = (attrs instanceof XmlPullParser)
// If we have a XmlPullParser, we can detect where we are in the layout
? ((XmlPullParser) attrs).getDepth() > 1
// Otherwise we have to use the old heuristic
: shouldInheritContext((ViewParent) parent);
}
//'經過createView()建立View實例'
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 */
);
}
}
複製代碼
AppCompatDelegateImpl
又把構建 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;
...
View view = null;
//'以佈局文件中控件的名稱分別建立對應控件實例'
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;
case "ToggleButton":
view = createToggleButton(context, attrs);
verifyNotNull(view, name);
break;
default:
view = createView(context, name, attrs);
}
...
return view;
}
//'構建 AppCompatTextView 實例'
protected AppCompatTextView createTextView(Context context, AttributeSet attrs) {
return new AppCompatTextView(context, attrs);
}
...
}
複製代碼
沒想到,最終竟然是經過switch-case
的方法來 new View 實例。
並且咱們沒有必要手動將佈局文件中的TextView
都換成AppCompatTextView
,只要使用AppCompatActivity
,它在Factory2.onCreateView()
接口中完成了控件轉換。
經過上面的分析,能夠得出兩條結論:
1. Activity 構建佈局時,須要先進行 IO 操做,將佈局文件讀取至內存中。
2. 遍歷內存佈局文件中每個標籤,並根據標籤名 new 出對應視圖實例,再把它們 addView 到 View 樹中。
這兩個步驟都是耗時的!到底有多耗時呢?
LayoutInflaterCompat
提供了setFactory2()
,能夠攔截佈局文件中每個 View 的建立過程:
class Factory2Activity : AppCompatActivity() {
private var sum: Double = 0.0
@ExperimentalTime
override fun onCreate(savedInstanceState: Bundle?) {
LayoutInflaterCompat.setFactory2(LayoutInflater.from(this@Factory2Activity), object : LayoutInflater.Factory2 {
override fun onCreateView(parent: View?, name: String?, context: Context?, attrs: AttributeSet?): View? {
//'測量構建單個View耗時'
val (view, duration) = measureTimedValue { delegate.createView(parent, name, context!!, attrs!!) }
//'累加構建視圖耗時'
sum += duration.inMilliseconds
Log.v(「test」, 「view=${view?.let { it::class.simpleName }} duration=${duration} sum=${sum}」)
return view
}
//'該方法用於兼容Factory,直接返回null就好'
override fun onCreateView(name: String?, context: Context?, attrs: AttributeSet?): View? {
return null
}
})
super.onCreate(savedInstanceState)
setContentView(R.layout.factory2_activity2)
}
}
複製代碼
在super.onCreate(savedInstanceState)
以前,將自定義的Factory2
接口注入到LayoutInflaterCompat
中。
調用delegate.createView(parent, name, context!!, attrs!!)
,就是手動觸發源碼中構建佈局的邏輯。
measureTimedValue()
是 Kotlin 提供的庫方法,用於測量一個方法的耗時,定義以下:
public inline fun <T> measureTimedValue(block: () -> T): TimedValue<T> {
contract {
callsInPlace(block, InvocationKind.EXACTLY_ONCE)
}
//'委託給MonoClock'
return MonoClock.measureTimedValue(block)
}
public inline fun <T> Clock.measureTimedValue(block: () -> T): TimedValue<T> {
contract {
callsInPlace(block, InvocationKind.EXACTLY_ONCE)
}
val mark = markNow()
//'執行原方法'
val result = block()
return TimedValue(result, mark.elapsedNow())
}
public data class TimedValue<T>(val value: T, val duration: Duration)
複製代碼
方法返回一個TimedValue
對象,其第一個屬性是原方法的返回值,第二個是執行原方法的耗時。測試代碼中經過解構聲明
分別將返回值和耗時賦值給view
和duration
。而後把構建每一個視圖的耗時累加打印。
瞭解了構建佈局的過程,就有了對症下藥優化的方向。
有了測量構建佈局耗時的方法,就有了對比優化效果的工具。
限於篇幅,構建佈局耗時縮短 20 倍的方法只能放到下一篇了。