xml 布局文件是如何變成 View 并填入 View 樹的?帶著這個問題,閱讀源碼穿挨,居然發(fā)現(xiàn)了一個優(yōu)化布局構建時間的方案。
這是 Android 性能優(yōu)化系列文章的第三篇肴盏,文章列表如下:
布局構建耗時是優(yōu)化 Activity 啟動速度中不可缺少的一個環(huán)節(jié)菜皂。
欲優(yōu)化贞绵,先度量。有啥辦法可以精確地度量布局耗時恍飘?
讀布局文件
以熟悉的setContentView()
為切入點榨崩,看看有沒有突破口:
public class AppCompatActivity
@Override
public void setContentView(View view) {
getDelegate().setContentView(view);
}
}
點開setContentView()
源碼,它的實現(xiàn)交給了一個代理章母,沿著調(diào)用鏈往下追查母蛛,最終的實現(xiàn)代碼在AppCompatDelegateImpl
中:
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();
}
}
}
先調(diào)用了getLayout()
獲取了和布局文件對應的解析器溯祸,沿著調(diào)用鏈繼續(xù)追查:
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) {
...
}
}
...
}
}
沿著調(diào)用鏈,最終走到了ResourcesImpl.loadXmlResourceParser()
舞肆,它通過AssetManager.openXmlBlockAsset()
將 xml 布局文件轉(zhuǎn)化成 Java 對象XmlBlock
:
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 方法焦辅,將布局文件讀取到內(nèi)存。走查到這里椿胯,有一件事可以確定筷登,即 “解析 xml 布局文件前需要進行 IO 操作,將其讀取至內(nèi)存中”哩盲。
解析布局文件
讀原碼就好像“遞歸”前方,剛才通過不斷地“遞”狈醉,現(xiàn)在通過“歸”回到那個關鍵方法:
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 操作將布局文件讀到內(nèi)存后,調(diào)用了inflate()
:
public abstract class LayoutInflater {
public View inflate(XmlPullParser parser, @Nullable ViewGroup root, boolean attachToRoot) {
synchronized (mConstructorArgs) {
...
try {
//'根據(jù)布局文件的聲明控件的標簽構建 View'
final View temp = createViewFromTag(root, name, inflaterContext, attrs);
ViewGroup.LayoutParams params = null;
//'構建 View 對應的布局參數(shù)'
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;
}
}
這個方法解析布局文件并根據(jù)其中聲明控件的標簽構建 View實例惠险,然后將其填充到 View 樹中苗傅。解析布局文件的細節(jié)在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()
的具體實現(xiàn)在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()創(chuàng)建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;
//'以布局文件中控件的名稱分別創(chuàng)建對應控件實例'
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()
接口中完成了控件轉(zhuǎn)換抱慌。
測量構建布局耗時
通過上面的分析逊桦,可以得出兩條結論:
1. Activity 構建布局時,需要先進行 IO 操作抑进,將布局文件讀取至內(nèi)存中强经。
2. 遍歷內(nèi)存布局文件中每一個標簽,并根據(jù)標簽名 new 出對應視圖實例寺渗,再把它們 addView 到 View 樹中匿情。
這兩個步驟都是耗時的!到底有多耗時呢信殊?
LayoutInflaterCompat
提供了setFactory2()
码秉,可以攔截布局文件中每一個 View 的創(chuàng)建過程:
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
中。
調(diào)用delegate.createView(parent, name, context!!, attrs!!)
须鼎,就是手動觸發(fā)源碼中構建布局的邏輯。
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()
//'執(zhí)行原方法'
val result = block()
return TimedValue(result, mark.elapsedNow())
}
public data class TimedValue<T>(val value: T, val duration: Duration)
方法返回一個TimedValue
對象茫虽,其第一個屬性是原方法的返回值,第二個是執(zhí)行原方法的耗時赡译。測試代碼中通過解構聲明
分別將返回值和耗時賦值給view
和duration
仲吏。然后把構建每個視圖的耗時累加打印。
了解了構建布局的過程蝌焚,就有了對癥下藥優(yōu)化的方向裹唆。
有了測量構建布局耗時的方法,就有了對比優(yōu)化效果的工具只洒。
限于篇幅许帐,構建布局耗時縮短 20 倍的方法只能放到下一篇了。