一、背景
Android 在 View 的使用中,過多的佈局文件 inflate 影響性能,尤其在一些滾動列表、樣式種類很豐富的場景下,inflate 次數相對較多,整體 inflate 耗時就會增加,導致滾動過程卡頓。
所以,需要 View 的異步 inflate,甚至 View 的全局緩存,通過這些方式,去減少 UI 線程 inflate 的耗時及次數,以便減少卡頓,提升性能。
二、現有的解決方案
要實現 View 的異步 Inflate,最簡單粗暴的方式是直接 new Thread 執行 inflate,或者使用 HandleThread+Handler 的方式,或者使用 android 官方 android.support.v4.view 包的 AsyncLayoutInflater 類。但都存在一個問題,就是如果 inflate view 實例的時候 view 的構造方法裏面直接有 new Handler()(原本想新建一個主線程 Handler 用於 UI 操作),將會達不到要求。
ListView、RecycleView 等控件,自身實現了“緩存複用”機制,這使得滑動過程性能得以提升,但更多的是控件間、或者頁面內的緩存複用,而對於頁面之間(activity 與 activity)緩存複用,沒一個有效的解決方案。當然,如果把 View 的緩存做成全局單例,是可以做到頁面之間的緩存複用,但 View 和 context 強綁定在一起,全局緩存可能會導致 activity 實例不能正常釋放,導致內存泄露問題;也可能在 context 使用上存在問題,如用 APPlication 實例傳遞給 View 的 context,代碼中又直接把 context 當成 activity 用,將導致一些問題。
三、異步 Infllate+全局緩存問題分析及解決方案
從現有方案中,提煉出兩個核心問題,一、異步 Inflater 的時候,View 中 new Handler 導致的安全問題;二、全局緩存,View 的 context 問題。
- 異步 Inflater 的時候,View 中 new Handler 導致的安全問題解決方案
對於直接 new Thread 執行 inflate,或者使用 HandleThread+Handler 的方式,或者使用 android 官方 android.support.v4.view 包的 AsyncLayoutInflater 類。
示例代碼:
三種方式的對比:
第一種和第三種方式,run 方法中由於沒有使用 Looper.loop 機制,這使得 new Handler 即使調用 post 方法發消息,並不會正在執行,導致 UI 不能正常刷新。
第二種,雖然 new Handler 能正常工作,但如刷新 UI,很可能會 crash,比如如下的異常:
所以 Handler hander = new Handler(),往 handler 拋的消息需要拋到主線程,比如改寫成 Handler hander = new Handler(Looper.getMainLooper())。但是我們無法更改 View 的 Handler 構造代碼,下面方案通過了反射的方式,強制把後臺的線程的 Looper 設置爲 mainLooper,這樣後臺線程 new Handler()方式也能把消息拋到主線程消息隊列
使用示例:
- 全局緩存,View 的 context 問題解決方案
在全局緩存時,爲了解決創建 view 的 context 不一定是 activity 導致的問題,或者是 activity 導致的內存泄露問題,對 Context 做封裝:新建了 ViewContext 代理類:
inflate 的時候,將用 ViewContext 傳入 View,方式如下:
四、基本實施及使用
- 基本架構
- 實施思路
1)View 的緩存大小應控制,且可動態修改:在 View 的緩存方面,設計一個緩存大小機制,且允許動態的修改對應的緩存大小,這樣可以根據具體需要設置,從而更好的控制內存使用;
2)緩存 View 的狀態處理,方便管理。異步創建 View,放入緩存池並標記可用,每次從緩存池獲取 View 後,標記狀態不可用,待回收後在標記可用;
3)異步創建 View,可預加載。提供 View 的異步創建,並放入緩存中,結合預加載,能有效的減少實際創建 View 所需的耗時,提升性能;
4)內存管理策略–應用低內存自動釋放緩存。通過 context 取到 APPlication 註冊一個 ComponentCallbacks,監聽 APP 內存狀態,適當的釋放緩存;
5)緩存優先級策略–可設置緩存釋放優先級。提供設置緩存釋放優先級的能力,業務方可以更精準的利用緩存,更好的滿足業務所需;
6)View 創建方式。對於 View 的創建,可以設計一個 IViewCreeator,創建 View 的過程由使用方決定。如佈局文件中只有 TextView,可以傳入 layoutId 選擇 new TextView()的方式。
- 實施
基本使用如下,新建單例(如 ViewHelper):
使用 ViewHelper 如下:
使用前後的效果:
- 注意事項
1)使用異步 Inflate+全局緩存構建的 View,在使用時需要重新設置 LayoutParams,不然顯示上可能不是最終想要的結果;
2)使用異步 Inflate+全局緩存構建的 View,如果 View 的解析過程中,存在 Theme 相關的,可能會導致 View 構建失敗。如原生的 TabLayout,解析時會讀取 Theme 中的屬性,如果 context 傳入的是 APPlication 且沒有設置相關 Theme,就會報錯;
3)使用異步 Inflate+全局緩存構建的 View,需要及時的調用 refreshCurrentActivity 方法,這樣儘可能的保持 context 是當前的 activity 實例。在使用 context 的時候,避免直接把 context 強轉 activity,而是使用 ViewContext 的 getActivity 方法獲取。
五、總結
選擇異步 Inflate ,應根據需要,合理的選擇。如只需 activity 級別的,選擇原生 AsyncLayoutInflater 的方式,就能很好的滿足要求,並且沒有 context 使用問題。如想更早的準備或者跨頁面複用,View 的異步 Inflate+全局緩存是更好的選擇,但要注意 context 使用問題,因爲 inflate 所需的 context 不一定是 activity,也正是這點使得單例緩存的 View,不用擔心內存泄露問題,滿足多頁面的緩存複用。
目前,優酷在 AsyncView 項目已經實現 View 的異步 Inflate+全局緩存,該項目已經對公司內部開源,是 AIOSO 的子項目之一,也在進一步的落實對外開源。它是“低侵入式”的,沒有對 Android 原生 UI 進行改造,Android UI 框架開發的 APP 都可以方便接入。該項目已經在優酷 APP 上大量使用,反饋效果良好,主要體現在:
1)在幀率方面,整體帶來了 5%左右提升。尤其在低端機,體感上有明顯的提升;
2)在啓動方面,結合各業務端提前做一些預加載任務,整體帶來了 20%左右的提升。
作者 | 阿里文娛高級無線開發工程師 瑞源