得物佈局構建耗時最佳化方案實踐

架构师修行手册發表於2024-03-05


來源:得物技術

目錄

一、背景

二、現有方案

1. 掌閱 X2C

2. AsyncLayoutInflater

3. ViewCompiler

三、得物自研 X2C 框架實踐

1. View 構建流程解析

2. 外掛選型: APT or Gradle Plugin ?

3. 預載入

4. 構建執行緒優先順序調優

5. 多執行緒構建探索

四、線上效能收益

五、框架對比

六、結語

背景


當談到移動應用程式的體驗時,頁面啟動速度是其中至關重要的一點,更快的頁面展示速度確保應用程式可以迅速載入並響應使用者的操作, 從而提高使用者使用 App 時的滿意度。在頁面啟動的整個流程中,隨著 UI 複雜度的上升,佈局的 Inflate 耗時佔據了相當一部分關鍵的比例,本文分享得物自身在頁面佈局構建耗時最佳化方案上的探索歷程。


現有方案


在佈局構建耗時最佳化上,開源社群上有一些現成的方案可供參考,我們首先看下目前一些已知的技術方案。


掌閱X2C


掌閱的 X2C 方案開源於 2018 年,其透過 APT 在編譯期間對目標 XML 檔案進行解析,並翻譯成 XML View 樹結構對應的 Java 檔案。比如以下的佈局 XML 檔案。





















<?xml version="1.0" encoding="utf-8"?><RelativeLayout xmlns:android="  xmlns:app="  xmlns:tools="  android:layout_width="match_parent"  android:layout_height="match_parent"  android:paddingLeft="10dp">
<include android:id="@+id/head" layout="@layout/head" android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_centerHorizontal="true" />
<ImageView android:id="@+id/ccc" style="@style/bb" android:layout_below="@id/head" /></RelativeLayout>

轉換成 Java 檔案:


























public class X2C_2131296281_Activity_Main implements IViewCreator {  @Override  public View createView(Context ctx, int layoutId) {        Resources res = ctx.getResources();
RelativeLayout relativeLayout0 = new RelativeLayout(ctx); relativeLayout0.setPadding((int)(TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP,10,res.getDisplayMetrics())),0,0,0);
View view1 =(View) new X2C_2131296283_Head().createView(ctx,0); RelativeLayout.LayoutParams layoutParam1 = new RelativeLayout.LayoutParams(ViewGroup.LayoutParams.WRAP_CONTENT,ViewGroup.LayoutParams.WRAP_CONTENT); view1.setLayoutParams(layoutParam1); relativeLayout0.addView(view1); view1.setId(R.id.head); layoutParam1.addRule(RelativeLayout.CENTER_HORIZONTAL,RelativeLayout.TRUE);
ImageView imageView2 = new ImageView(ctx); RelativeLayout.LayoutParams layoutParam2 = new RelativeLayout.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT,(int)(TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP,1,res.getDisplayMetrics()))); imageView2.setLayoutParams(layoutParam2); relativeLayout0.addView(imageView2); imageView2.setId(R.id.ccc); layoutParam2.addRule(RelativeLayout.BELOW,R.id.head);
return relativeLayout0; }}

得物佈局構建耗時最佳化方案實踐

優點:

  • 效能高,沒有了載入 XML 的 IO 和遞迴解析過程

  • 避免了類反射構建的耗時

  • 基於 APT 直接生成 Java 檔案。

缺點:

  • View 相容性差,適配成本高,自定義 View 需要配置屬性對應的方法

  • 功能不完整,不支援 Merge 標籤,無法查詢系統 style,所以只支援應用內 style

  • 由於 APT 本身的特性,在 XML 發生變化時,對應註解處理器生成的 Java 構建檔案不會同步發生變, 對於不熟悉的同學來說容易踩坑。


AsyncLayoutInflater


