启动流程
一、背景:介绍怎么fork App进程的
-
launchd是什么
launchd 是 iOS 和 macOS 系统中最早启动的第一个用户态进程,由内核在系统启动完成内核态初始化后直接启动 launchd 进程 PID 永远是 1 **
-
SpringBoard是什么
SpringBoard 是 iOS 的主界面管理器(Home Screen Manager,同时也是系统级事件的中心调度器。你在 iPhone 上看到的 桌面图标(App Icon)布局,文件夹管理,App启动和退出,Dock栏,小组件(Widget),其都是 SpringBoard 负责的
当用户点击图标后唤起 App,SpringBoard和launchd进程配合工作完成启动
- SpringBoard负责查找 app的相关信息 如:Bundle Identifie、Executable路径
- 通过进程间通信发送个launchd进程
- launchd进程校验 App 权限和签名,找到可执行 Mach-O 路径
- launchd进程fork 出一个新子进程
- 子进程内执行
execve()
系统调用,加载 App 的 Mach-O文件
二、内核加载App的Mach-O文件
1. 加载dyld动态链接器
- 读取 Mach-O 文件头
- 解析
LC_LOAD_DYLINKER
,发现要加载/usr/lib/dyld
- 把 dyld 映射进内存
2. dyld加载动态库接管控制权-加载动态库
- 读取其中的
LC_LOAD_DYLIB
,找出依赖的动态库路径 - 读取
__mod_init_func
被写入的方法进行调用 如: attribute((constructor)) c++构造函数 _objc_init方法 - runtime动态库通过该_objc_ini方法通过向动态链接器注册回调函数用于类注册
ini
_dyld_objc_callbacks_v4 callbacks = {
map_images,
load_images,
unmap_image,
_objc_patch_root_of_class,
};
_dyld_objc_register_callbacks((_dyld_objc_callbacks*)&callbacks);
最主要是map_image、load_images两个方法接下啦解释
3. rebase
1.作用?
修正 Mach-O 中原本写死的指针地址,使它指向正确的实际内存地址。
2.为什么要 rebase?
因为 App 或动态库被加载进内存的地址是随机的(ASLR 安全机制),不能用 Mach-O 编译时写死的地址,必须运行时修正。
3.举例说明
假设有一个动态库 libA.dylib
:
c
int globalValue = 100;
int getValue() {
return globalValue;
}
编译时:
- `globalValue` 假设地址是 `0x1000`
- `getValue()` 方法里 `mov eax, [0x1000]`
编译时:
- `globalValue` 假设地址是 `0x1000`
- `getValue()` 方法里 `mov eax, [0x1000]`
运行时:
- 由于 ASLR,libA 被加载到了 `0x300000000`
- `globalValue` 实际地址变成 `0x300001000`
rebase:
- dyld 扫描 Mach-O 的 `__DATA` 和 `__DATA_CONST` 段
- 找到所有相对地址值
- 把 `0x1000` 改成 `0x300001000`
4. bind符号绑定
1.作用?
把 Mach-O 文件中的符号(函数地址 / 全局变量地址)绑定到真正对应的动态库符号地址。
2.为什么要 bind?
因为 Mach-O 里写的只是个占位符,真正符号地址要根据当前进程中其他库的加载情况确定。
3.举例说明
c
int globalValue = 100;
int getValue() {
return globalValue;
}
bind:
如果 `globalValue` 是从其他库导入的符号(比如 Foundation.framework 中的 `_NSConcreteStackBlock`)
- Mach-O 符号表里写的是 `UNDEFINED`,指向一个占位 slot
- dyld 找到 Foundation.framework 中 `_NSConcreteStackBlock` 实际地址,比如 `0x310000000`
- 把符号表 slot 写成 `0x310000000`,完成绑定
三、runtime注册
runtime的注册是通过 map_images, load_images两个回调函数实现 可以参考:juejin.cn/post/749722...
四、调用main函数
在 iOS 中,main 函数通常会通过以下代码启动应用程序:
objectivec
int main(int argc, char * argv[]) {
@autoreleasepool {
return UIApplicationMain(argc, argv, nil, NSStringFromClass([AppDelegate class]));
}
}
UIApplicationMain 函数的调用流程如下:
-
创建应用程序实例:
UIApplicationMain 会创建一个 UIApplication 的实例,通常是 UIApplication.shared,这是应用程序的核心对象,负责管理应用程序的生命周期和事件循环。
-
设置应用程序代理:
UIApplicationMain 会创建一个 AppDelegate 的实例,并将其设置为应用程序的代理。代理对象负责处理应用程序生命周期事件,如启动、终止、进入后台等。
-
启动事件循环:
UIApplicationMain 会启动应用程序的主事件循环,开始接收和处理用户输入事件,如触摸、按键等。
四、首帧渲染时间
首页加载完毕
App启动流程时间监测
T1:进程 --> 第一个load方法执行
- 线程启动的时间
ini
+ (CFAbsoluteTime)processStartTime {
if (__t1 == 0) {
struct kinfo_proc procInfo;
int pid = [[NSProcessInfo processInfo] processIdentifier];
int cmd[4] = {CTL_KERN, KERN_PROC, KERN_PROC_PID, pid};
size_t size = sizeof(procInfo);
if (sysctl(cmd, sizeof(cmd)/sizeof(*cmd), &procInfo, &size, NULL, 0) == 0) {
__t1 = procInfo.kp_proc.p_un.__p_starttime.tv_sec * 1000.0 + procInfo.kp_proc.p_un.__p_starttime.tv_usec / 1000.0;
}
}
return __t1;
}
- 第一个load方法执行的时间
创建这个命名靠前的静态库,load的方法执行和静态库的链接顺序有关,链接顺序又和静态库得命名有关以A开头的会在最前面
T2:load -> main
T3:didFinishLaunch start -> 首诊出现
- 13.0 以上系统使用监听kCFRunLoopBeforeTimers回调作为首帧完成时机 13.0以下系统,使用注册block方法获取首帧完成时机
swift
- (void)startLaunchDetector {
//近似作为main方法开始时间
if (@available(iOS 13.0, *)) {
//13.0 以上系统使用监听kCFRunLoopBeforeTimers回调作为首帧完成时机
CFRunLoopRef mainRunLoop = [[NSRunLoop mainRunLoop] getCFRunLoop];
CFRunLoopActivity allActivitys = kCFRunLoopAllActivities;
CFRunLoopObserverRef observer = CFRunLoopObserverCreateWithHandler(kCFAllocatorDefault, allActivitys, YES, 0, ^(CFRunLoopObserverRef observer, CFRunLoopActivity activity) {
if (activity == kCFRunLoopBeforeTimers) {
self.firstFrameEndTimeInterval = [[NSDate date] timeIntervalSince1970];
CFRunLoopRemoveObserver(mainRunLoop, observer, kCFRunLoopCommonModes);
[[NSNotificationCenter defaultCenter] postNotificationName:BMLauchFirshFrameNotification object:nil];
}
});
CFRunLoopAddObserver(mainRunLoop, observer, kCFRunLoopCommonModes);
} else {
//13.0以下系统,使用注册block方法获取首帧完成时机
CFRunLoopRef mainRunloop = [[NSRunLoop mainRunLoop] getCFRunLoop];
CFRunLoopPerformBlock(mainRunloop,NSDefaultRunLoopMode,^(){
self.firstFrameEndTimeInterval = [[NSDate date] timeIntervalSince1970];
[[NSNotificationCenter defaultCenter] postNotificationName:BMLauchFirshFrameNotification object:nil];
});
}
}
存量问题治理和优化
1、二进制重排
- mach_o加载 Mach-O 文件在加载到内存时,系统是按 虚拟内存页(通常为 4KB 一页)加载的: dyld 会将 __TEXT 和 __DATA 等 segment 按需 mmap 到内存。 只有首次访问时才真正触发 page fault → 加载对应页面。一般是 2
6 页,约 824KB) - 减少page falut 方案: 将启动过程中的方法排列在代码段的前边通过xcode 配置 order file文件, 调整函数的排列顺序
- order file文件的获取 线下通过启动app调用的方法得到函数符号手动放到order file里 OC插桩 xcode中Build Settings --> Other C Flags添加标记
ini
-fsanitize-coverage=trace-pc-guard
swift插桩
ini
-sanitize-coverage=func
-sanitize=undefined
-
插桩原理 修改了二进制文件,在所有的 方法、函数、block 的实现的第一句插入一句 _sanitizer_cov_trace_pc_guard() 代码
-
查看App启动中断次数 通过xcode - Instruments - System Trace来查看App启动时的pageFault次数

