写点什么

干货 | Trip.com APP 启动优化实践

  • 2021-06-15
  • 本文字数:3848 字

    阅读完需:约 13 分钟

干货 | Trip.com APP 启动优化实践

引言

启动是用户对 App 的第一印象,对于用户体验尤为重要,所以我们花了很多时间在启动时间的优化上。本文将分享 Trip.com App 的启动优化实践,从分析 App 启动的过程开始,在了解启动流程的基础上制定大的优化原则和小的具体方案,希望能对大家有所帮助。

一、App 启动的流程分析

想做启动优化,首先要了解清楚启动的各个流程,然后才能对各个环节去做针对性措施。


借用 WWDC 对启动阶段的定义图:


1.1 System Interface

  • 加载 App 可执行文件

  • Load dylibs


加载动态链接器dyld ,dyld会递归加载 App 依赖的动态库,然后执行符号绑定RebaseBind。一般应用会加载 100 到 400 个 dylib 文件,幸运是大部分是系统库,且系统会在操作系统启动时计算和缓存系统动态库。


Apple 为了解决安全问题,引入ASLRCode Sign,如果不作符号修正,程序将没法正常运行,所以会有 Rebase 和 Bind 过程。



  • Rebase

在镜像内部调整指针的指向,其实就是将内部指针都加上偏移量(Slide=实际新地址-旧地址)


  • Bind

修正指外部的指针,比如上图中 malloc,这个符号不存在于我们 App 的 Mach-O 中,需要从外部的镜像中获取,这时候就需要 Bind 操作把这个关联起来。


  • libSystem init

调用系统的一些初始化方法,这部分一般时间比较固定,可以不用太关注。

1.2 Runtime Init

  • Objc 和 Swift 的初始化

通过_dyld_objc_notify_register注册回调,在 image 加载完时初始化语言相关。


  • 加载 category

在上面语言初始化完之后,会加载所有 category,处理 category 的所有方法,协议和属性等。


  • 调用所有+load

也是通过向 dyld 注册回调,在 image 加载完时,通过load_images 触发,处理该 image 相关的所有+load 方法,按照继承层级依次调用:父类+load→子类+load→category +load,注意 category 的+load 不会覆盖原类。


  • 调用 C++的构造函数属性函数 attribute((constructor)) 

1.3 UIKit Init

  • 实例化 UIApplication 和 UIApplicationDelegate

  • 开始事件处理和系统集成

1.4 Application Init

这部分是我们熟悉的 UIApplicationDelegate 的几个生命周期调用:


  • application:willFinishLaunchingWithOptions:

  • application:didFinishLaunchingWithOptions:

  • applicationDidBecomeActive:

  • scene:willConnectToSession:options:

  • sceneWillEnterForeground:

  • sceneDidBecomeActive:

1.5 Initial Frame Render

这里是 App 渲染第一帧,主要做了创建、布局和绘制视图的工作,并把准备好的第一帧提交给渲染层渲染。这里面布局计算,图片解码,图层树的递归 commit 到 Render Server 等都是可能影响耗时的点,所以要特别注意。

1.6 Extended

这里按照苹果的定义,是异步获取数据展示界面的逻辑。比如我们首页要从网络请求数据然后展示最新数据在页面上。

二、针对启动的各个流程我们能做什么

2.1 总体原则

不管哪个流程,我们都想尽量遵循下面两个原则:


删的原则是指,对 App 启动和运行不是必须的任务,或者跟首页渲染第一帧无关的任务,都从启动流程中删除。对于删除的任务,可以进行懒加载的形式,需要时再调用;也可以换到其他的时机去触发,比如首页渲染完之后。


压的原则是指,对 App 启动和运行必须的任务,或者直接影响首页渲染第一帧的任务,都尽可能压缩其运行时间。至于做法,可以是优化方法内的实现,使其运行更快;也可以将方法执行的线程切换到子线程,以并发的形式降低其对整个启动过程的影响。

2.2 具体方案

2.2.1 减少动态库

动态库的加载在启动阶段是必须的,所以我们要尽量减少非必要的动态库。对此我们做了以下几点:


1)梳理所有动态库,将用不到的或者可以简单替代的动态库删除


可以通过otool -L xxx.app/xxx 或者打开打包后的产物,从 xxx.app/Frameworks 路径中找到所有动态库,逐个筛选,将其中可以废弃和替代的动态库删除。


2)通过推进社区(第三方 SDK)将现有动态库转成静态库


因为依赖了第三方 SDK,我们是不包含源码的,所以这部分需要推进社区提供静态库的版本,或者通过 cocoapods 等工具打包 SDK 的静态库版本。


3)将我们自己的 SDK 编译成静态库


对于我们自己的 SDK,因为有源码,所以直接修改MACH_O_TYPE 为Static Library 重新打包即可。


4)App 最低支持系统版本升级到 12.2


因为 iOS 在 12.2 版本及以上才内置了 Swift 的支持,所以在此之前 Swift 的动态库都是随着 App 下发的,也在 xxx.app/Frameworks 里。