AsyncLayoutInflater 是由 Android Google 官方提供的非同步 Inflate API,其主要思路是將 Inflate 操作放在非同步執行緒並行操作,從而讓主執行緒可以繼續執行一些其他的初始化操作,透過非同步回撥在相應的 Layout View 建立完成後,再設定到頁面上。

@Overrideprotected void onCreate(@Nullable Bundle savedInstanceState) {    super.onCreate(savedInstanceState);    new AsyncLayoutInflater(this).inflate(            R.layout.async_layout,            null,            new AsyncLayoutInflater.OnInflateFinishedListener() {                @Override                public void onInflateFinished(View view, int resid, ViewGroup parent) {                    setContentView(view);                }            }    );}

優點:

  • 將 UI 載入過程遷移到了子執行緒,保證了 UI 執行緒的高響應

  • 不存在 View 相容性問題。

缺點:

  • 有一定改造成本,在原有的頁面直接引入 AsyncLayoutInflater 進行改造時,由於從同步呼叫改成非同步回撥呼叫導致的邏輯結構變化容易引入 NPE 之類的風險

  • 內部依然存在部分 View 的反射需要建立的開銷。


ViewCompiler


Google 加入了一個 ViewCompiler,從原理來看是系統在安裝 APK 的時候自動對佈局檔案做的編譯最佳化,ViewCompiler 會將可最佳化的 XML 佈局轉化為程式碼構建的程式碼,並編譯成 Dex 檔案。

得物佈局構建耗時最佳化方案實踐

之後在程式執行時,首次使用 Infalter 類時,就會提前載入該 Dex 檔案。

得物佈局構建耗時最佳化方案實踐

之後在呼叫 Infalte 函式 Inflate相應佈局資源時,會嘗試呼叫最佳化後的 pacakgeme.CompileView 類的 Infalte 函式,直接生成對應的 View。

得物佈局構建耗時最佳化方案實踐

得物佈局構建耗時最佳化方案實踐

ViewCompiler 編譯 Layout 的原理其實和現有的 XML To Code 方案是類似的,都是解析 Layout XML 檔案,再根據 XML 節點資訊生產組裝 View 的程式碼。只不過在應用層我們的方案是提前編譯生成 Java 或 Class 檔案,而系統是直接編譯生成 Dex 檔案。

得物佈局構建耗時最佳化方案實踐

ViewCompiler 雖然在 Android Q Beta 2 的時候被新增進來,但到目前為止仍是一個實驗性質的東西,預設情況下應用程式都是無法使用到的。

得物佈局構建耗時最佳化方案實踐


得物自研X2C框架實踐


針對以上問題,我們決定構建得物自研的 X2C 框架。






















<?xml version="1.0" encoding="utf-8"?><merge xmlns:android="    xmlns:app="    app:x2c="standard"    android:layout_width="match_parent"    android:layout_height="match_parent"    android:orientation="vertical">
<TextView android:id="@+id/tv_1" android:layout_width="wrap_content" android:layout_height="wrap_content" android:text="測試一下效果" android:textSize="24sp" />
<com.shizhuang.x2c.CustomView android:layout_width="50dp" android:layout_height="50dp" android:layout_gravity="center" app:mixColor="black" /></merge>

生成 XML2Code 程式碼為:


































@X2CRes(R.layout.activity_test2)public class activity_test2 implements IViewFactory {  @Override  public View createView(    Context themeContext,    ViewGroup parentView,    boolean attachToParent,    AttributeSet parentAttributeSet  ) {    if (parentView == null) {      throw new X2CException("parentView is null when root is merge");    }    XmlResourceParser parser = themeContext.getResources().getXml(R.layout.activity_test2);    AttributeSetHelper.nextAttributeSet(parser);
AttributeSet attrs9 = AttributeSetHelper.nextAttributeSet(parser); TextView view9 = new TextView(themeContext, attrs9); parentView.addView(view9, parentView.generateLayoutParams(attrs9)); ViewAccessHelper.notifyFinishInflate(view9);
AttributeSet attrs16 = AttributeSetHelper.nextAttributeSet(parser); CustomView view16 = new CustomView(themeContext, attrs16); parentView.addView(view16, parentView.generateLayoutParams(attrs16)); ViewAccessHelper.notifyFinishInflate(view16);
return parentView; }
@Override public String layoutName() { return "activity_test2"; }}


