增加、活躍、留存是移動 App 的常見核心指標,直接反映一款 App 甚至一個互聯網公司運行的健康程度和發展動能。啓動流程的體驗決定了用戶的第一印象,在必定程度上影響了用戶活躍度和留存率。所以,確保啓動流程的良好體驗相當重要。ios
「馬蜂窩旅遊」App 是馬蜂窩爲用戶提供服務的主要陣地,其承載的業務模塊不斷豐富和完善,產品功能日趨複雜,已經逐漸成長爲一個集合旅行信息、出行決策、自由行產品及服務交易的一站式移動平臺。緩存
「馬蜂窩旅遊」iOS App 歷經幾十個版本的開發迭代,在啓動流程上積累了必定的技術債務。爲了帶給用戶更流暢的使用體驗,咱們團隊實施了數月的專項治理,也總結出一些 iOS 啓動治理方面的實踐經驗,藉由本文和你們分享。性能優化
要分析和解決啓動問題,咱們首先須要界定啓動的內涵和邊界,從哪開始、到哪結束,中間經歷了哪些階段和過程。以不一樣視角去觀察時,能夠得出不一樣結論。bash
App 啓動本來就是程序啓動的技術過程。做爲開發人員,咱們很天然地更願意從技術階段去看待和定義啓動的流程。網絡
App 啓動的方式分爲冷啓動和熱啓動兩種。簡單來講,冷啓動發生時後臺是沒有這個應用的進程的,程序須要從頭開始,通過漫長的準備和加載過程,最終運行起來。而熱啓動則是在後臺已有該應用進程的狀況下發生的,系統不須要從新建立和初始化。所以,從技術視角討論啓動治理時,主要針對冷啓動。多線程
從技術視角出發,分析 iOS 的啓動過程,主要分爲兩個階段:併發
pre-main: main() 函數是程序執行入口,從進程建立到進入 main 函數稱爲 premain 階段, 主要包括了環境準備、資源加載等操做;app
post-main: main() 函數到-didFinishLaunchWithOptions:方法執行結束。該階段已得到代碼執行控制權,是咱們治理的主要部分。框架
<premain> <postmain>
+----------------X------------------------------------X--------->
start main -didFinishLaunchWithOptions:
複製代碼
iOS App 是面向終端用戶的產品,所以衡量啓動的最終標準仍是要從用戶視角出發。異步
從用戶視角定義啓動,主要以用戶主觀視覺爲依據,以頁面流程爲標準。這樣看來,常見的 App 啓動能夠分爲三個階段:
T1:閃屏頁
閃屏頁是啓動過程當中的靜態展現頁。在冷啓動的過程當中,App 尚未運行起來,須要經歷環境準備和初始化的過程。這個過渡階段須要展現一些視圖,供阻塞等待中的用戶瀏覽。
iOS 系統 (SpringBoard) 根據 App Bundle 目錄下的 Info.plist 中"Launch screen interface file base name"字段的值,找到所指定的 xib 文件,加載渲染展現該視圖。
閃屏頁的展現是系統行爲,所以沒法控制;加載的是 xib 描述文件,沒法定製動態展現邏輯,所以是靜態展現。
對應技術啓動階段的 pre-main 階段
T2(可選):歡迎頁(廣告)
App 運行後根據特定的業務邏輯展現的第一個頁面。常見的有廣告頁和裝機引導流程。
歡迎頁是業務定製的,所以可根據業務須要優化展現策略,該階段自己也是可選的。
T3:目標頁(落地頁)
App 啓動的目標頁。
能夠是首頁或特定的落地頁
目標頁的加載渲染渲染完成標誌着 T3 階段的結束,也標誌着啓動流程的結束。
啓動治理的最終目標是提高用戶體驗,在這樣的思想下,本文關於啓動流程的討論主要圍繞用戶視角進行。
對 iOS 啓動的治理,本質上是對應用性能優化 (App Performance Management) 的過程,其基本的方法論能夠概括爲:
界定問題
準確描述現象,肯定問題的邊界
肯定量化評價手段,明確關鍵指標
分析問題
分析問題產生的主要緣由,根本緣由
肯定問題的重要性,優先級
性能問題多是單點的短板,也多是複雜的系統性問題,切忌「頭痛醫頭,腳痛醫腳」。要嚴謹全面地分析問題,找到主要緣由、根本緣由予以優先解決
解決問題
肯定解題的具體技術方案
根據關鍵指標量化成果
對問題進行總結,積累沉澱
持續監控
性能問題是持續的,長期的
對關鍵技術指標創建長效的監控機制,確保增量能被及時反饋,予以處理
1. 啓動耗時
啓動耗時是衡量啓動性能的核心指標,由於它直接影響了用戶體驗並對用戶轉化率產生影響。
對啓動耗時指標的拆解有助於細粒度地監控啓動過程,幫助找到問題環節。具體能夠拆解爲:
技術啓動耗時指標
pre-main
core-postmain
主觀啓動耗時指標
T1_duration :從程序運行起點到主視窗可見
T2_duration
T3_duration
total_duration
根據對馬蜂窩 App 用戶的行爲數據分析確認,咱們獲得如下結論:
啓動耗時和啓動流失率正相關
啓動耗時和第二天留存負相關
2.啓動流失率
1). 如何定義啓動流失
用戶視角的啓動流程完成前(即目標頁渲染完成前),用戶主動離開 App(進入後臺,殺死 App, 切換到其餘 App 等),記作一次啓動流失。
啓動流失率計算公式爲:
啓動 PV 流失率:啓動流失 PV / App 首次進入前臺 PV
啓動 UV 流失率:啓動流失 UV / DAU
UV 絕對流失率:當日僅進入前臺一次且流失的 UV / DAU
2) 如何定義首次進入前臺
咱們先來區分下冷啓動,熱啓動和首次進入前臺的概念:
iOS App 有後臺機制,App 可在某些條件下,在用戶不感知的狀況下在後臺啓動(如後臺刷新)。因爲用戶不感知,若是當日該用戶沒有主動進入前臺,則不會記做活躍用戶。所以,單純的後臺啓動不是啓動流失率的分母。
可是當 iOS App 從後臺啓動,並留在內存中沒有被操做系統清除,而一段時間後,用戶觸發 App 進入前臺,這種狀況雖然是熱啓動,但應被看做「首次進入前臺」。
3) 如何定位流失的時機
根據定義,用戶主動離開 App 則記做一次流失。從技術角度能夠找到兩個點:
applicationdidEnterBackground
applicaitonWillTerminate
但在實踐的典型場景中咱們發現,從用戶點擊 Home 鍵到程序接收到-applicationdidEnterBackground 回調存在必定的時間差,該時間差會影響到流失率的判斷。
例如,用戶在時刻 0.0s 啓動 app,啓動總時長爲 4.0s。用戶在時刻 3.8s 點擊了 home 鍵離開 App,則應該記做 launch_leave = true。而程序在時刻 4.3s 接收到了-applicationDidEnterBackground 回調,此時啓動已經結束,得到了啓動耗時 4.0s。經過比較 Tleave > Tlaunch_total,則錯誤地記爲 launch_leave = false。
由此推測,這裏的 delay 是設置靈敏度阻尼,消除用戶決策的擺動。這個延時大約在 0.5s 左右。
爲了不這個偏差,咱們的解決方案是利用 inactive 狀態,找到準確的用戶決策起點:
用戶即將離開前臺時,會先進入 inactive 狀態,經過-appWillResignActive:拿到決策起點的時間戳 Tdetermine
根據用戶最終決策行爲,是否確實離開,再決定決策 Tdetermine 是否有效
最終根據有效的 Tdetermine 做爲判斷流失行爲的標準,而不是-applicationdidEnterBackground 的時間點
3. 啓動廣告曝光率
廣告是 App 盈利的主要手段之一。廣告曝光率直接決定了廣告點擊消費率;而廣告曝光 PV 和加載 PV 直接影響了廣告售價。
咱們定義:啓動廣告曝光率 = 啓動廣告曝光 PV / 啓動廣告加載 PV。
其中廣告素材須要下載,素材渲染須要必定耗時,這些都會對廣告曝光率產生影響。進一步來講,啓動廣告的曝光率會受到 App 啓動性能的影響,但更主要的是受緩存和曝光策略的影響,詳細闡述在下文「精細化策略」部分介紹。
以上,咱們對 iOS App 啓動治理的思路和關鍵指標進行了分析和拆解,下面來講一下從技術層面和業務層面,咱們對啓動性能的優化和流程治理分別作了哪些事情。
1. 優化pre-main
1). pre-main 主要流程分析
在進行該階段的優化前,咱們須要對 Pre-Main 階段的過程有所瞭解,網上的文章較多,這裏主要推薦兩篇 WWDC 參考文章:
App Startup Time: Past, Present, and Future(developer.apple.com/videos/play…
Optimizing App Startup Time(developer.apple.com/videos/play…
總結來看,pre-main 主要流程包括:
1. fork 進程
2. 加載 executable
3. 加載 DYLD
4. 分析依賴,迭代加載動態庫
a. rebase
b. rebind
c. 耗時多
5. 準備環境
a. 準備 OC 運行時
b. 準備 C++環境
6. main 函數
2). 優化建議
a. 儘可能編譯到靜態庫中,減小 rebase,rebind 耗時
b.儘可能合併動態庫,減輕依賴關係
控制 Class 類的數量規模
因爲 selector 須要在初始化時作惟一性檢查,應儘可能減小使用
少用 initializers
a. 嚴格控制 +load 方法使用
a. Swift 沒有運行時
b. Swift 沒有 initializers
c. Swift 沒有數據不對齊問題
3). 性能監控:如何獲取啓動起點
啓動的結束時間相對來講是比較好肯定的,但如何定位啓動的起點,是啓動監控的一個難點。
對於開發環境,能夠經過 Xcode 配置啓動參數,得到 pre-main 的啓動報告:
DYLD_PRINT_STATICS = 1
複製代碼
對於線上環境,根據 premain 主要流程的分析,咱們的解決方案是:
建立動態庫 ABootMonitor.dylib
ABootMonitor.dylib 實現+load 方法,記錄啓動起點時間
將 ABootMonitor.dylib 放在 executable 動態庫依賴的頭部
經過上述方法,能夠在線上環境儘可能地模擬出最先的啓動時間點,從而更好地監測優化效果。
2. 優化post-main
post-main 階段的技術優化主要針對兩個方法的執行耗時來進行:
- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:
- (void)applicationDidBecomeActive:(UIApplication *)application;
爲何包含 2,須要咱們對 iOS App 生命週期有必定理解。從操做系統的視角來看,iOS App 本質上是一個進程。對於 Mac OS/iOS 系統,進程的生命週期狀態包括了:
not-running
running
suspend
zombie
terminated
而對於 UIApplication,定義了生命週期狀態:
// UIApplication.h
typedef NS_ENUM(NSInteger, UIApplicationState) {
UIApplicationStateActive, // 前臺, UIApplication響應事件
UIApplicationStateInactive, // 前臺, UIApplication不響應事件
UIApplicationStateBackground // 後臺, UIApplication不在屏幕上顯示
} NS_ENUM_AVAILABLE_IOS(4_0);
複製代碼
組合起來的狀態機以下圖:
經過上面的討論,咱們能夠分析出如下問題:
UIApplication 會由於某種緣由,在用戶不感知的狀況下被喚起,進程進入 running 狀態,但停留在 iOS 的 background 狀態
每次冷啓動都會執行- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:,但未必進入前臺
在 didFinishLaunchingWithOptions 中進行大量 UI 和網絡請求等操做是不合理
post-main 優化思路和建議
整理拆分啓動項,以啓動項爲粒度進行測量
啓動項執行儘可能在背景線程
啓動項併發執行
啓動項延遲執行
當 CPU 時間片跑滿時,使用多線程併發不能提升性能,反而會由於頻繁的線程上下文切換,形成 overhead 耗時增加
儘量將啓動項延遲執行,在時間軸上平滑,下降 CPU 利用率峯值
啓動項分組
-didFinishLaunchingWithOptions 只執行必要的核心啓動項
其餘啓動項,在首次調用-applicationDidBecomeActive:後執行
1. 交互優化
經過技術的實現手段,咱們能夠從客觀上減小啓動的絕對耗時。而從用戶視角來看,對於啓動是否流暢會受到不少心理因素的主觀影響。所以從另外一方面,咱們能夠從優化交互的角度提高用戶體驗。
避免阻塞等待
咱們都但願用戶能夠儘快地使用 App,不要出現流失。但在快消費的時代,用戶的耐心是極其有限的。
所以,若是有理由須要用戶進行等待,就應該注意儘可能避免產品流程是阻塞的。即便有更充足的理由必須讓用戶在阻塞狀態原地等待,也應該給用戶提供可響應的交互。
例如,在 T2 歡迎/廣告頁階段,爲了不用戶阻塞等待,應該提供明顯的「跳過」按鈕,容許用戶進行跳過操做。
若是非要用戶在這個階段等待不可,也能夠花一些當心思提供可響應的交互,好比點擊觸發視覺的變化等,不要讓用戶除了等待無事可作。
增長視覺信息量
增長屏幕上視圖的信息量提供給用戶消費,轉移其注意力,下降用戶對等待的感覺。
例如,在 T1 閃屏頁階段,用戶處於阻塞等待的狀態,沒法跳過。並且閃屏頁是系統渲染的靜態視圖,咱們沒法提供動態響應。那麼,咱們能夠經過在靜態視圖上提供更多信息量,給等待中的用戶消費。
主觀感覺對好比下圖:
合理的動態提示
事實上,早期在部分高性能 Android 設備上,App 的啓動比同水平 iDevice 要快。但因爲 iOS 設計了符合神經認知學的交互動畫,使得主觀感覺到的時間縮短。
動畫是否「合適」,關鍵在於對場景的選擇和數量的把握。一個常見的動畫耗時約爲 0.25s,對於啓動流程來講,已經能夠解決或掩蓋很多問題了。
好的交互體驗和產品流程,至少應該是符合用戶預期的。給以合適的動態提示,讓用戶知道此刻使用的 App 正在發生什麼,能夠極大地提高用戶體驗。
例如在 T2 廣告頁階段,廣告須要佔時 3 秒鐘的時間。交互上建議給與廣告消失的倒計時提示:
一方面,倒計時提示能夠有動態 loading 的視覺效果,展示 App 的良好運行;
另外一方面,倒計時可讓用戶安心,主觀上耗時減小,情緒上不至於焦慮和退出。
2. 基於場景的啓動會話
根據對啓動過程的定義,咱們能夠列舉出一些啓動的「起點」和「終點」,好比:
啓動觸發點:
點擊 App 圖標正常啓動
初次安裝
點擊 PUSH 進入
應用間跳轉
3DTouch
Siri 喚起
其餘
啓動終點--目標頁:
應用首頁
指定的落地頁
能夠看出,啓動的起點和終點多種多樣,而對於啓動流程的設定,不少都是和業務場景強相關的,好比:
初次安裝須要進入裝機引導流程
正常啓動須要展現廣告
PUSH 進入能夠不展現廣告,直達落地頁
其餘
如何才能維護這些複雜的啓動關係,提升業務承載能力呢?咱們的優化思路是基於場景建立啓動會話:
由啓動參數和其餘條件肯定啓動場景
根據啓動場景建立具體的啓動會話
啓動會話接管以後的啓動流程
3. 啓動廣告曝光和緩存策略
廣告曝光主要流程爲:請求廣告接口 —> 準備廣告素材 —> 展現廣告頁,進行曝光。
在準備廣告素材環節,咱們會判斷廣告素材是否命中緩存。若是命中則直接使用緩存,這樣能夠明顯縮短廣告加載的時間。若是沒有命中,則開始下載廣告素材。當廣告素材超過設定的準備時長,則這次曝光不顯示。
經過以往數據量化分析,咱們發現一般狀況下,廣告未曝光的主要緣由是因爲廣告素材準備超時,且素材體積和廣告曝光率是負相關的。爲了保證廣告的曝光率,咱們應該儘可能減小廣告素材的體積,而且提升廣告素材緩存的命中率。
下面分別介紹下咱們的啓動廣告預緩存策略和啓動廣告曝光策略。
啓動廣告預緩存策略
廣告素材接口和廣告曝光接口分離
在可能的合適時機,下載廣告素材
儘量地提早下發廣告素材
拉長廣告素材投放的時間窗口
常見地可提早半月下發廣告素材
對於「雙十一等大促活動,應儘早地下發素材
啓動廣告曝光策略
分級的廣告曝光QoS策略
若業務許可,可對廣告優先級進行分級
對於低優先級,應用 cache-only 的曝光策略
對於普通優先級,應用 max-wait 的曝光策略
對於高優先級,應用 max-retry 的曝光策略
靈活的曝光時機選擇
一般咱們僅在首次進入前臺時,進行廣告曝光,但這有必定的缺陷:
啓動耗時長了,用戶體驗差,啓動流失率高
對於當日只有一次啓動且啓動流失的用戶,丟了這個 DAU
咱們能夠在 App 首次進入前臺,和熱啓動切回前臺時選擇時機,進行有策略的曝光
可依據策略,在首啓時不展現廣告頁,提高用戶體驗,DAU,減小啓動流失
可在 App 切回時展現,提高廣告曝光 PV,和曝光率。
因爲 App 以前已經啓動,此時大機率已經緩存了廣告素材
因爲 App 一次生命週期存在屢次切回前臺,曝光 PV 能夠獲得提高
根據馬蜂窩 App 的統計分析,在激進策略下可提高曝光 PV 約 4 倍
iOS 通過多年的迭代,提供了不少智能的平臺機制。合理利用這些機制,能夠強化 App 的功能和性能。
1. 內存保活
咱們已經討論了冷啓動和熱啓動的區別:
冷啓動是進程並不存在的狀態,一切須要從 0 開始。
熱啓動是指進程在內存中(iOS 不支持 SWAP),此時可能處於 background 的 running 狀態或 suspend 狀態,用戶喚起進去前臺。
熱啓動能夠極大地減小 T1 閃屏頁時間,從而減小啓動耗時。
所以,咱們應該儘可能增長熱啓動機率,而且儘可能減小 App 在後臺被系統回收的機率。
iOS App 生命週期中關於系統內回收策略以下:
App 進入後臺後,進程會活躍一段時間後,會被操做系統掛起,進入 suspend 狀態。除非在 info.plist 指定進入後臺即退出。
前臺運行的 App 擁有內存的優先使用權
當前臺的 App 須要更多物理內存時,系統根據必定策略,將一部分掛起的 App 進行釋放
系統優先選擇佔用內存多的 App 進行釋放
優化思路:
App 進入後臺時,應該將內存資源竟可能的釋放,儘可能在內存中保活
尤爲對於可重得的圖片,文件等資源進行釋放
對於可持久化的非重要內存,也可作持久化後釋放
對於線上,應利用後臺進程激活狀態,增強對後臺內存使用的監控
2. 後臺拉起
iOS 系統提供了一些機制,能夠幫助咱們實如今用戶不感知的狀況下拉起 App。合適的拉起策略,能夠優化 App 性能和功能表現,好比提高當日首啓熱啓動的機率;在後臺準備更新一些數據,如更新 PUSH token、準備啓動廣告素材等。
iOS 常見的後臺拉起機制包括:
Background-fetch 後臺刷新
須要權限
在某特定時機拉起,智能策略
PUSH
靜默推送
遠端推送
aps 中指定 "content-available = 1"
App 實現相關處理方法
地理圍欄
後臺網絡任務 NSURLBackgroundSession
VOIP 等其餘
使用後臺機制時,有如下幾點須要注意:
常見的後臺機制須要 entitlement 聲明和用戶受權
部分節能模式會使部分拉起機制失效,致使節能量模式不可用
拉起策略參考用戶意圖,用戶主動殺死 App,會使部分拉起機制失效
正常進入後臺,該 App 會向系統應用「AppSwitcher」註冊,並受其管理
若是用戶主動殺死 App,該 App 不會向「AppSwitcher」註冊
後臺拉起時,主要從 AppSwitcher 的註冊列表選擇 App 進行操做。例如,後臺刷新會根據某種策略排序,依此拉起 AppSwitcher 中註冊的部分 App
批量拉起會致使服務端接口壓力過大
頁面棧/樹優化
App 經過頁面進行組織,在啓動過程當中,咱們須要構建根頁面棧。
由上分析咱們知道,App 存在後臺拉起,咱們建議在首次進入前臺時才進行頁面渲染操做。但另外一方面,根頁面棧是 App 的基本結構,應該做爲核心啓動流程。所以咱們提出如下解決方案:
涉及啓動的頁面,如首頁、落地頁等,應將頁面棧建立、數據請求、頁面渲染分離
在覈心啓動流程 (didFinishLaunch) 建立核心頁面棧
在即將進入前臺時,異步請求數據
在目標頁即將展現時,進行渲染
例如,在廣告頁消失前的 1s,通知首頁進行渲染,以下圖
因爲目標頁可能和 T2 等啓動階段重疊,應特別注意頁面加載的性能問題,避免交叉影響
通過團隊 3 個月的持續優化治理,馬蜂窩 iOS App 的啓動優化取得了一些成果:
啓動耗時:約 3.6s,減小約 50%
PV啓動流失率:下降約 30%
啓動廣告曝光率:大幅提高
ios App 的啓動治理乃至性能管理,是一個長期且艱鉅的過程,須要各位開發同窗具有良好的對平臺和對代碼性能的理解意識。其次,性能問題也經常是一個複雜的系統性問題,須要嚴謹地分析和推理,在此感謝支持以上工做的馬蜂窩數據分析師。最後,這項工做須要創建完善的性能監控機制,持續跟蹤,主動解決。
咱們計劃於近期將馬蜂窩 iOS 的啓動框架開源,歡迎持續關注馬蜂窩公衆號動態。期待和你們交流。
本文做者:許旻昊,馬蜂窩 iOS 研發技術專家。
(馬蜂窩技術原創內容,轉載務必註明出處保存文末二維碼圖片,謝謝配合。)