LayoutInflater原理分析與複雜佈局優化實踐
前言
Android佈局的加載默認是在主線程的,如果佈局太過複雜或者冗餘,則會影響頁面加載速度,降低UI線程的響應速度,進而讓用戶感覺卡頓,影響用戶體驗。當然,目前也有很多佈局優化方法。比如:儘量使佈局扁平化、merge標籤使用、ViewStub延遲化加載標籤、避免過度繪製等等。但是當所有技術都用上後,限於業務龐大,佈局確實複雜無法再優化,加載佈局的過程仍然很耗時,該怎麼辦呢,我們通過分析加載佈局的LayoutInflater類尋求解決方案。
LayoutInflater定義
在Android中,LayoutInflater大家一定不陌生,它就是Android的佈局加載器,它可以將xml佈局文件實例化爲相應的View對象。
獲取LayoutInflater:
LayoutInflater layoutInflater = (LayoutInflater) context
.getSystemService(Context.LAYOUT_INFLATER_SERVICE);
LayoutInflater layoutInflater = LayoutInflater.from(context);
第二種方式其實最終也是調用第一種方法,只是Android給我們做了一層封裝。
佈局加載過程分析
-
在獲取了LayoutInflater實例化對象之後,就可以調用inflate()來進行加載xml佈局了,可以看到有四個重載方法。
-
重載方法一:
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();
final XmlResourceParser parser = res.getLayout(resource);
try {
return inflate(parser, root, attachToRoot);
} finally {
parser.close();
}
}
-
重載方法四:
public View inflate(XmlPullParser parser, @Nullable ViewGroup root, boolean attachToRoot){
//方法體內容過多,不再貼出
}
可以看見,方法一調用了方法三,方法二和方法三最終都調用了方法四;而方法三中有個final XmlResourceParser parser = res.getLayout(resource);這個方法跟進去發現是從磁盤中讀取xml佈局文件並進行解析得到XmlResourceParser,可知此操作涉及IO,瞭解到它是個耗時操作。
由於方法四源碼過多,就不再一一貼出,後面我們都把源碼分析過程轉換成流程圖來呈現。
先來看下inflate方法調用流程:
inflate調用流程總結:如果是merge標籤直接調用rInflate方法,否則通過createViewFromTag方法先去創建這個佈局的根節點view,再將其作爲parent入參調用rInflateChildren方法,由於rInflateChildren內部調用了rInflate,而rInflate最終還是遍歷view樹循環調用createViewFromTag方法進行創建每一層級的view並將其作爲parent入參調用rInflateChildren。整個過程其實就是遍歷DOM樹進行遞歸創建view。
-
接下來再看下createViewFromTag方法調用流程圖:
-
createViewFromTag調用流程總結:如果mFactory2不爲null則通過調用mFactory2的onCreateView,否則如果mFactory不爲null則調用mFactory的onCreateView...如果工廠類都爲null,則調用LayoutInflater中的onCreateView方法,通過查看onCreateView方法源碼我們可以知道最終都是通過反射創建view的。
-
經過源碼分析可以發現整個佈局加載過程中主要耗時操作有兩點:
-
涉及IO讀取操作的xml佈局文件解析
-
通過反射的方式來遞歸創建 View 對象
比較容易想到的解決方法:
1. 直接動態的創建佈局,new出每個view對象,繞過xml解析和反射,但是這樣顯然很難維護且可讀性差,浪費了Android的xml可視化便捷佈局方式。
2. 將佈局的加載過程放到子線程處理,google其實提供了比較成熟方案,那就是v4包下的AsyncLayoutInflater,它是將LayoutInflater.inflater過程放到子線程來做。
AsyncLayoutInflater實踐
-
監控頁面加載耗時情況
首先針對我們頁面加載進行監控,結合業務代碼找到有些view加載比較耗時的地方,通過Android Studio自帶的Profiler監控可以很方便的查看各方法調用耗時與所佔比例,例如:
可以看到initStyleInfoView耗時61ms,經過查看代碼在頁面初次進入時,在此方法裏對一些必備的view進行了inflate加載。
-
嘗試使用異步加載佈局,例如:
/**
* 初始化style info的view
*/
private void initStyleInfoView() {
if (xxxViewA == null) {
new AsyncLayoutInflater(this).inflate(R.layout.xxx, null, new AsyncLayoutInflater.OnInflateFinishedListener() {
@Override
public void onInflateFinished(@NonNull View view, int i, @Nullable ViewGroup viewGroup) {
xxxViewA = view;
xxxViewA.init();
}
});
}
?
}
-
對一些佈局進行異步加載後再對應用耗時進行監控,該方法耗時截圖如下:
可以看到initStyleInfoView方法耗時降到1.5ms,其真正的耗時操作inflate已經放到了子線程處理。
-
優化前後目標幀時間監控對比
我們在手機開發者選項中打開GPU呈現模型分析,然後 通過adb shell dumpsys gfxinfo <包名>在終端打印幀時間日誌,如下圖:
將其複製到表格進行圖形化,方便我們分析數據,並對進行優化前後的效果做個對比。
優化之前每幀時間柱狀圖:
圖1
優化之後每幀時間柱狀圖
圖2
對比分析:我們知道Draw + Prepare + Process + Execute = 完整顯示一幀的時間,這個時間小於16ms才能保證理想的每秒60幀,所以從第一張優化之前的圖可以看出首次進入頁面請求數據回來後加載頁面時第41、42、44、46、47幀,共5幀時間超過了16ms,其他每幀均保持在16ms之下;我們優化之後,部分佈局加載放入了子線程中,減少了主線程的佈局加載耗時,圖2是優化之後的首次進入頁面每幀耗時統計,僅剩3幀時間超過16ms,優化了兩幀,通過優化前後的數據對比,可見其效果顯著。當然在我們的業務代碼中還有其他很多複雜的view,我們都可以不斷嘗試通過異步加載佈局來持續的優化,以不斷提升用戶體驗。
-
由此可見,對於業務量龐大,佈局複雜的頁面來說,異步加載佈局的確是個不錯的選擇。
AsyncLayoutInflater實現原理
概述:首先它會創建一個阻塞隊列,開啓一個子線程,當調用AsyncLayoutInflater的inflate佈局時會往阻塞隊列裏添加inflate任務,子線程再從隊列中取出inflate任務進行加載,加載完成後再通過handler轉換線程,將view回調到主線程。
-
先看下AsyncLayoutInflater的構造函數
public AsyncLayoutInflater(@NonNull Context context) {
mInflater = new BasicInflater(context);
mHandler = new Handler(mHandlerCallback);
mInflateThread = InflateThread.getInstance();
}
先創建一個BasicInflater對象,它繼承自LayoutInflater,只是重寫了onCreateView。
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) {
}
}
return super.onCreateView(name, attrs);
}
}
創建一個Handler,作用只是爲了切換線程。
創建一個InflateThread,從名字就看得出來它是一個子線程,用來加載佈局的線程。
-
AsyncLayoutInflater解析佈局的inflate方法
@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);
}
它通過mInflateThread獲取到InflateRequest任務對象後,設置必要參數後,加入任務隊列,等待子線程處理。
-
InflateThread代碼
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);
public void runInner() {
InflateRequest request;
try {
request = mQueue.take();
} catch (InterruptedException ex) {
return;
}
try {
request.view = request.inflater.mInflater.inflate(
request.resid, request.parent, false);
} catch (RuntimeException 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方法可以看出這個線程開啓一個while循環執行runInner方法,而此方法通過mQueue.take()從阻塞隊列ArrayBlockingQueue中取任務,進行佈局解析,解析完成後再通過handler發送消息到主線程。
-
AsyncLayoutInflate雖然看起來很不錯,但使用起來也遇到不少“坑”,如:
* 使用異步inflate,需要這個佈局的父佈局的generateLayoutParams函數是線程安全的。
* 異步加載的view中不能有創建Handler或者調用myLooper()的操作,原因通過上面源碼也知道,我們加載view是在子線程的默認沒有Looper.prepare。
* AsyncLayoutInflate是不支持設置Factory和Factory2的,這會導致有些佈局無法得到系統的兼容。
* 不支持加載包含Fragment的佈局。
* 從AsyncLayoutInflater源碼也可以看出其內部使用單線程來做所有的佈局加載工作,假如有很多任務,單線程可能不夠用;內部ArrayBlockingQueue阻塞隊列的默認大小隻有10,假如超過了10個任務,也會導致主線程的等待。
總的來說,AsyncLayoutInflater已經能滿足大部分需求,爲佈局優化提供了很好的支持,當然也可以根據自己的業務規模或者使用環境做一些定製化,針對AsyncLayoutInflater的缺點做一些相應的改進,比如可以自定義一個AsyncLayoutInflater,改造BasicInflater使其支持Factory2,進而得到系統的兼容;也可以引入線程池處理佈局加載任務,減少單線程的等待等等,以滿足自己的業務需求。相信後續google也會進一步優化AsyncLayoutInflater,使其功能更加完善,並且擁有更好的兼容性,能更好的滿足用戶需求。
參考文檔:
https://developer.android.google.cn/reference/androidx/asynclayoutinflater/view/AsyncLayoutInflater?hl=en
https://developer.android.google.cn/reference/kotlin/android/view/LayoutInflater?hl=en