View構建流程解析

生成 AttributeSet


在生產 AttributeSet 的探索上,我們首先研究系統的 LayoutInfalter 是如何生成 AttributeSet 的,透過對原始碼分析後發現,AttributeSet 是一個抽象介面,其唯一的直接子類是 XmlResourceParser。

得物佈局構建耗時最佳化方案實踐

系統的 LayoutInflater 的構建過程中,首先透過 Resources 生成對應佈局檔案的 XmlResourceParser。

得物佈局構建耗時最佳化方案實踐

由於 Parser 繼承自 AttributeSet,因此可以將 Parser 強轉為 Attributeset,之後先生成 RootView,再呼叫 RInfalteChildren 構建所有的子 View

得物佈局構建耗時最佳化方案實踐

而在子 View 的構建過程中,使用的還是一開始從 XmlPullParser 轉換的 AttributSet,這裡的 XmlPullParser 和 AttributeSet 其實是同一個物件,XmlPullParser 解析二進位制 XML 採用的是 SAX 方式,即邊讀邊解析, 透過不斷呼叫 Next 函式,在構建對應節點的 View 時,讀取當前的 AttributeSet 資訊。

得物佈局構建耗時最佳化方案實踐


建立 View 的方式


得物佈局構建耗時最佳化方案實踐

View 例項的建立有兩種方式:

第一種是類似掌閱 X2C 的方式, 直接呼叫目標 View(Context Context) 建構函式建立,此時還需要生成額外的屬性設定 API,如 SetWidth,對於自定義的屬性需要做專門的適配處理。

第二種是呼叫 View(Context Context, AttributeSet Attrs) 建構函式,LayoutInflater 內部解析 XML 並構建相應 View時,呼叫的就是這個建構函式。

因此,從相容性的角度上考慮,採用第二種方式構建更為合理,剩下的問題就轉化為如何生成對應佈局檔案中對應 View 的 AttributeSet。


生成 LayoutParams


AttributeSet 除了用於構建當前節點的 View 以外,還用於構建 LayoutParams。

得物佈局構建耗時最佳化方案實踐

LayoutParams 的構建同時還依賴於當前節點的夫容器 Parent,不同的容器生成不同的 LayoutParams,例如 FrameLayout.LayoutParams、LinearLayout.LayoutParams 等。


Merge 和 Include 標籤


Merge 標籤跟普通標籤的區別在於,Merge 標籤是一個虛擬根節點。Merge 是為了降低 View 巢狀層級設計的,所以 Merge 標籤為根節點的佈局是沒有根 View 的,所以也無法返回佈局根 View,只能將引數的 ViewParent 返回。

得物佈局構建耗時最佳化方案實踐

Merge 標籤需要搭配 Include 標籤使用,但是 Include 標籤卻並不是只能搭配 Merge 標籤。所以在解析 Include 標籤的 Layout 的時候,我們並不知道包含過來的是普通佈局還是 Merge 佈局。

得物佈局構建耗時最佳化方案實踐

但是普通佈局和 Merge 佈局的實現並不一樣。

對於 Include 普通佈局,邏輯要複雜的多。Include 標籤本身有 AttributeSet 資訊,包含的佈局根節點也有 AttributeSet 資訊,應該使用哪個呢?構建根 View 的時候,使用根節點的 AttributeSet,但是在 View 構建完成後,需要將 Include 標籤屬性中的 Android:ID 和 Android:visiablity 屬性賦值給根 View。

