设为首页 加入收藏

TOP

从 iOS App 启动速度看如何为基础性能保驾护航(一)
2023-07-26 08:17:27 】 浏览:413
Tags:iOS App 何为基 能保驾

1 前言

启动是App给用户的第一印象,一款App的启动速度,不单单是用户体验的事情,往往还决定了它能否获取更多的用户。所以到了一定阶段App的启动优化是必须要做的事情。App启动基本分为以下两种

1.1 冷启动

App 点击启动前,它的进程不在系统里,需要系统新创建一个进程分配给它启动的情况。这是一次完整的启动过程。

表现:App第一次启动,重启,更新等

1.2 热启动

App 在冷启动后用户将 App 退后台,在 App 的进程还在系统里的情况下,用户重新启动进入 App 的过程,这个过程做的事情非常少。

所以我们主要说道说道冷启动的优化

2 启动流程

2.1 APP启动都干了什么

要对启动速度进行优化,我们需要知道启动过程中的大致流程是什么,做了什么事情,是否能针对性优化。
下图是启动流程的详细分解

  1. 点击图标,创建进程
  2. mmap 主二进制,找到 dyld 的路径
  3. mmap dyld,把入口地址设为_dyld_start

dyld 是启动的辅助程序,是 in-process 的,即启动的时候会把 dyld 加载到进程的地址空间里,然后把后续的启动过程交给 dyld。dyld 主要有两个版本:dyld2 和 dyld3。

iOS 12之前主要是dyld2,iOS 13 开始 Apple 对三方 App 启用了 dyld3,dyld3 的最重要的特性就是启动闭包,闭包存储在沙盒的 tmp/com.apple.dyld 目录,清理缓存的时候切记不要清理这个目录。

闭包里主要有以下内容:

  • dependends,依赖动态库列表
  • fixup:bind & rebase 的地址
  • initializer-order:初始化调用顺序
  • optimizeObjc: Objective C 的元数据
  • 其他:main entry, uuid等等

上图虚线之上的部分是out-of-process的,在App下载安装和版本更新的时候会去执行,直接从缓存中读取数据,加快加载速度

这些信息是每次启动都需要的,把信息存储到一个缓存文件就能避免每次都解析,尤其是 Objective-C 的运行时数据(Class/Method…)解析耗时, 所以对启动速度是一个优化提升

4.把没有加载的动态库 mmap 进来,动态库的数量会影响这个阶段

dyld从主执行文件的header获取到需要加载的所依赖动态库列表,然后它需要找到每个 dylib,而应用所依赖的 dylib 文件可能会再依赖其他 dylib,所以所需要加载的是动态库列表一个递归依赖的集合

5.对动态库集合循环load, mmap 加载到虚拟内存里,对每个 Mach-O 做 fixup,包括 Rebase 和 Bind。

对每个二进制做 bind 和 rebase,主要耗时在 Page In,影响 Page In 数量的是 objc 的元数据

  • Rebase 在Image内部调整指针的指向。在过去,会把动态库加载到指定地址,所有指针和数据对于代码都是对的,而现在地址空间布局是随机化(ASLR),所以需要在原来的地址根据随机的偏移量做一下修正, 也就是说Mach-O 在 mmap 到虚拟内存的时候,起始地址会有一个随机的偏移量 slide,需要把内部的指针指向加上这个 slide.
  • Bind 是把指针正确地指向Image外部的内容。这些指向外部的指针被符号(symbol)名称绑定,dyld需要去符号表里查找,找到symbol对应的实现, 像 printf 等外部函数,只有运行时才知道它的地址是什么,bind 就是把指针指向这个地址,这也是后面我们能用fishhook来hook一些动态符号的核心

如下图,编译的时候,字符串 1234 在__cstring的 0x10 处,所以 DATA 段的指针指向 0x10。但是 mmap 之后有一个偏移量 slide=0x1000,这时候字符串在运行时的地址就是 0x1010,那么 DATA 段的指针指向就不对了。Rebase 的过程就是把指针从 0x10,加上 slide 变成 0x1010。运行时类对象的地址已经知道了,bind 就是把 isa 指向实际的内存地址。

6.初始化 objc 的 runtime,由于闭包已经初始化了大部分,这里只会注册 sel 和装载 category

7.+load 和静态初始化被调用,除了方法本身耗时,这里可能还会引起大量 Page In,如果调用了dispatch_async则会延迟启动后的runloop开启后执行,如果触发静态初始化,则会延迟到运行时执行

8.初始化 UIApplication,启动 Main Runloop,可以在之前章节利用runloop统计首屏耗时,也可以在启动结束做一些预热任务

9.执行 will/didFinishLaunch,这里主要是业务代码耗时。首页的业务代码都是要在这个阶段,也就是首屏渲染前执行的,主要包括了:首屏初始化所需配置文件的读写操作;首屏列表大数据的读取;首屏渲染的大量计算等;sdk的初始化;对于大型组件化工程,也包含了很多moudle的启动加载项

10.Layout,viewDidLoad 和Layoutsubviews 会在这里调用,Autolayout 太多会影响这部分时间

11.Display,drawRect 会调用

12.Prepare,图片解码发生在这一步

13.Commit,首帧渲染数据打包发给 RenderServer,走GPU渲染流水线流程,启动结束

(tips: 2.2.10-2.2.13这里主要是图形渲染流水线的部分流程,Application产生图元阶段(CPU阶段))。后续会交由单独的RenderServer进程,再调用渲染框架(Metal/OpenGL ES)来生成 bitmap,放到帧缓冲区里,硬件根据时钟信号读取帧缓冲区内容,完成屏幕刷新

2.2 启动各阶段时长统计

上一小节对启动各个阶段过程的详细阐述,归纳起来大致分为6个阶段(WWDC2019):

通过对各个阶段进行时长统计分析,进行优化然后对比。

可以在Xcode中设置环境变量DYLD_PRINT_STATISTICS和DYLD_PRINT_STATISTICS_DETAILS看下启动阶段和对应的耗时(iOS15后环境变量失效)

也可以通过Xcode MetricKit 本身也可以看到启动耗时:打开 Xcode -> Window -> Origanizer -> Launch Time

如果公司有对应的成熟监控体系最好,这里我们主要通过手动无侵入埋点去统计启动时长,对启动流程pre main-> after main进行统计分析

2.1.1 进程创建时间打点

通过 sysctl 系统调用拿到进程创建的时间戳

#import <sys/sysctl.h>
#import <mach/mach.h>


+ (BOOL)processInfoForPID:(int)pid procInfo:(struct kinfo_proc*)procInfo
{
    int cmd[4] = {CTL_KERN, KERN_PROC, KERN_PROC_PID, pid};
    size_t size = sizeof(*procInfo);
    return sysctl(cmd, sizeof(cmd)/sizeof(*cmd), procInfo, &size, NULL, 0) == 0;
}


+ (NSTimeInte
首页 上一页 1 2 3 4 下一页 尾页 1/4/4
】【打印繁体】【投稿】【收藏】 【推荐】【举报】【评论】 【关闭】 【返回顶部
上一篇ios 自制Framework 获取指定bundl.. 下一篇优先级反转那些事儿

最新文章

热门文章

Hot 文章

Python

C 语言

C++基础

大数据基础

linux编程基础

C/C++面试题目