1.車系頁佈局渲染現狀 車系頁是重要的車系信息頁面,更新迭代多年,頁面佈局不斷變化,xml佈局文件越寫越複雜。 獲取車系頁佈局文件耗時: startTime = System.currentTimeMillis(); setContentView(R.layout.car_series_revisi ...
1.車系頁佈局渲染現狀
車系頁是重要的車系信息頁面,更新迭代多年,頁面佈局不斷變化,xml佈局文件越寫越複雜。
獲取車系頁佈局文件耗時:
startTime = System.currentTimeMillis();
setContentView(R.layout.car_series_revision_activity);
long durTime = System.currentTimeMillis() - startTime;
LogHelper.e("佈局總耗時","車系頁佈局耗時:" + durTime);
結果如下:
2.卡頓的原因
2.1
Android繪製原理
► 1.Android的屏幕刷新中涉及到最重要的三個概念
(1)CPU:執行應用層的measure、layout、draw等操作,繪製完成後將數據提交給GPU
(2)GPU:進一步處理數據,並將數據緩存起來
(3)屏幕:由一個個像素點組成,以固定的頻率(16.6ms,即1秒60幀)從緩衝區中取出數據來填充像素點
總結一句話就是:CPU 繪製後提交數據、GPU 進一步處理和緩存數據、最後屏幕從緩衝區中讀取數據並顯示。
► 2.雙緩衝機制
當佈局比較複雜,或設備性能較差的時候,CPU並不能保證在16.6ms內就完成繪製數據的計算,所以這裡系統又做了一個處理。
當你的應用正在往Back Buffer中填充數據時,系統會將Back Buffer鎖定。
如果到了GPU交換兩個Buffer的時間點,你的應用還在往Back Buffer中填充數據,GPU會發現Back Buffer被鎖定了,它會放棄這次交換。
這樣做的後果就是手機屏幕仍然顯示原先的圖像,這就是我們常常說的掉幀。
2.2
佈局載入原理
頁面啟動時,佈局載入在主線程上進行耗時操作,會導致頁面渲染及載入慢。
佈局載入主要通過setContentView來實現,下麵是它的調用時序圖:
我們可以看到,在setContentView中主要有兩個耗時操作:
(1)解析xml,獲取XmlResourceParser,這是IO過程。
(2)通過createViewFromTag,創建View對象,用到了反射。
以上兩點就是佈局載入慢的原因,也是佈局的性能瓶頸。
3.佈局載入優化
上一章分析了佈局載入慢的主要原因,因此,我們的優化方式主要有以下兩種:
(1)非同步載入,將佈局載入過程轉移到子線程
(2)去掉IO和反射過程
3.1
非同步載入,AsyncLayoutInflater方案
setContentView 預設是在UI主線程載入佈局的,其載入過程中的耗時操作,如解析xml,反射創建view對象等也是在主線程執行,AsyncLayoutInflater 可以讓這些載入過程在子線程中執行,這樣可以提高UI線程的響應性,UI線程同時可以進行其他操作。AsyncLayoutInflater使用方式如下:
new AsyncLayoutInflater(this).inflate(R.layout.car_series_revision_activity, null, new AsyncLayoutInflater.OnInflateFinishedListener() {
@Override
public void onInflateFinished(@NonNull View view, int resid, @Nullable ViewGroup parent) {
setContentView(view);
}
});
AsyncLayoutInflater方案的缺點:
(1) UI佈局和view的初始化在子線程中進行,如果view還未初始化成功,在主線程中再調用view會引起崩潰。
(2) 一般情況下,主線程會調用view,涉及到大量子線程和主線程在view調用上的同步問題,這就犧牲了易用性,代碼可維護性也會變差。
(3) 如果是在老頁面邏輯結構上引入AsyncLayoutInflater進行改造,結構改動很大,很容易發生view調用崩潰錯誤,不太可行。
3.2
X2C方案
X2C 是掌閱開源的一套佈局載入框架。X2C的主要思路是利用apt工具,在編譯期將我們寫的xml佈局文件解析成view,並根據xml動態設置view的各類屬性,這樣,我們在運行時,調用findViewById,根據view id拿到的view,已經是直接new 出來的view,避免了運行時的xml IO操作和反射操作,這就解決了佈局時的耗時問題。
原始的xml佈局文件:
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:id="@+id/constraintLayout"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:gravity="center"
android:orientation="vertical">
<Button
android:id="@+id/x2c"
style="@style/btn"
android:text="X2C" />
<Button
android:id="@+id/xml"
style="@style/btn"
android:layout_marginTop="10dp"
android:text="XML" />
<Button
android:id="@+id/sub"
style="@style/btn"
android:layout_marginTop="10dp"
android:text="subModule" />
</LinearLayout>
public class X2C127_Activity implements IViewCreator {
@Override
public View createView(Context ctx) {
Resources res = ctx.getResources();
LinearLayout linearLayout0 = new LinearLayout(ctx);
linearLayout0.setTag(R.id.x2c_rootview_width,ViewGroup.LayoutParams.MATCH_PARENT);
linearLayout0.setTag(R.id.x2c_rootview_height,ViewGroup.LayoutParams.MATCH_PARENT);
linearLayout0.setId(R.id.constraintLayout);
linearLayout0.setGravity(Gravity.CENTER);
linearLayout0.setOrientation(LinearLayout.VERTICAL);
Button button1 = new Button(ctx);
LinearLayout.LayoutParams layoutParam1 = new LinearLayout.LayoutParams((int)(TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP,150,res.getDisplayMetrics())),(int)(TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP,50,res.getDisplayMetrics())));
button1.setBackgroundColor(res.getColor(R.color.colorAccent));
button1.setTextSize(TypedValue.COMPLEX_UNIT_DIP,20);
button1.setGravity(Gravity.CENTER);
button1.setTextColor(Color.parseColor("#ffffff"));
button1.setId(R.id.x2c);
button1.setText("X2C");
button1.setLayoutParams(layoutParam1);
linearLayout0.addView(button1);
return linearLayout0;
}
}
X2c的優點:
(1)易用性和可維護性好,對原有代碼侵入性不強,應用代碼還是使用xml寫佈局
(2)載入耗時可縮短到原來的1/2到1/3
X2c的缺點:
(1)View的屬性支持不完全
(2)相容性和穩定性不是很高,在高版本的gradle 編譯工具,如gradle3.1.4,會出現找不到R.java文件,找不到xml對應的java文件等問題
(3)目前,X2C更新到2021年,並沒有持續維護和解決issue
3.3
Compose方案
Compose 是 Jetpack 中的一個新成員,是 Android 團隊在2019年I/O大會上公佈的新的UI庫。
Compose使用純kotlin開發,使用簡潔方便,但它是完全拋棄了View 和 ViewGroup這套系統,自己把整個的渲染機制從裡到外做了個全新的,是未來取代XML的官方方案。
Compose的優點:
(1)使用聲明式UI,摒棄了xml佈局運行時解析,佈局效率更高
(2)使用kotlin開發,簡單易用,佈局形式上跟flutter統一。
如果是使用kotlin開發的新項目,可以引入Compose方案,對於老項目的優化,Compose方案並不適用。
3.4
我們的優化方案-在佈局反射上做文章
Xml解析到view,完全自己來做,比較複雜且有很多風險,這個過程涉及到兩個耗時的點:
(1)xml解析,IO操作
(2)反射
xml解析這部分工作複雜度很高,可以交給android系統來做。我們可以想辦法去除反射的邏輯。
我們需要找到一個反射生成view的入口。我們知道,View生成相關邏輯在LayoutInflater的createViewFromTag中,調用了onCreateView(parent, name, context, attrs),通過反射生成了view。
通過android系統的LayoutInflater setFactory,我們不僅可以控制View的生成,還可以把View變成另外一個View。在setFactory的onCreateView(parent, name, context, attrs)回調中,我們接管單個view的生成,去掉反射,new 出我們自己的view就解決了問題。而onCreateView(parent, name, context, attrs)中的參數name返回的就是xml中使用到的view的名字,根據這個name,直接new出來新的view。方式如下:
LayoutInflaterCompat.setFactory(getLayoutInflater(), new LayoutInflaterFactory() {
@Override
public View onCreateView(View parent, String name, Context context, AttributeSet attrs) {
switch (name) {
case "TextView":
return new TextView(context, attrs);
case "ImageView":
return new ImageView(context, attrs);
case "com.cubic.choosecar.ui.car.view.NewStarView":
return new com.cubic.choosecar.ui.car.view.NewStarView(context, attrs);
case "com.cubic.choosecar.ui.carseries.scrolllayout.ScrollableLayout":
return new com.cubic.choosecar.ui.carseries.scrolllayout.ScrollableLayout(context, attrs);
case "View":
return new View(context, attrs);
case "com.cubic.choosecar.newui.carseries.view.CarRevisionSeriesImageScaleLayout": //自定義view
return new com.cubic.choosecar.newui.carseries.view.CarRevisionSeriesImageScaleLayout(context, attrs);
case "ViewStub":
return new ViewStub(context, attrs);
case "ScrollView":
return new ScrollView(context, attrs);
case "androidx.constraintlayout.widget.ConstraintLayout":
return new androidx.constraintlayout.widget.ConstraintLayout(context, attrs);
case "FrameLayout":
return new FrameLayout(context, attrs);
case "RelativeLayout":
return new RelativeLayout(context, attrs);
case "androidx.appcompat.widget.Toolbar":
return new androidx.appcompat.widget.Toolbar(context, attrs);
case "LinearLayout":
return new LinearLayout(context, attrs);
default:
View view = getDelegate().createView(parent, name, context, attrs);
return view;
}
//return view;
}
});
包括系統view和我們自定義的view。
此方案對已有項目的代碼侵入性很小,改造成本低,相容性也很高,相對來講,在渲染效率上比X2C方案低一些,但比較匹配我們對已有舊項目複雜佈局的渲染優化。
3.5
進一步在佈局上優化
我們可以使用viewStub實現佈局的懶載入。思路是將佈局分成不同的模塊,讓部分模塊使用viewStub標簽替代,一半屏幕的模塊元素渲染完成以後,再通過viewStub來渲染生成viewStub所包含的其它模塊,實現延遲渲染載入。
通過分析車系頁佈局,已經將佈局元素,按功能做了一些模塊的劃分,我們進一步將關聯度大的佈局模塊集中在一起,封裝在一個自定義VIEW中,使用viewStub包含替換這些模塊View。UI線程setContentView渲染佈局時,viewStub所包含的模塊並不會被渲染,只會渲染屏幕的部分元素,等待主介面數據返回,再使用viewStub延遲其它模塊,實現了佈局的懶載入,加快了主線程的渲染速度。
4.優化結果
通過3.4和3.5節的優化方法,車系頁複雜佈局渲染優化對比結果如下:
通過對比可以看到,在不同檔次的android機型上,渲染耗時降低了20%-35%左右,在低端機型上,減少的絕對耗時更多,感受可能會明顯一些。
作者|蔣雄鋒
本文來自博客園,作者:古道輕風,轉載請註明原文鏈接:https://www.cnblogs.com/88223100/p/Optimization-Practice-for-Android-Page-Rendering-Efficiency.html