得物佈局構建耗時最佳化方案實踐

在生成根 View 的 LayoutParams 的時候,優先使用 Include 標籤的 AttributeSet,如果生成失敗再使用根節點的 AttributeSet。

得物佈局構建耗時最佳化方案實踐


外掛選型:APT or Gradle Plugin?

APT 方式的問題


在 XML 生成程式碼構建的實踐過程中,我們一開始也是採用的掌閱 X2C 的方案,在業務程式碼中插入如下註解,用於標記需要轉換成 Java 的 XML 檔案,在各業務模組中註冊註解處理器,直接生成對應的 Java 原始碼。

@Xml("activity_test2")

最後發現這樣的方式會帶來不小的問題:

  • APT 的編譯 Target 是 Java 原始碼, 所以在只有 XML 檔案變更時,並不會自動重新生成新的 Java 佈局程式碼。這樣一次 XML 修改,在轉換成 Java 程式碼的時候,就被編譯系統忽略了。

  • 使用 XML 註解標註檔名的方式,並沒有讓註解跟檔案本身繫結。當檔案改名的時候,這個註解並不能感知,檔案的修改者也無法感知到有這麼一個跟檔案沒有直接關係的檔名註解。

  • 得物採用的是多倉庫多模組開發,殼工程引入子工程的依賴,最後是以 AAR 二進位制依賴的方式進行構建。每個模組接入的 X2C 外掛版本不同,因此構建出的產物也不同,這會導致 X2C 版本碎片化嚴重。容易出現生成之前生成的 View 構建程式碼和最新的執行時 X2C-SDK 不相容的問題,也增加了 X2C-SDK 後續升級過程中的維護困難。


使用 AGP 統一構建


我們最終採用的透過 AGP 外掛,在殼工程對所有目標 XML 進行統一構建的方式。

得物佈局構建耗時最佳化方案實踐

在 Android 工程的編譯過程中,ProcessResources 任務將所有依賴的模組的資源進行處理,生成 Resources.ap_ 檔案和 R 檔案。Resources.ap_ 是資源壓縮包,裡面的 XML 資源是已經被編譯成二進位制格式的資源。

X2C-AGP 的核心功能主要有兩部分:

  • GenerateJavaTask 是將 XML 佈局檔案 轉換成 Java 佈局程式碼

  • ExcludeTransform 後續介紹

我們約定當佈局 XML 檔案中,新增了自定義屬性 app:x2c 表示該檔案需要進行 X2C 構建程式碼生成。GenerateJavaTask 任務遍歷 Resources.ap_ 檔案,將包含該自定義屬性的佈局檔案轉換成 Java 程式碼。還生成了 Resource ID 到 Java 佈局類的對映關係。

public class X2CResPool {  public static IViewFactory getFactoryBy(int layoutRes) {    switch(layoutRes) {      case R.layout.activity:        return new activity();      case R.layout.activity_main_du2:        return new activity_main_du2();      case R.layout.activity_test1:        return new activity_test1();      case R.layout.activity_test2:        return new activity_test2();      case R.layout.merge_activity:        return new merge_activity();      default:        return null;    }  }}

殼工程透過任務 GenerateJavaTask 將二進位制 XML 佈局檔案,轉換成 Java 佈局程式碼時。Java 佈局程式碼中使用了很多自定義 View。這些自定義 View 是在業務模組中定義的,而在殼工程的 App 模組中,由於並沒有顯示申明對應 View 的模組依賴,會導致編譯 Java 佈局檔案時出現類未找到的問題,導致編譯失敗。而如果人手動去解決該問題,為 App 模組新增相應 View 的模組依賴,顯得較為繁瑣。每次增加一個需要支援 X2C 的 XML 檔案的時候,都需要增加殼工程的工程依賴關係,且自定義 View 到底在哪個模組也不不是這麼一目瞭然。

