增長、活躍、留存是移動 App 的常見核心指標,直接反映一款 App 甚至一個網際網路公司執行的健康程度和發展動能。啟動流程的體驗決定了使用者的第一印象,在一定程度上影響了使用者活躍度和留存率。因此,確保啟動流程的良好體驗至關重要。
「馬蜂窩旅遊」App 是馬蜂窩為使用者提供服務的主要陣地,其承載的業務模組不斷豐富和完善,產品功能日趨複雜,已經逐漸成長為一個集合旅行資訊、出行決策、自由行產品及服務交易的一站式移動平臺。
「馬蜂窩旅遊」iOS App 歷經幾十個版本的開發迭代,在啟動流程上積累了一定的技術債務。為了帶給使用者更流暢的使用體驗,我們團隊實施了數月的專項治理,也總結出一些 iOS 啟動治理方面的實踐經驗,藉由本文和大家分享。
0X0 如何定義「啟動」
要分析和解決啟動問題,我們首先需要界定啟動的內涵和邊界,從哪開始、到哪結束,中間經歷了哪些階段和過程。以不同視角去觀察時,可以得出不同結論。
技術視角
App 啟動原本就是程式啟動的技術過程。作為開發人員,我們很自然地更願意從技術階段去看待和定義啟動的流程。
App 啟動的方式分為冷啟動和熱啟動兩種。簡單來說,冷啟動發生時後臺是沒有這個應用的程式的,程式需要從頭開始,經過漫長的準備和載入過程,最終執行起來。而熱啟動則是在後臺已有該應用程式的情況下發生的,系統不需要重新建立和初始化。因此,從技術視角討論啟動治理時,主要針對冷啟動。
從技術視角出發,分析 iOS 的啟動過程,主要分為兩個階段:
-
pre-main: main() 函式是程式執行入口,從程式建立到進入 main 函式稱為 premain 階段, 主要包括了環境準備、資源載入等操作;
-
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 階段的結束,也標誌著啟動流程的結束。
啟動治理的最終目標是提升使用者體驗,在這樣的思想下,本文關於啟動流程的討論主要圍繞使用者視角進行。
0X1 方法論及關鍵指標
APM 方法論
對 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 啟動效能的影響,但更主要的是受快取和曝光策略的影響,詳細闡述在下文「精細化策略」部分介紹。
0X2 iOS 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 方法使用
- 多用 Swift
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
- 程式被掛起,不可以執行程式碼,通常在 UIApplication 進入後臺後一段時間被系統掛起
-
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 佔用較高,佔用主執行緒會導致卡頓,耗時延長,使用者體驗不佳
-
啟動項併發執行
-
啟動項延遲執行
-
當 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
-
-
批量拉起會導致服務端介面壓力過大
- 例如使用 PUSH 拉起,則短時間內可能有數千萬的 App 被拉起,此時介面請求不亞於一次針對服務端的 DDOS 攻擊,需要整理和優化
四、結構化定製
頁面棧/樹優化
App 通過頁面進行組織,在啟動過程中,我們需要構建根頁面棧。
由上分析我們知道,App 存在後臺拉起,我們建議在首次進入前臺時才進行頁面渲染操作。但另一方面,根頁面棧是 App 的基本結構,應該作為核心啟動流程。因此我們提出以下解決方案:
-
涉及啟動的頁面,如首頁、落地頁等,應將頁面棧建立、資料請求、頁面渲染分離
-
在核心啟動流程 (didFinishLaunch) 建立核心頁面棧
-
在即將進入前臺時,非同步請求資料
-
在目標頁即將展示時,進行渲染
-
例如,在廣告頁消失前的 1s,通知首頁進行渲染,如下圖
-
由於目標頁可能和 T2 等啟動階段重疊,應特別注意頁面載入的效能問題,避免交叉影響
-
0x3 結語
經過團隊 3 個月的持續優化治理,馬蜂窩 iOS App 的啟動優化取得了一些成果:
-
啟動耗時:約 3.6s,減少約 50%
-
PV啟動流失率:降低約 30%
-
啟動廣告曝光率:大幅提升
ios App 的啟動治理乃至效能管理,是一個長期且艱鉅的過程,需要各位開發同學具備良好的對平臺和對程式碼效能的理解意識。其次,效能問題也常常是一個複雜的系統性問題,需要嚴謹地分析和推理,在此感謝支援以上工作的馬蜂窩資料分析師。最後,這項工作需要建立完善的效能監控機制,持續跟蹤,主動解決。
One More Thing
我們計劃於近期將馬蜂窩 iOS 的啟動框架開源,歡迎持續關注馬蜂窩公眾號動態。期待和大家交流。
本文作者:許旻昊,馬蜂窩 iOS 研發技術專家。
(馬蜂窩技術原創內容,轉載務必註明出處儲存文末二維碼圖片,謝謝配合。)