面试02-iOS启动周期

iOS 启动流程

1: 内核加载

  解析 Mach-O​ 头

  创建进程,分配 PID、分配内存空间 

  跳转到 dyld(动态链接器)入口

2:dyld加载

  加载共享缓存

  加载动态库

  rebase/bind 从定位

  ObjC Runtime 初始化

3:Runtime 初始化 

      runtime_init() 初始化 SideTables、AutoreleasePoolPage、Category 哈希表

     map_images   读取 Mach-O 中的 __objc_classlist、__objc_selrefs等段,注册类和 selector
     load_images 调用所有类的 +load方法

  Initializers (初始化函数)   

    

iOS APP 领启动核心流程

 

iOS 冷启动核心链路-详细

 

pre-main 阶段详解

pre-main 阶段由 dyld(动态链接器)主导,是启动优化最关键的环节。

1. 加载可执行文件与 dyld
系统内核在创建进程后,会 mmap App 的 Mach-O 文件,并从中读取 LC_LOAD_DYLINKER 命令,找到 /usr/lib/dyld 的路径,将 dyld 加载到内存中,并将入口地址设为 _dyld_start。从这一刻起,控制权就从内核交给了 dyld。

2. 递归加载依赖的动态库
dyld 会解析 Mach-O 的 LC_LOAD_DYLIB 加载命令,获取所有依赖的动态库列表(包括 libSystem、libobjc、UIKit 等),然后递归地加载这些库。在 iOS 13+ 中,dyld3 会利用启动闭包缓存,将依赖列表、修复地址等信息存储在沙盒中,避免每次启动都重新解析,大幅提升了加载速度。

3. Rebase 与 Bind(指针修复)
由于 ASLR(地址空间布局随机化)的存在,每个镜像的加载地址都是随机的,因此需要修正指针。

  • Rebase:修正指向镜像内部的指针。dyld 会遍历 __DATA 段,将内部指针加上 ASLR 的随机偏移量(slide)。

  • Bind:修正指向镜像外部的指针。例如调用 NSLog 时,dyld 需要查询符号表,找到其真实地址并写入 __got 或 __la_symbol_ptr 段。

4. ObjC Runtime 初始化
dyld 会调用 _objc_init,开始初始化 Objective-C Runtime:

  • 注册所有类、元类、协议,将 Category 的方法插入到类的方法列表中。

  • 初始化 SideTables(包含 weak 表、引用计数溢出表等),这正是上一轮对话中提到的 weak 表在 main 之前就被创建的原因。

5. Initializers(初始化函数)
Runtime 初始化完成后,dyld 会执行所有注册的初始化函数:

  • 调用所有 ObjC 类的 +load 方法(父类先于子类,类先于分类)。

  • 执行 C/C++ 的 constructor 函数和 __attribute__((constructor)) 标记的函数。

  • 执行 C++ 静态对象的构造函数。

6. 返回 main 函数地址
所有初始化工作完成后,dyld 会查找 LC_MAIN 加载命令,获取 main() 函数的地址,然后跳转执行,正式进入 main() 阶段。

main() 之后阶段详解

进入 main() 函数后,启动流程进入 UIKit 主导的应用生命周期阶段。

1. UIApplicationMain 的调用
main() 函数的核心是调用 UIApplicationMain。这个函数会创建 UIApplication 单例对象,并根据传入的 AppDelegate 类名创建 AppDelegate 实例,同时建立主 RunLoop 和事件处理机制。

2. AppDelegate 回调
UIApplicationMain 会依次调用 AppDelegate 中的方法:

  • application(_:willFinishLaunchingWithOptions:):可选实现,用于 Launch 前的最后准备。

  • application(_:didFinishLaunchingWithOptions:):最常用,在此创建 UIWindow,设置 rootViewController,并调用 makeKeyAndVisible()。

3. Scene Delegate(iOS 13+)
对于采用 Scene 架构的 App,UIKit 会调用 scene(_:willConnectTo:options:) 来完成 UI 连接与场景配置。

4. 首个 ViewController 生命周期
随后,第一个 UIViewController 的生命周期方法被依次调用:loadView → viewDidLoad → viewWillAppear → viewDidAppear。当 viewDidAppear 执行完毕,系统会移除启动画面,App 的启动流程正式结束。

冷启动与热启动

启动过程分为两种类型:

  • 冷启动:进程不在系统中,需要完整地创建进程、加载链接库、初始化 Runtime。App 首次启动、重启后启动、版本更新后启动都属于冷启动。

  • 热启动:App 被退到后台,进程仍在系统中,再次启动时只需重新激活,几乎不需要重新加载和初始化,速度极快。

启动优化要点