一個解決方案是不再生成 Java 原始碼,直接生成 Java 位元組碼,這樣可以繞過編譯依賴。直接生成位元組碼的方案增加了專案的升級和維護成本,且不便於業務側同學驗證生成的 Java 佈局程式碼是否正確。

另一種方案是在殼工程重新實現一次依賴的自定義 View,這樣就造成了 APK 中會有重複的類,所以需要在 Transform 階段將重複的 View 去掉,ExcludeTransform 就是完成這個任務的。殼工程中實現的自定義 View 會有 @X2CResTemp 註解,在 ExcludeTransform 中,透過 ASM 遍歷工程中所有位元組碼,將有 @X2CResTemp 註解的類從編譯系統中刪除。

如何在殼工程中實現依賴的自定義 View 呢,觀察生成的 Java 程式碼,會發現我們只用了自定義 View 的建構函式,並不需要實現一個完整的自定義 View,只要有建構函式,就可以在編譯階段透過了。











@X2CResTemppublic class CustomView extends ViewGroup {  public CustomView(Context context, AttributeSet attributeSet) {    super(context, attributeSet);  }
@Override protected void onLayout(boolean changed, int l, int t, int r, int b) { }}


預載入


最佳化佈局的載入效能,除了 X2C 方案以外,預載入是一個效果更為顯著的功能。在 Androidx 中已經有提供了 AsyncLayoutInflater 用於進行 XML 的非同步載入,在這個類基礎上可以封裝一個非同步預載入工具,但是實際使用下來會發現直接使用 AsyncLayoutInflater 很容易出現鎖的問題,甚至導致了更多的耗時。透過分析我們發現,這是因為在 LayoutInflate 中存在著物件鎖,並且即使透過構建不同的 LayoutInflate 物件繞過這個物件鎖,在 AssetManager 層、Native 層仍然會有其他鎖。


預載入時機


佈局預載入存在於兩個時機:

  • App 啟動時,Application 的 OnCreate 階段,可以對首頁佈局進行預載入

  • 開啟新的 Activity 前,預載入這個 Activity 的佈局

在 App 啟動階段對主頁的佈局檔案進行預載入,統一放到啟動任務載入中去做。新的 Activity 啟動之前,如何做佈局預載入呢?開啟新的 Activity 的場景可能十分多,難道需要在每個 StartActivity 呼叫之前都插入一段預載入佈局的程式碼嗎,且開啟新的 Activity 的地方需要能獲取佈局資源 ID。

答案是跟路由結合在一起,ARouter 提供了路由攔截器,不同的業務模組,可以在模組中使用註解註冊一個 ARouter 路由攔截器,並在攔截器中自定義自身模組內頁面的預載入策略,如下:

@Interceptor(priority = 1)class X2CPreloadInterceptor : IInterceptor {    private lateinit var applicationContext: Context    override fun init(context: Context) {        applicationContext = context.applicationContext    }    override fun process(postcard: Postcard, callback: InterceptorCallback) {        if (postcard.path == CommunityRouterTable.FEED_DETAILS_PAGE) {            X2CUtil.preload(applicationContext, R.layout.du_trend_detail_fragment_trend_details_tab)            X2CUtil.preload(applicationContext, R.layout.du_trend_detail_fragment_trend_details)        }        callback.onContinue(postcard)    }}

所有開啟新 Activity 的場景都需要使用路由,所以在路由攔截器中能收斂開啟新 Activity 的場景。


Context 及主題適配


對 Activity 的佈局檔案進行預載入的時候,Activity 還沒有建立,所以我們無法拿到 Activity 的 Context。但是構建 View 需要 Context,所以我們使用 Application 的 Context 代替。但是很多業務側拿著 View 的 Context 當 Activity 用的場景,為了相容這種場景,所以在預載入的 View 被新增到 ViewTree 前需要將 ApplicationContext 替換成 Activity 的 Context。

