作者: 海瀦,錦逸
隨著閒魚App端更多新功能、新技術的加入,應用冷啟動速度越來越慢,這也意味著用戶看到有效內容的時間被拉長,對用戶體驗有著很大的傷害。目前,在內部測試版本中,我們已經將Android的冷啟動時間從原來的10s降低到了5s內。
閒魚是如何快速將啟動時間減少一半的呢?分為建立標準
、分析現狀
、抓大放小
三個步驟。
建立標準
做性能優化不是討論哲學問題,建立合理的數據衡量標準非常重要。儘管已經有了很多關於如何卡口關鍵函數、如何判斷頁面第一幀渲染完成的討論,但從代碼層面進行判斷始終與用戶的感知無法100%得匹配。如何迅速建立起啟動時間的標準?我們借鑑了手淘的方式和標準,利用內部的魔鏡平臺,使用視頻關鍵幀的方式記錄下App圖標被點下到首頁第一屏渲染完成作為一整個應用冷啟動的過程。這與用戶看到的啟動過程吻合。
對於設備的選擇上,我們使用y67這樣一臺現在看起來相對性能較差的機型作為優化的目標機型。低端機存在CPU能力弱,IO速度慢等問題,而慢代碼與IO恰恰是拖慢應用啟動最大的原因。定位優化的目標機型可以更加快速得解決common類型的啟動問題。
閒魚現狀
我們先使用日誌打點的方式來統計啟動過程中耗時的大頭,以便可以快速得將啟動性能提高上去。可以看到圖中,進入首頁渲染前,common
、interactive
兩部分佔去了大部分的時間,這是啟動器在執行啟動任務。而在進入首頁後,頁面的請求與view的排版佔用了大部分的時間。
基於上面的分析,第一階段我們將”啟動任務治理“和”首頁渲染加速“作為快速提升啟動時間的重點來優化。
啟動任務優化
閒魚的Android端在16年的時候上線了一個基於DAG(有向無環圖)的啟動器,它將啟動任務編排為一個DAG,並使用多核多線程併發的執行任務。上面說到的common
與interactive
屬於啟動器執行任務的兩個階段,它們都會讓主線程等待階段中的任務全部執行完,所以這兩個階段的任務,我們叫它阻塞型任務
。
目前為止,整個閒魚Android在啟動階段有77個任務需要執行,其中阻塞型任務有61個,y67上的總執行耗時在8s以上,併發後需要將近2.5s的時間。
對於啟動階段阻塞型任務,最快的優化方式有三點:
- 部分任務延遲執行
- 降低任務本身的耗時
- 拆分大任務
任務拆分與延遲執行
減少阻塞型任務的數量,是加速啟動最直接的手段。這裡需要根據任務的DAG進行依賴分析,能夠無傷被延遲執行的任務最明顯的特徵就是”沒有其他任務依賴於它“。如果任務之間有依賴,則需要根據後續首頁對於模塊的使用情況來決定是否將整個依賴鏈上的任務全部延遲。
閒魚的首頁金剛位大部分是weex、web和小程序的入口,另外首頁也會用到端智能相關的功能。然而這四個sdk的初始化,普遍都在300-500ms左右,屬於比較”硬核“任務。在將這四個任務移動到異步非阻塞階段後,整個啟動降低了500ms(當然要設置最高優先級以保證用戶儘量少的等待時間)。
非阻塞任務的觸發時機
任務啟動的時機就像跟女生表白一樣,不是你想啟動,啟動就啟動的。錯誤的時機大概率造成災難性得後果。
在我們將幾個大任務移動到非阻塞階段後發現,如果階段啟動的時候首頁還沒開始渲染或者沒有渲染完成,整個首頁的渲染會變得非常緩慢,圖片的加載也隨之變慢。總之就是誰碰到誰倒黴。實測中,非阻塞階段啟動的時機會對首頁的渲染產生將近1s左右的波動,使得啟動時間不斷得在危險的邊緣
瘋狂試探。
這是由於非阻塞階段會在進入首頁後的第一個queueIdle
回調之後觸發。而它的執行佔用了多過的系統資源,造成CPU佔用、網絡請求排隊、IO密集等問題。最終導致主線程、渲染變慢的情況。
那麼什麼時間才是啟動非阻塞任務的合適時機呢?既然我們選擇首頁渲染為最高優先級,非阻塞任務的啟動就必須排在後面。於是一咬牙一跺腳——”砍“!
我們讓首頁在確認view都上屏後,發信號給啟動器。啟動器這個時候才開始註冊queueIdle
回調,並啟動一個延遲6s的runnable作為”備份“,防止message queue過忙長時間無法觸發非阻塞階段的任務。
但這裡有個矛盾點,首頁上幾大金剛位都是通往weex、web或者小程序的,如果用戶點擊這些頁面比非阻塞階段的觸發更早,該怎麼辦呢?當然是原諒觸發它啊!
這裡我們採用的方式是,當這些功能被觸發的時候,需要先去check需要的模塊是否已經初始化完成。如果沒有的話,check非阻塞階段是否已經啟動。如果已啟動,就進入等待,否則強制觸發(這個時候首頁必然已經渲染完成了),並等待所需要的任務執行完成。
任務耗時治理
要快速治理,需要利用一些成熟的工具。可以先對任務中的每一行代碼進行時間統計,篩選出執行時間較長的調用後,使用系統提供的method trace
進行更細粒度的分析。
既然是要快,那麼一定是找通用類型的問題下手:
- 對於IO出來的值,儘量做內存緩存,避免多次IO
- 避免產生大的SharedPreference文件,儘可能得將對commit的調用換成apply
- 注意一些異步接口回調的線程,如果是主線程,也需要保證回調後的代碼快速執行完
首頁啟動優化
優化前,閒魚的首頁需要先進行三個排隊的網絡請求,彈出廣告頁,接著進行動態模板的渲染與數據綁定,總消耗時間在3.5s以上,這裡面還不包括圖片上屏的時間。
閒魚首頁部分的啟動優化,主要也從三個方面來做:
- 廣告頁
- 數據預先加載
- View的預創建
廣告頁優化
閒魚之前的廣告頁的流程如下圖:
先拉起啟動頁,然後啟動頁拉起首頁,首頁再拉起廣告頁,廣告頁起來先展示默認圖,然後同時去做是否有廣告的判斷,然後再去做廣告的展示,這個過程如果沒有廣告,也會讓默認的廣告頁展示3秒鐘再關閉。
這個過程顯然是不合理的,廣告有自己的疲勞期,那麼在沒有廣告的時候,拉起廣告頁就是一個浪費。其次廣告頁作為一個Activity拉起,需要經歷一些IPC的調用,整個操作也是比較重的。
基於這兩點,我們在廣告頁這塊,先在初始化的時候就做提前的資源拉取和預判,這樣如果確實沒有廣告資源,那麼廣告頁直接不做啟動,節省啟動資源。其次,我們將廣告頁由一個activity,改造成一個全屏展示的Dialog,進一步來節省廣告拉起時資源消耗,讓首頁其他內容的加載有更充足的系統資源。
數據預先加載
在性能優化中,空間換時間與提前預加載就像廣為人知的”中間加一層“一樣好用。
閒魚首頁必須的兩個接口,冷啟和熱啟接口耗時在1秒左右,而他們是在首頁第一幀回調回來之後的時機才開始請求的。這裡完全可以把請求的時機提前到初始化的過程中並行去做,從而為閒魚啟動-1s。
於是我們設計了針對這種情況下的預取模塊,在初始化的時候,就去做首頁數據的預加載,整體的模塊的時序如下:
這一步做完之後,本地機器測試結果大約節約了950ms的啟動時間。
View預創建
在解決完數據的問題之後,我們通過魔鏡平臺,會發現在y67上,首頁展示之後,有大量的白屏的時間,view的創建和渲染,在這裡消耗了大量資源,並佔用了很長的時間(這裡每一幀是100ms),平均大概在1400ms
於是我們自然而然的想到了在初始化的過程中去提前創建view,但是如果是在初始化過程中的主線程去創建view,那麼勢必會跟啟動頁和廣告頁等ui元素競爭主線程的使用,基本等於白乾。
於是這裡我們採用在子線程預先創建view並執行mesure與layout操作。等待首頁渲染時,使用對應的id進行取出和使用。做完之後,會發現view的上屏時間,在y67上縮短到600ms,減少了一倍的的時間:
總結與下一步優化
通過上面的方式,整個啟動階段的時間從2.5s降低到了1.3s,降低了將近一倍的時間。另外啟動任務所消耗的總時間從8s降低到了3s。首頁的渲染幾乎達到了秒出。整體啟動時間降低到了4.5s左右。
這個階段主要是對啟動過程中的任務與首頁代碼本身的優化。下一階段,我們會對整個啟動過程中的運行環境進行優化:
- 對啟動時候的資源消耗進行整理,減少不必要的網絡請求與IO以及線程切換。
- 對啟動器中的線程負載進行優化,目前啟動的任務分配方式距離理論上的最優值(平均值)大約還有50%的空間。
- 使用dex-relayout、PGO加速啟動