zoukankan      html  css  js  c++  java
  • 马蜂窝 iOS App 启动治理:回归用户体验

    增长、活跃、留存是移动 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(https://developer.apple.com/videos/play/wwdc2017/413/)

    • Optimizing App Startup Time(https://developer.apple.com/videos/play/wwdc2016/406/)

    总结来看,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 研发技术专家。

    (题图来源:网络)

    关注马蜂窝技术公众号,找到更多你需要的内容

  • 相关阅读:
    完美世界笔试(动态规划,背包问题)
    腾讯笔试3
    腾讯笔试2
    腾讯笔试1
    阿里笔试1
    Merge Sorted Array
    Partition List
    Reverse Linked List II
    Remove Duplicates from Sorted List II
    白菜刷LeetCode记-704. Binary Search
  • 原文地址:https://www.cnblogs.com/mfwtech/p/10843257.html
Copyright © 2011-2022 走看看