得物佈局構建耗時最佳化方案實踐

View 沒有提供替換 Context 的 API,所以使用反射替換 mContext 成員的值。

得物佈局構建耗時最佳化方案實踐

如此這般,大部分場景下已經沒有什麼問題了,但是仍然遇到了新的問題

java.lang.IllegalArgumentException: The style on this component requires your app theme to be Theme.AppCompat (or a descendant).    at com.shizhuang.x2c.task.DeferredRunnable.run(deferred.kt:56)    at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1167)    at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:641)    at com.shizhuang.duapp.common.base.delegate.tasks.optimize.startupoptimize.DefaultThreadFactory$newThread$1.run(X2CInitTask.kt:82)    at java.lang.Thread.run(Thread.java:920)Caused by: java.lang.IllegalArgumentException: The style on this component requires your app theme to be Theme.AppCompat (or a descendant).    at com.google.android.material.internal.ThemeEnforcement.checkTheme(ThemeEnforcement.java:243)    at com.google.android.material.internal.ThemeEnforcement.checkAppCompatTheme(ThemeEnforcement.java:213)    at com.google.android.material.internal.ThemeEnforcement.checkCompatibleTheme(ThemeEnforcement.java:148)    at com.google.android.material.internal.ThemeEnforcement.obtainStyledAttributes(ThemeEnforcement.java:76)    at com.google.android.material.tabs.TabLayout.<init>(TabLayout.java:474)    at com.google.android.material.tabs.TabLayout.<init>(TabLayout.java:454)    at com.shizhuang.x2c.res.layout.du_trend_detail_fragment_trend_details_tab.createView(du_trend_detail_fragment_trend_details_tab.java:88)    at com.shizhuang.x2c.inflate.IViewFactory$DefaultImpls.createView$default(IViewFactory.kt:9)    at com.shizhuang.x2c.X2CUtil$preload$3$1$1.invoke(X2CUtil.kt:150)    at com.shizhuang.x2c.X2CUtil$preload$3$1$1.invoke(X2CUtil.kt:149)    at com.shizhuang.x2c.task.DeferredRunnable.run(deferred.kt:49)    ... 4 more

這是因為佈局使用了跟主題相關的內容,Application 的 Context 沒有主題資訊,所以預載入的 Context 需要加上佈局檔案所屬的 Activity 的主題,如下:

X2CUtil.preload(applicationContext.withTheme(R.style.FeedDetailsActivity), R.layout.du_trend_detail_fragment_trend_details_tab)


構建執行緒優先順序調優


在框架開發完成後,我們在得物首頁場景下進行了框架接入,在 Application 的 onCreate 階段對 HomeActivity 的佈局進行了相應佈局的預載入。對預載入進行線下測試,線下資料表現較好。在開啟預載入的時候,秒開資料顯著好於無預載入場景。然而預載入功能上線後,線上 AB 統計的平均耗時資料確令人不解,在開啟預載入情況下,首頁佈局載入耗時竟然大於無預載入情況,分析了樣本資料後,發現在非同步執行緒構建存在的異常耗時樣本遠遠多於在主執行緒構建的樣本數量。

得物佈局構建耗時最佳化方案實踐

我們線上下針對線上容易出現異常耗時的裝置進行了複測,發現確實存在類似的情況,此時我們聯想到 Android 系統在對 SharedPrefenrece 做的一個最佳化,由於非同步執行緒的優先順序預設比主線低,因此在 Activity onStop 的時候,系統會把非同步執行緒 SP 未完成同步的任務直接取出到主執行緒執行,非同步構建是不是也是由於執行緒優先順序導致非同步構建時無法獲取到充足的 CPU 時間片導致的,最終我們線上下列印了主執行緒和非同步執行緒執行時獲取的 CPU 時間片佔比,驗證了該猜想。

得物佈局構建耗時最佳化方案實踐