基于以上流程,启动优化的核心思路是减少 pre-main 阶段的工作量和延迟 main() 阶段的非关键任务。

  • 减少动态库数量:合并或使用静态库,减少 dylib 的加载和链接时间。

  • 减少类与分类数量:每个类和分类都会在 ObjC Setup 阶段产生元数据,减少它们可以缩短 Rebase/Bind 和 ObjC Setup 的时间。

  • 谨慎使用 +load:+load 在 pre-main 阶段执行,耗时操作会直接拖慢启动速度。非必要逻辑可移至 +initialize 或 didFinishLaunching 中。

  • 延迟初始化:将非首屏必需的 SDK、数据库、网络配置等初始化任务,从 didFinishLaunching 中移出,改为懒加载或异步执行。

总结一下,iOS 启动的本质是 内核 → dyld → Runtime → UIKit 的控制权交接过程。理解这个链条,就能精准定位启动瓶颈,知道每一步“时间花在了哪里”以及“可以从哪里优化”。

 

 

 

Runtime 初始化流程(深度剖析)

 

一、总览:Runtime 初始化的触发时机

Runtime 不是 app 启动时第一个运行的,它是在 dyld 加载完所有镜像(Image)并完成绑定后,由 dyld 回调触发的。

dyld::_main()

    ↓

加载所有动态库 & 链接

    ↓

通知 runtime:镜像已加载(map_images)

    ↓

通知 runtime:镜像已绑定完成(load_images)

    ↓

runtime 完成初始化

 

二、_objc_init—— Runtime 的"入口函数"

源码位置:objc-os.mm

void _objc_init(void) {

    static bool initialized = false;

    if (initialized) return;

    initialized = true;

 

    // 1. 环境变量初始化

    environ_init();

 

    // 2. 线程局部存储

    tls_init();

 

    // 3. C++ 静态构造函数(静态 ObjC 对象)

    static_init();

 

    // 4. runtime 内部数据结构初始化

    runtime_init();

 

    // 5. 异常处理

    exception_init();

 

    // 6. 缓存初始化

    cache_init();

 

    // 7. block 实现初始化

    _imp_implementationWithBlock_init();

 

    // 8. 注册 dyld 通知(关键!)

    dyld_register_image_state_change_handler(

        dyld_image_state_bound,

        1,  // batch

        &load_images

    );

 

    dyld_register_image_state_change_handler(

        dyld_image_state_dependents_initialized,

        0,  // not batch

        &map_images

    );

}

 

关键点:_objc_init本身只是"登记",真正的类注册和 +load调用是 dyld 后续回调 触发的。

 

三、map_images—— 类、协议、selector 的注册

当 dyld 完成一个镜像的加载(但还没 bind 完),会回调 map_images。

3.1 调用链

map_images(count, paths, mh)

    ↓

map_images_nolock(count, paths, mh)

    ↓

_read_images(hInfo, mhCount)    ← 核心函数

3.2 _read_images做了什么(重点)

这是 runtime 初始化中最重的一个函数,逐个处理 Mach-O 中的 __objc_*段:

步骤

处理内容

数据结构

1

读取 __objc_classlist,分配 class_t结构体

NXMapTable(类名→类)

2

读取 __objc_selrefs,注册所有 selector

NSSet(全局 selector 表)

3

读取 __objc_protolist,注册协议

NXMapTable(协议名→协议)

4

读取 __objc_classrefs,修复类引用

修正指针

5

读取 __objc_superrefs,修复父类引用

修正指针

6

处理 Category(只是读取,不合并方法)

存入 category 哈希表

7

修复 class 的 isa指针(指向元类)

isa 初始化

8

将类加入 runtime 的可迭代列表

allocatedClasses

关键源码(简化):

void _read_images(header_info **hList, uint32_t hCount) {

    // 1. 注册 selector

    for (i = 0; i < hCount; i++) {

        SEL *sels = _getObjc2SelectorRefs(hList[i]);

        for (j = 0; j < count; j++) {

            sel_registerNameNoLock(sels[j], NO);

        }

    }

 

    // 2. 分配类

    for (i = 0; i < hCount; i++) {

        classref_t *classes = _getObjc2ClassList(hList[i]);

        for (j = 0; j < count; j++) {

            Class cls = readClass(classes[j]);

        }

    }

 

    // 3. 修复类引用

    for (i = 0; i < hCount; i++) {

        classref_t *refs = _getObjc2ClassRefs(hList[i]);

        for (j = 0; j < count; j++) {

            remapClassRef(refs[j]);

        }

    }

 

    // 4. 处理 Category

    for (i = 0; i < hCount; i++) {

        category_t **cats = _getObjc2CategoryList(hList[i]);

        for (j = 0; j < count; j++) {

            addUnattachedCategoryForClass(cats[j]);

        }

    }

 

    // 5. 修复 isa

    for (i = 0; i < hCount; i++) {

        classref_t *classes = _getObjc2ClassList(hList[i]);

        for (j = 0; j < count; j++) {

            fixupMessageRef(cls);

        }

    }

}

 

