一文搞定App启动流程、时间监测、优化措施

启动流程

一、背景:介绍怎么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 函数的调用流程如下:

  1. 创建应用程序实例:

    UIApplicationMain 会创建一个 UIApplication 的实例,通常是 UIApplication.shared,这是应用程序的核心对象,负责管理应用程序的生命周期和事件循环。

  2. 设置应用程序代理:

    UIApplicationMain 会创建一个 AppDelegate 的实例,并将其设置为应用程序的代理。代理对象负责处理应用程序生命周期事件,如启动、终止、进入后台等。

  3. 启动事件循环:

    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、二进制重排

  1. mach_o加载 Mach-O 文件在加载到内存时,系统是按 虚拟内存页(通常为 4KB 一页)加载的: dyld 会将 __TEXT 和 __DATA 等 segment 按需 mmap 到内存。 只有首次访问时才真正触发 page fault → 加载对应页面。一般是 26 页,约 824KB)
  2. 减少page falut 方案: 将启动过程中的方法排列在代码段的前边通过xcode 配置 order file文件, 调整函数的排列顺序
  3. 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
  1. 插桩原理 修改了二进制文件,在所有的 方法、函数、block 的实现的第一句插入一句 _sanitizer_cov_trace_pc_guard() 代码

  2. 查看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
相关推荐
传奇开心果编程3 小时前
【Compose Multiplatform 跨端开发学与练】第3课 布局与组件
android·windows·学习·ui·ios·kotlin·composer
tink4 小时前
告别 Xcode IDE:用 VS Code + SweetPad + XcodeGen 开发 iOS 应用的完整指南
ios·swiftui·xcode
传奇开心果编程6 小时前
【Compose Multiplatform 跨端开发学与练】第8课 资源管理与主题
android·windows·学习·ios·kotlin·web·composer
传奇开心果编程7 小时前
【Compose Multiplatform 跨端开发学与练】第9课 测试与调试
android·学习·macos·ios·kotlin·web·composer
传奇开心果编程7 小时前
【Compose Multiplatform 跨端开发学与练】第4课 导航与路由
android·windows·学习·ui·ios·kotlin·composer
黑科技iOS上架7 小时前
iOS隐私合规扫描工具
ios·审核
传奇开心果编程8 小时前
【Compose Multiplatform 跨端开发学与练】第6课 状态管理与架构
android·学习·ui·ios·架构·kotlin·composer
传奇开心果编程8 小时前
【Compose Multiplatform 跨端开发学与练】第2课 Compose 基础语法
android·windows·学习·ui·ios·kotlin·composer
可乐鸡翅yeah_8 小时前
hls.js 缓冲区参数 maxBufferLength、maxBufferSize 通俗讲解与业务调参
开发语言·javascript·ios·音视频·safari·m3u8
茶底世界之下1 天前
Metal 冷启动并发下,Pipeline Cache 为什么必须合并在途编译
ios·swift