馬蜂窩 iOS App 啟動治理:迴歸使用者體驗

馬蜂窩技術發表於2019-05-10

增長、活躍、留存是移動 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 參考文章:

總結來看,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 主要流程的分析,我們的解決方案是:

  1. 建立動態庫 ABootMonitor.dylib
  2. ABootMonitor.dylib 實現+load 方法,記錄啟動起點時間
  3. 將 ABootMonitor.dylib 放在 executable 動態庫依賴的頭部

通過上述方法,可以線上上環境儘量地模擬出最早的啟動時間點,從而更好地監測優化效果。

2. 優化post-main

post-main 階段的技術優化主要針對兩個方法的執行耗時來進行:

  1. - (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:
  2. - (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 研發技術專家。

(馬蜂窩技術原創內容,轉載務必註明出處儲存文末二維碼圖片,謝謝配合。)

關注馬蜂窩技術公眾號,找到更多你需要的內容

相關文章