当然,这个决策是会直接应用到用户和订单的,所以是要有数据支持的,我们是根据用户占比到达某个阈值才支持 12.2 的。如果允许,甚至可以升级到 iOS 13,因为 iOS13 以上 dlyd3 做了很多加载和缓存的优化。

2.2.2 删除无用代码

如果符号越多,很显然 Rebase 和 Bind 的处理时间就会越长,Objc 的初始化也受影响,所以我们需要尽可能减少代码:


1)通过逆向二进制或者生成 linkmap,解析所有方法(TEXT.text)和引用到的方法(__DATA _objcselrefs),找出无用方法删除


2)解析所有类(DATA.objcclasslist)和引用到的类(DATA.objcclassrefs),找出无用的类删除


3)使用第三方工具或者 clang 扫描重复代码,精简去重


4)使用LLVM_LTOGCC_OPTIMIZATION_LEVEL等其他编译选项优化二进制大小

2.2.3 合并 category

合并 category,可以减少 category 加载时的耗时。不过这部分收益不大,并且也会影响编程习惯,所以我们并没有投入很多时间,不再赘述。

2.2.4 删除+load

以前会有很多代码为了省事,加到了+load 中,这部分很显然占用启动时间,所以尽量要把这其中的代码转移,可以放到 initialize 中懒加载,或者放到启动任务中并发执行,尽量减少这部分的影响。


Xcode 调试时,可以通过正则添加所有+load 方法的断点br s -r "\+\[.+ load\]$" ,然后使用br list打印出所有+load 列表,这样方便我们定位所有+load。

2.2.5 UIApplication 子类优化

为了减少 UIKit Init 的时间,可以对 UIApplication 的子类初始化工作优化。我们这部分不存在,所以没有做什么工作。

2.2.6 启动任务并发

想象一下,如果application:didFinishLaunchingWithOptions:里面执行的所有启动任务不作任何处理,那么代码框架将会很乱,你的优化也只能单点单点去做。


所以我们将application:didFinishLaunchingWithOptions:阶段所有方法任务化,一个任务做一种类型的事。任务拆分好之后,就可以根据任务之间的相关性,选择哪些任务是可以并发执行,哪些任务是必须有依赖关系前后执行。


以前:



现在:



当然,任务的拆分颗粒度也很重要,拆分太粗的话,很难达到最优的组合,可能一个任务里的方法之间仍然有并行的空间。拆分太细的话,也有可能导致同一时间并发数太多,造成额外的线程切换开销。

2.2.7 I/O 处理

尤其要注意启动阶段的 I/O,一般出现于读取磁盘中的文件,比如配置文件等。


使用 Instrument→App Launch 去查看启动过程就会发现,如果主线程执行出现很多灰色的块,那就是 I/O,找到这些 I/O 产生的方法,尽量在子线程并发执行,避免阻塞主线程。

2.2.8 首页数据的预加载和懒加载

首页上有很多数据要加载,比如图片、上次缓存在本地的数据等等,这些数据的加载如果在写代码时不作特殊处理,那会在主线程执行,不知不觉就会有很多耗时。


1)预加载

对首页渲染必须的数据,比如一个 icon,或者一个翻译的数据,我们通过在启动任务(之前提到的拆分的并发任务)中新增加一个预加载启动任务,专门负责在application:didFinishLaunchingWithOptions: 的过程中并发执行数据的获取。因为获取数据大多比较耗时,所以放在子线程充分利用启动阶段的空闲。同时这类任务大多数是 I/O 操作,并不会占用太多 CPU 资源。


更进一步,其实可以对首页用到的资源在运行时作个标记,记录到磁盘,下次启动的时候读取这个记录,对用到的资源进行提前预加载,这样避免 hard code 很多资源名在代码中。


2)懒加载

首页的数据往往很多,但并不是一开始要全部用到。可以对数据作区分,和第一屏展示无关的,使用懒加载,真正用到的时候再去加载。

2.2.9 二进制重排

1)page fault 

由于虚拟内存的机制,应用启动时不会把所有数据加载到内存,而是以页为单位逐步从磁盘中加载,内存中的虚拟地址和磁盘中的物理地址有个映射关系。当程序执行时,如果发现要访问的东西不在内存里,就会触发一次page fault ,去磁盘中加载新的一页。


启动阶段有很多方法要调用,而这些方法在 Mach-O 中的位置又是在编译时确认的。如果有 10 个方法刚好在不同页,可能就要产生 10 次page fault 。


二进制重排要做的就是将启动阶段要用到的方法,在编译时提前确定,通过.order 文件告诉编译器,这样这些方法会排布在 Mach-O 的最前面,之前的 10 次page fault 很可能就变成一两次page fault

通过在 Other C Flags 中添加-fsanitize-coverage=func,trace-pc-guard 再通过__sanitizer_cov_trace_pc_guard记录启动阶段所有方法的调用,再将这些写入到.order 文件中,在 Xcode 的ORDER_FILE 设置中配置即可生效。


