【AICon】探索RAG 技术在实际应用中遇到的挑战及应对策略!AICon精华内容已上线73%>>> 了解详情
写点什么

干货 | 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:001394

评论

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

怎样才是刷面试题的正确姿势?Android400道面试题+通关知识宝典助你进大厂

android 程序员 移动开发

性能优化,还得看AspectJ,android高级开发实战

android 程序员 移动开发

战战兢兢面试了一个35岁Android开发程序员,就这水平开发了8年?

android 程序员 移动开发

我,Android开发,40岁熬到中层月薪15K,百度Android岗一面+二面内容

android 程序员 移动开发

成为一个优秀的Android开发者,需要必备哪些技术&工作技能?

android 程序员 移动开发

成功逆袭:越来越胖怎么能忍,我的APK瘦身之路,完整版开放免费下载

android 程序员 移动开发

我想谈谈关于-Android-面试那些事,写给有开发经验的你们

android 程序员 移动开发

我才知道原来Flutter内置了10多种Button控件,音视频开发工程师抖音

android 程序员 移动开发

我的2020年终总结:疫情之下裸辞后的一些感悟和面试心得(上篇

android 程序员 移动开发

总结了30个例子之后,我悟到了Flutter的布局原理,android移动开发基础答案

android 程序员 移动开发

想掌握Android面试官必问的 Binder 机制?那别想绕开 Binder 驱动源码分析!

android 程序员 移动开发

想搞懂Jetpack架构可以不搞懂生命周期知识吗?,阿里Android面试必问

android 程序员 移动开发

我们来剖析一下这个Android猴子的面试过程,以及被问到的问题

android 程序员 移动开发

战战兢兢面试了一个35岁Android开发程序员,就这水平开发了8年?难怪没有一个公司愿意要你

android 程序员 移动开发

惨遭社会毒打,公司倒闭突然失业,程序员该如何在下次危机对准时狠狠还击

android 程序员 移动开发

我敢打赌!你从未见过如此简单的Dagger-导航---基于-Android-Studio-4-1

android 程序员 移动开发

微信小程序之商品属性分类 —— 微信小程序实战商城系列

android 程序员 移动开发

微博热门清华学霸的计划表刷屏,程序员该如何制定你的学习计划?

android 程序员 移动开发

想掌握Android面试官必问的-Binder-机制?那别想绕开-Binder-驱动源码分析!

android 程序员 移动开发

我来告诉你:零基础如何成为高级Android开发,入职字节跳动

android 程序员 移动开发

我是如何做到在-5-分钟之内将应用大小减少-60%-的?,零基础学android编程

android 程序员 移动开发

我肝了3个月终于整理出了这份超全面的《Android面试题及解析》(1)

android 程序员 移动开发

微信逆向之朋友圈,2021最新Android大厂面试真题大全

android 程序员 移动开发

我们来剖析一下这个Android猴子的面试过程,以及被问到的问题(1)

android 程序员 移动开发

我又开发了一个非常好用的开源库,调试Android数据库有救了 (1)

android 程序员 移动开发

我又开发了一个非常好用的开源库,调试Android数据库有救了

android 程序员 移动开发

我肝了3个月终于整理出了这份超全面的《Android面试题及解析》

android 程序员 移动开发

怎样让你更快的完成工作去“摸鱼”,我的Android美团求职之路

android 程序员 移动开发

想进阶高级架构师,你需要养成这10个习惯!,掌握这套精编Android高级面试题解析

android 程序员 移动开发

我学习Android的一些套路,这份333页关于性能优化知识点的PDF你不能不看

android 程序员 移动开发

手写一款Android-手绘风格组件----这是你从未见过的组件库

android 程序员 移动开发

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