2、删除无用类
- 通过对Mach-O文件的了解,可以知道__TEXT:__objc_methname:中包含了代码中的所有方法,而__DATA__objc_selrefs中则包含了所有被使用的方法的引用,通过取两个集合的差集就可以得到所有未被使用的代码.
存量问题治理
梳理相关业务,将不必要的内容尽量放在首页加载完成之后
三、管控增量问题
梳理初始化业务分为几个阶段
设计分发机制讲启动阶段时机分发给业务规范启动项的添加
PreMain
我们利用最后一个执行的启动构造函数时机分发给业务 方法交换
DidFinish
我们hook了代理方法 先于首页业务 bugly
首页加载完成之后
我们注册了runloop监听回调中
框架设计
一、目标
你希望实现:
c
// 在注册函数中标记自己属于哪个阶段
MY_EXPORT static void register_module_A() {
printf("注册阶段 = PRELOAD\n");
}
然后在执行时,比如:
c
run_all_register_functions_for_stage(PRELOAD);
只调用属于 PRELOAD
阶段的函数。
二、解决方案:封装结构体 + 放入 section
步骤 1:定义阶段枚举 + 注册结构体类型
c
typedef enum {
STAGE_PRELOAD,
STAGE_BOOTSTRAP,
STAGE_POST_LAUNCH
} InitStage;
typedef struct {
InitStage stage;
void (*func)(void);
} RegisterItem;
步骤 2:定义宏放进 section
c
#define STAGE_EXPORT(stage, fn) \
__attribute__((used, section("__DATA,MyRegister"))) \
static const RegisterItem _reg_item_##fn = {stage, fn}
步骤 3:写你的注册函数
c
static void register_module_A() {
printf("Module A 注册 - preload\n");
}
static void register_module_B() {
printf("Module B 注册 - bootstrap\n");
}
STAGE_EXPORT(STAGE_PRELOAD, register_module_A);
STAGE_EXPORT(STAGE_BOOTSTRAP, register_module_B);
步骤 4:在启动时按阶段执行
c
extern const RegisterItem __start_MyRegister __asm("section$start$__DATA$MyRegister");
extern const RegisterItem __stop_MyRegister __asm("section$end$__DATA$MyRegister");
void run_all_register_functions_for_stage(InitStage stage) {
const RegisterItem *p = &__start_MyRegister;
while (p < &__stop_MyRegister) {
if (p->stage == stage && p->func) {
p->func();
}
p++;
}
}
注意:
section$start$__DATA$...
这种方式在 Clang + Mach-O 格式(iOS/macOS) 下可用,能正确获取段首尾地址。
三、输出示例
c
run_all_register_functions_for_stage(STAGE_PRELOAD);
// 输出:Module A 注册 - preload
run_all_register_functions_for_stage(STAGE_BOOTSTRAP);
// 输出:Module B 注册 - bootstrap