> For the complete documentation index, see [llms.txt](https://www.doruby.com/llms.txt). Markdown versions of documentation pages are available by appending `.md` to page URLs; this page is available as [Markdown](https://www.doruby.com/mobile/performance/app-qi-dong-you-hua-zhi-yi-main.m-fang-fa-zhi-qian-de-you-hua.md).

# App 启动优化之一：main.m 方法之前的优化

说到APP的启动优化，网上应该也有不少经验总结，不过就我所之相对有些零散，缺乏一些系统性的梳理 。为了让我们有一个完整的认识，也为了能和大家一起学习提高，这里我先抛砖引玉，引入第一篇文章从头梳理APP启动的各个环节，希望对大家有所帮助。

![](https://3933803984-files.gitbook.io/~/files/v0/b/gitbook-legacy-files/o/assets%2F-Lw8fe5ohtBLKudCKXmM%2F-M5VuhlN37OQYPkMPu9C%2F-M5Vv02PLQ42HBXUpNXU%2Fimage.png?alt=media\&token=fd5fbfae-436d-4e8d-88a7-61b00abe6fe1)

我们首先要谈的是在开启一个 APP 的生命周期前，也就是进入 `main.m()` 方法之前，系统要经历怎样的一个加载过程呢？

如果从这里开始分析，就不得不谈到 iOS 操作系统内核的工作流程，它是如何解析我们的二进制包，如何分配内存，然后找到入口，进入 **main()** 方法中的呢？在这个过程中我们还有没有可优化的空间，或者要注意的事呢？

### 关于iOS系统加载APP过程

1. App启动流程的关键节点

   ![](https://3933803984-files.gitbook.io/~/files/v0/b/gitbook-legacy-files/o/assets%2F-Lw8fe5ohtBLKudCKXmM%2F-M5VuhlN37OQYPkMPu9C%2F-M5VuwOiIMJp7G87fj-H%2Fimage.png?alt=media\&token=b16b74eb-886e-40d4-ae36-5574e94f46fa)

   引用自：`Jamin's Blog`
2. 可执行文件的内核流程如下图

   ![](https://3933803984-files.gitbook.io/~/files/v0/b/gitbook-legacy-files/o/assets%2F-Lw8fe5ohtBLKudCKXmM%2F-M5VuhlN37OQYPkMPu9C%2F-M5Vv6qeaPAYJufTSM8z%2Fimage.png?alt=media\&token=29ce8a7a-7227-448f-8067-8a76d998040a)

   引用自《Mac OS X and iOS Internals : To the Apple's Core》
3. 对应到源代码里：

   由于源代码较多，只引用关键性的代码.

```
// /bsd/kern/ker_exec.c
execve(proc_t p, struct execve_args *uap, int32_t *retval) 
{
    __mac_execve(proc_t p, struct __mac_execve_args *uap, int32_t *retval)
    {
        exec_activate_image(struct image_params *imgp)
        {
            for(i = 0; error == -1 && execsw[i].ex_imgact != NULL; i++) {
                // 这里只分析Mach-O这种执行格式，所以exec_fat_imgact和exec_shell_imgact最终都会调到exec_mach_imgact                
                error = (*execsw[i].ex_imgact)(imgp); 

               (*execsw[i].ex_imgact)(imgp) = exec_mach_imgact(imgp)
                exec_mach_imgact(struct image_params *imgp)
                {

                    load_machfile(struct image_params *imgp, ...) 
                    {                        

                        // 设置内存映射，关键点1
                        if (create_map) {
                            vm_map_create();
                        }                                                      
                        // 解析 Mach-O 包核心，关键点2
                           parse_machfile(struct vnode *vp, ..., load_result_t *result)
                           {
                               ...
                          }

                    }

                    ...

                    // 设置入口点，关键点3
                    /* Set the entry point */
                    thread_setentrypoint(thread, load_result.entry_point);                                                      
                    ...
                }

            }
        }
    }

}
```

* 关键点1： vm\_map\_create() 设置内存映射，这和你的APP分配的初始内存大小有关
* 关键点2： 加载解析Mach-O文件，并得到APP入口,重点在这个函数: `parse_machfile()`，下面会细聊到。
* 关键点3：进入入口点（也就是main()函数的入口地址）

如想更深入研究，请下载[源代码](http://opensource.apple.com/tarballs/xnu/xnu-2782.1.97.tar.gz)

#### 关于 `parse_machfile()` 函数做了什么:

* `parse_machfile()` 是递归解析的，最初的递归深度为0，最高深度到6，防止无限递归。使用递归解析，主要是将不同Mach-O文件类型按照依赖关系，分前后进行解析。如解析可执行二进制文件类型(MH\_EXECUTABLE)的Mach-O文件需要调用load\_dylinker来处理加载命令LC\_LOAD\_DYLINKER，而动态链接器也是Mach-O文件，所以就需要递归到不同的深度进行解析；
* 其次，`parse_machfile()` 的每一次递归，在解析加载命令时，会将内核需要解析的加载命令按照加载循序划分为三组进行解析，在代码的体现上就是通过三次循环，每趟循环只关注当前趟需要解析的命令： (1)：解析线程状态，UUID和代码签名。相关命令为LC\_UNIXTHREAD、LC\_MAIN、LC\_UUID、LC\_CODE\_SIGNATURE (2)：解析代码段Segment。相关命令为LC\_SEGMENT、LC\_SEGMENT\_64； (3)：解析动态链接库、加密信息。相关命令为：LC\_ENCRYPTION\_INFO、LC\_ENCRYPTION\_INFO\_64、LC\_LOAD\_DYLINKER
* 解析完可执行二进制文件类型的Mach-O文件(假设为A)之后，我们会得到A的入口点；但线程并不立刻进入到这个入口点。这是由于我们还会加载动态链接器(dyld)，在`load_dylinker()`中，dyld会保存A的入口点，递归调用`parse_machfile()`之后，将线程的入口点设为dyld的入口点；动态链接器dyld完成加载库的工作之后，再将入口点设回A的入口点，程序启动完成；

### 关于要解析的 APP 包

我们最终上线后的 APP是一组文件所组成，Apple 称它们为 **Bundle**

```
xxx.app/
Documents/
Library/
tmp/
...
```

**xxx.app** 就是我们的 app 应用程序，主要包含了执行文件，和一堆资源文件（图片，xib文件）等。其中的执行文件是 Mach-O(Mach-Object) 格式的二进制文件。

Mach-O文件可能有多种架构组成，因为不同的iOS设备对应的CPU架构也是不同的，所以为了得到最优的执行效率，这个二进制包会有多个不同的架构所组成的包，比如 armv7, armv7s。

Mach-O 文件是一个独有的二进制可执行文件格式。虽然iOS/OS X采用了类UNIX的Darwin操作系统核心，完全符合UNIX标准系统，但在执行文件上，却没有支持UNIX的[ELF](https://en.wikipedia.org/wiki/Executable_and_Linkable_Format)。<br>

![](https://3933803984-files.gitbook.io/~/files/v0/b/gitbook-legacy-files/o/assets%2F-Lw8fe5ohtBLKudCKXmM%2F-M5VuhlN37OQYPkMPu9C%2F-M5VvMKD6dyCKyXYA3rN%2Fimage.png?alt=media\&token=5a21729d-e5cf-4150-bb8f-eebae583e3cb)

### 深入分析 Load Commends

这些加载命令在Mach-O文件加载解析时，被内核加载器或者动态链接器调用，指导如何设置加载对应的二进制数据段。

接下来我们来对比一下，一个最简单的APP（XCode模板创建的Signle View Application）与之后又多添加了几个动态链库的不同，观察一下Load Commends：<br>

![](https://3933803984-files.gitbook.io/~/files/v0/b/gitbook-legacy-files/o/assets%2F-Lw8fe5ohtBLKudCKXmM%2F-M5VuhlN37OQYPkMPu9C%2F-M5VvSIkcuG8AmwjqEwz%2Fimage.png?alt=media\&token=79ab69ac-4a83-41f6-b399-792e1c317b48)

可以看到即使最简单的APP，也链接了一个 APP 所必需的动态链接库(UIKit等)。如果我们在XCode 里这样引入了更多的动态链接库，像这样：

![](https://3933803984-files.gitbook.io/~/files/v0/b/gitbook-legacy-files/o/assets%2F-Lw8fe5ohtBLKudCKXmM%2F-M5VuhlN37OQYPkMPu9C%2F-M5VvWNqmMtSRbDA-78H%2Fimage.png?alt=media\&token=0faab3ef-03c6-441b-aaff-8b514d150fcb)

就会多出这几项的链接库。

*(ps: 一些黑客就是利用这一点来嵌入自己的动态链接库以达到修改APP的目的)*

**LC\_MAIN** 对应的就是APP的入口。

### 总结可能存在的优化点

有了上述的分析我们可以总结一下可能存在的优化点。

1. **减少动态链接库**

   内核在到达入口之前，会先装载所有的动态链接库。所以尽可能用静态链接库。
2. **减少不必要的 ObjectC 对象**

   OC创建的类，会在DATA SEGMENT 中占用较多的 VM Size，即使你创建了一个空的Class。因为本身这个对象就会留下一空较大的内存区域来创建与管理，虽然现在的内存分配已经够快，但也会有一定的开销。
3. **特别注意 load() 方法里的内容**

   在 main 入口被解析执行到之前，所有的OC实现类都会先执行`load()`方法。如果你有定制这个方法，特别是在方法里有太多外部依赖时，也会额外增加解析的时间。所以如果有特别耗时的操作在这里请谨慎!

#### 参考资料

* [Jamin's Blog](http://oncenote.com/2015/06/01/How-App-Launch/)
* [Mac OS iOS Internals Apples](http://www.amazon.com/Mac-OS-iOS-Internals-Apples/dp/1118057651)

以上观点仅代表个人，由于水平有限，可能有遗漏和不对的地方，欢迎一起来纠正，讨论！