可以看到,雖然提前進行了非同步構建的工作,但是到頁面需要使用對應 View 的時候,非同步構建的任務還沒有完成,因此主執行緒只能進行等待,並且由於非同步執行緒優先順序較低,出現了一個高優先順序的執行緒等待另一個低優先順序執行緒的情況,並且優先順序導致的時間片分配的原因,這裡的等待其實不如直接在主執行緒直接重新構建。非同步 View 構建執行緒其實是為主執行緒服務的,我們需要提高對應工作執行緒的優先順序。

Android 設定執行緒優先順序的方法有兩種:

  • Java API 使用 Thread 類的 setPriority,值為 0~10,值越大,優先順序越高,所能獲取的時間片越多。

  • Android 系統使用 Process 類的 setThreadPriority,值為 -20~20,值越小,優先順序越高,所能獲取的時間片越多。

在 Android 中,無論透過什麼方式設定的執行緒優先順序,其實本質上都是透過 Native 層,設定 Nice 的值來實現的。執行緒優先順序必須線上程建立成功後,才能設定,因為執行緒建立完成後,才能拿到執行緒 ID。注意 Thread 的 Start 方法執行後,執行緒不一定建立完成,Thread 的 Runnable 開始執行才能認為建立完成。

執行緒預設優先順序為 0,主執行緒預設為-20,部分 ROM 的主執行緒預設-10。我們將預載入執行緒優先順序提升為-16











private class DefaultThreadFactory : ThreadFactory {    private val number = AtomicInteger(0)
override fun newThread(r: Runnable): Thread { return Thread(null, Runnable { Process.setThreadPriority(-16) r.run() }, "X2C-Thread${number.incrementAndGet()}") }}

經過調整後,效能提升顯著,在對應頁面需要獲取 View 時,非同步任務基本已經提前完成。

得物佈局構建耗時最佳化方案實踐


多執行緒構建探索


預設情況下,一個 View 樹的構建是單執行緒的,即總是從 ViewRoot 層級向下構建,無論採用現有的哪種方案,最終構建的總耗時總是大於每個 View 構建耗時之和,無法利用多執行緒的優勢縮減 View 構建耗時。

得物佈局構建耗時最佳化方案實踐

為了進一步提升預載入的效率,我們考慮使用多執行緒對預載入進行效能提升。佈局的載入受限於 XML 的解析,XML 的解析只能使用單執行緒。對二進位制 XML 檔案格式進行研究,看看是否有進一步最佳化的可能性。


自己生成 AttributeSet


透過 XmlResourceParser 獲取 AttributeSet 是實現成本較低的方式,但它存在以下問題:

  • 仍需要 XML 檔案的存在,透過 Resource 讀取二進位制 XML 資源,涉及到一部分檔案 IO

  • XmlResourceParser 對 XML 檔案讀取是 Pull 模式,如果我們計劃對 ViewTree 的構建過程進行多執行緒構建最佳化,無法直接獲取對應節點的 AttributeSet 資訊。

因此,我們進行了自己生成 AttributSet 的探索,首先,XmlBlock 的生成,除了類似 LayoutInflater 構建過程中直接傳入 LayoutID 的方式(如下)

得物佈局構建耗時最佳化方案實踐

也可以直接傳入對應的 Byte[] 進行生成, 因此,我們如果可以直接生成 XML 檔案中各個 View 屬性資訊對應的二進位制檔案,就可以直接透過 XMLBlock 構建對應的 AttributeSet。

得物佈局構建耗時最佳化方案實踐

得物佈局構建耗時最佳化方案實踐


二進位制 XML 重組


二進位制的 XML 檔案其內容結構如下:

得物佈局構建耗時最佳化方案實踐

二進位制 XML 有以下 6 部分組成:

  1. 檔案頭

  2. 字串常量池

  3. 系統資源 ID 池

  4. Start NameSpace Chunk

  5. 巢狀的節點 Chunks

  6. End NameSpace Chunk