3.3 为什么 selector 要注册到全局表?

  • Selector 本质是字符串(const char *)
  • 为了 SEL比较用指针比较(==)而不是字符串比较
  • 所有镜像的 selector 统一注册,保证全局唯一

 

四、load_images—— +load方法的执行

当 dyld 完成一个镜像的所有依赖初始化后,回调 load_images。

4.1 调用链

load_images(hInfo, mh)

    ↓

prepare_load_methods(hInfo)    ← 收集所有 +load 方法

    ↓

call_load_methods()            ← 执行 +load

4.2 prepare_load_methods—— 收集顺序

void prepare_load_methods(header_info *hi) {

    // 1. 先收集类的 +load(父类优先)

    classref_t *classes = _getObjc2NonlazyClassList(hi);

    for (i = 0; i < count; i++) {

        schedule_class_load(classes[i]);

    }

 

    // 2. 再收集 Category 的 +load

    category_t **cats = _getObjc2CategoryList(hi);

    for (i = 0; i < count; i++) {

        add_category_to_loadable_list(cats[i]);

    }

}

schedule_class_load递归保证父类先执行:

static void schedule_class_load(Class cls) {

    if (cls->hasLoadMethod()) {

        schedule_class_load(cls->superclass);  // 递归父类

        add_class_to_loadable_list(cls);

    }

}

4.3 call_load_methods—— 执行顺序

void call_load_methods(void) {

    do {

        // 1. 先执行所有类的 +load

        while (loadable_classes_used > 0) {

            call_class_loads();

        }

 

        // 2. 再执行 Category 的 +load

        more_categories = call_category_loads();

 

    } while (more_categories);

}

 

关键细节:

  • +load是直接通过函数指针调用((*load_method)(cls, SEL_load)),不是消息发送
  • 所以 +load不触发消息转发,也不走 objc_msgSend
  • 这也是为什么 +load的调用速度比普通方法快

 

五、Category 的合并时机

这是一个经典面试题:Category 的方法什么时候合并到主类?

答案:不是在 runtime 初始化时,而是在第一次消息发送前

具体流程:

runtime 初始化(map_images)

    ↓

Category 只是被读取,存入 unattachedCategories 哈希表

    ↓

+load 执行(此时 Category 方法还没合并)

    ↓

第一次消息发送 → lookUpImpOrForward

    ↓

realizeClass → methodizeClass

    ↓

attachCategories → 合并 Category 方法到 class 的 method_list

源码关键路径:

// methodizeClass 中

static void methodizeClass(Class cls) {

    // 合并 Category 方法

    attachCategories(cls);

}

 

// attachCategories

static void attachCategories(Class cls, category_list *cats) {

    // 将 Category 的方法列表附加到类的方法列表前面

    // 这就是为什么 Category 方法会"覆盖"原类方法

}

注意:Category 方法不是"替换",而是"插入到方法列表前面",所以 class_copyMethodList会返回 Category 的方法在前。

 

六、类 realization(实现化)

类在 runtime 初始化时只是"注册",不是"实现"。

6.1 什么是 realized?

状态

含义

注册

类结构体已分配,名字已记录

实现(realized)

父类链已解析、ivar 布局已计算、方法已合并

6.2 realization 的触发时机

Class realizeClass(Class cls) {

    // 1. 递归 realize 父类

    if (cls->superclass) {

        realizeClass(cls->superclass);

    }

 

    // 2. 计算 ivar 布局

    // 3. 修正 isa

    // 4. 合并 Category

    // 5. 标记为 realized

}

触发条件(任一):

  • 第一次向该类发送消息
  • 调用 objc_allocateClassPair
  • 调用 NSClassFromString
  • 调用 class_getInstanceMethod等 runtime API

 

七、完整时序图(文字版)

dyld 启动

    │

    ├── _objc_init()

    │       ├── environ_init()

    │       ├── runtime_init()      ← SideTables, AutoreleasePool 等

    │       ├── cache_init()

    │       └── 注册 dyld 回调

    │

    ├── map_images() 回调

    │       └── _read_images()

    │               ├── 注册 selector(全局表)

    │               ├── 分配类结构体

    │               ├── 修复类引用

    │               ├── 处理 Category(仅读取)

    │               └── 修复 isa

    │

    ├── load_images() 回调

    │       ├── prepare_load_methods()

    │       │       ├── 递归收集类的 +load(父类优先)

    │       │       └── 收集 Category 的 +load

    │       └── call_load_methods()

    │               ├── 执行类的 +load(函数指针直接调用)

    │               └── 执行 Category 的 +load

    │

    ├── main() 函数

    │

    └── 第一次消息发送

            └── lookUpImpOrForward()

                    ├── realizeClass()(如果未实现)

                    │       ├── 递归 realize 父类

                    │       ├── 计算 ivar 布局

                    │       └── attachCategories()

                    └── 查找方法 IMP

 

posted on 2026-09-20 21:35  风zk  阅读(15)  评论(0)    收藏  举报

导航