通过测试,我们的二进制重排大概优化 100-200ms。

2.2.10 其他通用手段

针对启动任务和首页渲染阶段,通用的手段是通过 instrument,profile 出耗时长的任务,对任务针对性地做方法优化。如果有的方法是第三方库的,那就需要推进社区去更新。我们在做的过程中给 Firebase 和 Google 的一些 SDK 提了很多 issue,对方开发人员配合很积极,对我们帮助很大。

三、成果如何

通过长期的优化,以上手段全部用完之后,我们的启动时间从原来的 2 秒,优化到 1 秒以内。

总结

在优化启动时间的过程中,我们的收获不仅是对启动时间的优化,也对系统的启动机制有了更深的了解,同时优化了我们自己的代码,使其变得更加更加健壮和高性能。


作者简介

Shanks,携程移动开发专家,关注移动端基础技术。


本文转载自:携程技术中心(ID:ctriptech)

原文链接:干货 | Trip.com APP 启动优化实践

2021-06-15 08:001582

评论

发布
暂无评论
发现更多内容

2023年Java面试题精选(蚂蚁金服/滴滴/美团/拼多多腾讯)

架构师之道

java面试

数据治理如何做?火山引擎DataLeap帮助这款产品3个月降低计算成本20%

字节跳动数据平台

大数据 数据治理 数据研发 企业号 2 月 PK 榜

F5 分布式云服务为软银集团的私有基础设施带来云原生能力

F5 Inc

如何将Excel文档转换为PDF文档

Geek_249eec

Java Excel PDF

NFTScan x TiDB丨一栈式 HTAP 数据库为 Web3 数据服务提供毫秒级多维查询

NFT Research

数据库 NFT

第七周作业-王者荣耀商城异地多活架构设计

不爱学习的程序猿

Zebec生态持续深度布局,ZBC通证月内翻倍或只是开始

鳄鱼视界

研发效能DevOps推荐书单

laofo

DevOps cicd 研发效能 持续交付

MQTT 5.0介绍

EMQ映云科技

性能 物联网 IoT mqtt 企业号 2 月 PK 榜

IoT 物联网平台如何实现 100万/秒 消息广播?——实践类

阿里云AIoT

小程序 监控 物联网 传感器 测试技术

PDF电子书下载 和 企业物联网实例 视频讲解——实践类

阿里云AIoT

运维 监控 物联网

CodeArts Repo:6大特性助力企业代码稳定可靠安全无忧

华为云开发者联盟

云计算 后端 华为云 企业号 2 月 PK 榜 华为云开发者联盟

A/B测试成为企业“新窗口”:增长盈利告别经验主义,数据科学才是未来

字节跳动数据平台

大数据 AB testing实战 企业号 2 月 PK 榜

有奖调研!第五期(2022-2023)传统行业云原生技术落地调研——金融篇

York

容器 微服务 云原生 问卷调研

深入理解跳表及其在Redis中的应用

京东科技开发者

redis 数据结构 算法 跳表 链接

接地电阻要小于4Ω,你知道是为什么吗?

元器件秋姐

科普 元器件 电阻 接地电阻

基于SpringBoot实现操作GaussDB(DWS)的项目实战

华为云开发者联盟

数据库 后端 华为云 企业号 2 月 PK 榜 华为云开发者联盟

瓴羊Quick BI提供移动端自助分析整体解决方案,Fine BI、Smart BI何时赶上?

小偏执o

chatGPT接入微信公众号方法总结(纯聊技术)

特立独行的猫

微信 ChatGPT 公众号接入

MQTT协议Keep Alive详解

EMQ映云科技

物联网 IoT mqtt 企业号 2 月 PK 榜 半连接

模块7作业

程序员小张

「架构实战营」

DevData Talks | 对谈谷歌云 DORA 布道师,像谷歌一样度量 DevOps 表现

思码逸研发效能

研发效能

移动应用程序开发新趋势

没有用户名丶

被骂惨了!复旦版「MOSS」服务器挤崩,一口吃不成ChatGPT

引迈信息

人工智能 AI ChatGPT MOSS

DevEco Studio端云协同开发之云数据库

白晓明

云数据库 HarmonyOS 端云协同

任务管理-轻松搞定 IoT 设备重启、资源包更新、固件升级等业务——实践类

阿里云AIoT

json 物联网 数据格式

云管理行业标杆产品有哪些品牌?大家重点推荐哪家?

行云管家

云计算 云服务 云管理 云管

全球首个云渗透测试认证专家课程发布!腾讯安全领衔编制

腾讯安全云鼎实验室

云安全

2023“Java基础-中级-高级”面试集结,已奉上我的膝盖

程序知音

Java java面试 金三银四 后端技术 Java面试八股文

Java单元测试浅析(JUnit+Mockito)

京东科技开发者

Java 单元测试 代码 JUnit Mockito

长沙等保测评公司有哪些?现在有新增吗?

行云管家

等保 等级保护 等保测评 长沙

干货 | Trip.com APP 启动优化实践_移动_携程技术_InfoQ精选文章