二進位制 XML 保留了文字 XML 中節點的巢狀結構關係。XML 的節點之間除了用巢狀結構來描述父子關係外,父子之間沒有資訊依賴,子節點的解析不依賴於任何父節點資訊。父子節點的資訊解析是可以完全獨立的,所以我們在解析檔案之前,將完整的 XML 檔案按節點拆成每個 N 個獨立的檔案,檔案格式如下:

  1. 檔案頭

  2. 字串常量池

  3. 系統資源 ID 池

  4. Start NameSpace Chunk

  5. 節點 Chunk

  6. End NameSpace Chunk

得物佈局構建耗時最佳化方案實踐

檔案重組後,每個檔案的 File Size 欄位需要重新計算。二進位制資料儲存在程式碼中,用函式分割儲存。

class activity_test2 {  public byte[] xmlHeader() {    return new byte[] {FileHeader+StringPool+ResourcesIdPool+StartNamespaceChunk}  }    public byte[] tag1() {    return new byte[] {StartTagChunk:LinearLayout+EndTagChunk:/LinearLayout}  }    public byte[] tag2() {    return new byte[] {StartTagChunk:TextView+EndTagChunk:/TextView}  }    public byte[] tag3() {    return new byte[] {StartTagChunk:Button+EndTagChunk:/Button}  }    public byte[] xmlEnd() {    return new byte[] {EndNamespaceChunk}  }}


留待進一步


多執行緒載入方案對單個 XML 的預載入效能有所提升,但是因為預載入主要是在 App 啟動的時候使用,這個時候影響效能的並不是執行緒不夠,而是 CPU 效能不夠。同時 App 啟動階段預載入的資源不是隻有一個,而是多個。多執行緒主要是拉平了各個執行緒的算力消耗。

得物佈局構建耗時最佳化方案實踐

實現多執行緒方案,也引入了新的問題:

  • 讓 X2C 的實現變的複雜了,相容多執行緒方案的實現效能相對不相容多執行緒的下降了。

  • 多執行緒方案依賴於對二進位制 XML 進行重組,程式碼中多複製了一份資源。


線上效能收益


以首頁的啟動速度為例。

這裡的啟動速度標準是,從首頁Actiivty 的 onCreate 開始執行到 onResume 函式執行結束。

  • LOCAL: 表示未做任何最佳化的資料 ,平均耗時 292ms。

  • X2C: 未做預載入,但使用了X2C的infalte構建, 平均耗時 267ms。

  • CACHE: 進行了提前預載入,平均耗時 216ms。

得物佈局構建耗時最佳化方案實踐

以 社群容器 頁面的啟動速度為例。

  • LOCAL: 平均耗時 293ms。

  • X2C: 平均耗時 210ms。

  • CACHE: 平均耗時 150ms。

得物佈局構建耗時最佳化方案實踐


框架對比


得物佈局構建耗時最佳化方案實踐


結論


透過實踐上述最佳化方案,可以顯著減少佈局構建的耗時,提高應用的效能和使用者體驗。本次專案經過三輪的最佳化迭代,整個技術迭代過程中,一個核心的理念就是資料驅動,一切的最佳化都要以資料的提升來作為標準,遇到問題解決問題。

本次技術最佳化最初的切入點是 XML2Code,但是在進行線上驗證後,發現僅僅只是 XML2Code 並不能達成我們預期的結果。於是整個專案迴歸到了更高層級的目標上 —— 最佳化佈局構建耗時。為了進一步最佳化佈局構建的耗時,預載入、多執行緒構建,可謂“無所不用其極”,最後達成預期結果。

所以盯住結果,不要拘泥於什麼具體的技術!



來自 “ ITPUB部落格 ” ,連結:https://blog.itpub.net/70027824/viewspace-3008069/,如需轉載,請註明出處,否則將追究法律責任。

相關文章