xcode编译过程

作者: CoderLF | 来源:发表于2018-12-17 20:42 被阅读14次

    在 iOS 开发的过程中,Xcode 为我们提供了非常完善的编译能力,正常情况下,我们只需要 Command + R 就可以将应用运行到设备上,即使打包也是一个相对愉快的过程。

    但正如我们写代码无法避开 Bug 一样,项目在编译的时候也会出现各种各样的错误,最痛苦的莫过于处理这些错误。其中的各种报错都不是我们在日常编程中所能接触的,而我们无法快速精准的定位错误并解决的唯一原因就是我们根本不知道在编译的时候都做了些什么,都需要些什么。就跟使用一个新的类,如果不去查看其代码,永远也无法知道它到底能干什么一样。

    这篇文章将从由简入繁的讲解 iOS App 在编译的时候到底干了什么。一个 iOS 项目的编译过程是比较繁琐的,针对源代码、xib、framework 等都将进行一定的编译和操作,再加上使用 Cocoapods,会让整个过程更加复杂。这篇文章将以 Swift 和 Objective-C 的不同角度来分析。

    1. 什么是编译

    在开始之前,我们必须知道什么是编译?为什么要进行编译?

    CPU 由上亿个晶体管组成,在运行的时候,单个晶体管只能根据电流的流通或关闭来确认两种状态,我们一般说 0 或 1,根据这种状态,人类创造了二进制,通过二进制编码我们可以表示所有的概念。但是,CPU 依然只能执行二进制代码。我们将一组二进制代码合并成一个指令或符号,创造了汇编语言,汇编语言以一种相对好理解的方式来编写,然后通过汇编过程生成 CPU 可以运行的二进制代码并运行在 CPU 上。

    但是使用汇编语言开发仍然是一个相对痛苦的过程,于是通过上述方式,c、c++、Java 等语言就一层一层的被发明出来。Objective-c 和 Swift 就是这样一个过程,他们的基础都是 c 和 c++。

    当我们使用 Objective-c 和 Swift 编写代码后,想要代码能运行在 CPU 上,我们必须进行编译,将我们写好的代码编译为机器可以理解的二进制代码。

    1.1 LLVM

    有了上面的简单介绍,可以发现,编译其实是一个用代码解释代码的过程。在 Objective-c 和 Swift 的编译过程中,用来解释代码的,就是 LLVM。点击可以看到 LLVM 的官方网站,在 Overview 的第一行就说明了 LLVM 到底是什么:

    The LLVM Project is a collection of modular and reusable compiler and toolchain technologies. Despite its name, LLVM has little to do with traditional virtual machines. The name “LLVM” itself is not an acronym; it is the full name of the project.
    LLVM 项目是一个模块化、可重用的编译器、工具链技术的集合。尽管它的名字叫 LLVM,但它与传统虚拟机的关系并不大。“LLVM”这个名字本身不是一个缩略词; 它的全称是这个项目。
    // LLVM 命名最早源自于底层虚拟机(Low Level Virtual Machine)的缩写。

    LVVM 的作者写了一篇关于什么是 LLVM 的文章,详细的描述了 LLVM 的使用的技术点:LLVM

    简单的说,LLVM 是一个项目,其作用就是提供一个广泛的工具,可以将任何高级语言的代码编译为任何架构的 CPU 都可以运行的机器代码。它将整个编译过程分类了三个模块:前端、公用优化器、后端。(这里不要去思考任何关于 web 前端和 service 后端的概念。)

    • 前端:对目标语言代码进行语法分析,语义分析,生成中间代码。在这个过程中,会进行类型检查,如果发现错误或者警告会标注出来在哪一行。我们在开发的过程中,其实 Xcode 也会使用前端工具对你的代码进行分析,并实时的检查出来某些错误。前端是针对特定语言的,如果需要一个新的语言被编译,只需要再写一个针对新语言的前端模块即可。
    • 公用优化器:将生成的中间文件进行优化,去除冗余代码,进行结构优化。
    • 后端:后段将优化后的中间代码再次转换,变成汇编语言,并再次进行优化,最后将各个文件代码转换为机器代码并链接。链接是指将不同代码文件编译后的不同机器代码文件合并成一个可执行文件。

    虽然目前 LLVM 并没有达到其目标(可以编译任何代码),但是这样的思路是很优秀的,在日常开发中,这种思路也会为我们提供不少的帮助。

    1.2 clang

    clang 是 LLVM 的一个前端,它的作用是针对 C 语言家族的语言进行编译,像 c、c++、Objective-C。而 Swift 则自己实现了一个前端来进行 Swift 编译,优化器和后端依然是使用 LLVM 来完成,后面会专门对 Swift 语言的 前端编译流程进行分析。

    上面简单的介绍了为什么需要编译,以及 Objectie-C 和 Swift 代码的编译思路。这是基础,如果没有这些基础,后面针对我们整个项目的编译就无法理解,如果你理解了上面的知识点,那么下面将要讲述的整个项目的编译过程就会显得很简单了。

    2. iOS 项目编译过程简介

    Xcode 在编译 iOS 项目的时候,使用的正是 LLVM,其实我们在编写代码以及调试的时候也在使用 LLVM 提供的功能。例如代码高亮(clang)、实时代码检查(clang)、代码提示(clang)、debug 断点调试(LLDB)。这些都是 LLVM 前端提供的功能,而对于后端来说,我们接触到的就是关于 arm64、armv7、armv7s 这些 CPU 架构了,记得之前还有 32 位架构处理器的时候,设定指定的编译的目标 CPU 架构就是一个比较痛苦的过程。

    下面来简单的讲讲整个 iOS 项目的编译过程,其中可能会有一些疑问,先保留着,后面会详细解释:

    我们的项目是一个 target,一个编译目标,它拥有自己的文件和编译规则,在我们的项目中可以存在多个子项目,这在编译的时候就导致了使用了 Cocoapods 或者拥有多个 target 的项目会先编译依赖库。这些库都和我们的项目编译流程一致。Cocoapods 的原理解释将在文章后面一部分进行解释。

    1. 写入辅助文件:将项目的文件结构对应表、将要执行的脚本、项目依赖库的文件结构对应表写成文件,方便后面使用;并且创建一个 .app 包,后面编译后的文件都会被放入包中;
    2. 运行预设脚本:Cocoapods 会预设一些脚本,当然你也可以自己预设一些脚本来运行。这些脚本都在 Build Phases 中可以看到;
    3. 编译文件:针对每一个文件进行编译,生成可执行文件 Mach-O,这过程 LLVM 的完整流程,前端、优化器、后端;
    4. 链接文件:将项目中的多个可执行文件合并成一个文件;
    5. 拷贝资源文件:将项目中的资源文件拷贝到目标包;
    6. 编译 storyboard 文件:storyboard 文件也是会被编译的;
    7. 链接 storyboard 文件:将编译后的 storyboard 文件链接成一个文件;
    8. 编译 Asset 文件:我们的图片如果使用 Assets.xcassets 来管理图片,那么这些图片将会被编译成机器码,除了 icon 和 launchImage;
    9. 运行 Cocoapods 脚本:将在编译项目之前已经编译好的依赖库和相 关资源拷贝到包中。
    10. 生成 .app 包
    11. 将 Swift 标准库拷贝到包中
      12 .对包进行签名
    12. 完成打包

    在上述流程中:2 - 9 步骤的数量和顺序并不固定,这个过程可以在 Build Phases 中指定。Phases:阶段、步骤。这个 Tab 的意思就是编译步骤。其实不仅我们的整个编译步骤和顺序可以被设定,包括编译过程中的编译规则(Build Rules)和具体步骤的参数(Build Settings),在对应的 Tab 都可以看到。关于整个编译流程的日志和设定,可以查看这篇文章:Build 过程,跟着它的步骤来查看自己的项目将有助于你理解整个编译流程。后面也会详细讲解这些内容。

    查看对应位置的方法:在 Xcode 中选择自己的项目,在 targets 中选择自己的项目,就可以看到对应的 Tab 。

    3. 文件编译过程

    Objective-C 的文件中,只有 .m 文件会被编译 .h 文件只是一个暴露外部接口的头文件,它的作用是为被编译的文件中的代码做简单的共享。下面拿一个单独的类文件进行分析。这些步骤中的每一步你都可以使用 clang 的命令来查看其进度,记住 clang 是一个命令行工具,它可以直接在终端中运行。这里我们使用 c 语言作为例子类进行分析,它的过程和 Objective-C 一样,后面 3.7 会讲到 Swift 文件是如何被编译的。

    3.1 预处理

    在我们的代码中会有很多 #import 宏,预处理的第一步就是将 import 引入的文件代码放入对应文件。

    然后将自定义宏替换,例如我们定义了如下宏并进行了使用:

    #define Button_Height 44
    #define Button_Width 100
    
    button.frame = CGRectMake(0, 0, Button_Width, Button_Height);
    

    那么代码将被替换为:

    button.frame = CGRectMake(0, 0, 44, 100);
    

    按照这样的思路可以发现,在自定义宏的时候要格外小心,尤其是一些携带参数和功能的宏,这些宏也只是简单的直接替换代码,不能真的代替方法或函数,中间会有很多问题。

    在将代码完全拆开后,将会对代码进行符号化,对于分析代码的代码 (clang),我们写的代码就是一些字符串,为了后面给这些代码进行语法和语义分析,需要将我们的代码进行标记并符号化,例如一段 helloworld 的 c 代码:

    #include <stdio.h>
    int main(int argc, char *argv[])
    {
        printf("Hello World!\n");
        return 0;
    }
    

    使用 clang 命令 clang -Xclang -dump-tokens helloworld.c 转化后的代码如下(去掉了 stdio.h 中的内容):

    int 'int'    [StartOfLine]  Loc=<helloworld.c:2:1>
    identifier 'main'    [LeadingSpace] Loc=<helloworld.c:2:5>
    l_paren '('     Loc=<helloworld.c:2:9>
    int 'int'       Loc=<helloworld.c:2:10>
    identifier 'argc'    [LeadingSpace] Loc=<helloworld.c:2:14>
    comma ','       Loc=<helloworld.c:2:18>
    char 'char'  [LeadingSpace] Loc=<helloworld.c:2:20>
    star '*'     [LeadingSpace] Loc=<helloworld.c:2:25>
    identifier 'argv'       Loc=<helloworld.c:2:26>
    l_square '['        Loc=<helloworld.c:2:30>
    r_square ']'        Loc=<helloworld.c:2:31>
    r_paren ')'     Loc=<helloworld.c:2:32>
    l_brace '{'  [StartOfLine]  Loc=<helloworld.c:3:1>
    identifier 'printf'  [StartOfLine] [LeadingSpace]   Loc=<helloworld.c:4:2>
    l_paren '('     Loc=<helloworld.c:4:8>
    string_literal '"Hello World!\n"'       Loc=<helloworld.c:4:9>
    r_paren ')'     Loc=<helloworld.c:4:25>
    semi ';'        Loc=<helloworld.c:4:26>
    return 'return'  [StartOfLine] [LeadingSpace]   Loc=<helloworld.c:5:2>
    numeric_constant '0'     [LeadingSpace] Loc=<helloworld.c:5:9>
    semi ';'        Loc=<helloworld.c:5:10>
    r_brace '}'  [StartOfLine]  Loc=<helloworld.c:6:1>
    eof ''      Loc=<helloworld.c:6:2>
    

    这里,每一个符号都会标记出来其位置,这个位置是宏展开之前的位置,这样后面如果发现报错,就可以正确的提示错误位置了。针对 Objective-C 代码,我们只需要转化对应的 .m 文件就可以查看。

    3.2 语意和语法分析
    3.2.1 AST

    对代码进行标记之后,其实就可以对代码进行分析,但是这样分析起来的过程会比较复杂。于是 clang 又进行了一步转换:将之前的标记流转换为一颗抽象语法树(abstract syntax tree – AST)。

    使用 clang 命令 clang -Xclang -ast-dump -fsyntax-only helloworld.c,转化后的树如下(去掉了 stdio.h 中的内容):

    `-FunctionDecl 0x7f8eaf834bb0 <helloworld.c:2:1, line:6:1> line:2:5 main 'int (int, char **)'
      |-ParmVarDecl 0x7f8eaf8349b8 <col:10, col:14> col:14 argc 'int'
      |-ParmVarDecl 0x7f8eaf834aa0 <col:20, col:31> col:26 argv 'char **':'char **'
      `-CompoundStmt 0x7f8eaf834dd8 <line:3:1, line:6:1>
        |-CallExpr 0x7f8eaf834d40 <line:4:2, col:25> 'int'
        | |-ImplicitCastExpr 0x7f8eaf834d28 <col:2> 'int (*)(const char *, ...)' <FunctionToPointerDecay>
        | | `-DeclRefExpr 0x7f8eaf834c68 <col:2> 'int (const char *, ...)' Function 0x7f8eae836d78 'printf' 'int (const char *, ...)'
        | `-ImplicitCastExpr 0x7f8eaf834d88 <col:9> 'const char *' <BitCast>
        |   `-ImplicitCastExpr 0x7f8eaf834d70 <col:9> 'char *' <ArrayToPointerDecay>
        |     `-StringLiteral 0x7f8eaf834cc8 <col:9> 'char [14]' lvalue "Hello World!\n"
        `-ReturnStmt 0x7f8eaf834dc0 <line:5:2, col:9>
          `-IntegerLiteral 0x7f8eaf834da0 <col:9> 'int' 0
    

    这是一个 main 方法的抽象语法树,可以看到树顶是 FunctionDecl:方法声明(Function Declaration)。

    这里因为截取了部分代码,其实并不是整个树的树顶。真正的树顶描述应该是:TranslationUnitDecl。

    然后是两个 ParmVarDecl:参数声明。

    接着下一层是 CompoundStmt:说明下面有一组复合的声明语句,指的是我们的 main 方法里面所使用到的所有代码。

    再到里面就是每一行代码的使用,方法的调用,传递的参数,以及返回。在实际应用中还会有变量的声明、操作符的使用等。

    关于 AST 的详细解释可以查看:Introduction to the Clang AST

    3.2.2 静态分析

    有了这样的语法树,对代码的分析就会简单许多。对这棵树进行遍历分析,包括类型检查、实现检查(某个类是否存在某个方法)、变量使用,还会有一些复杂的检查,例如在 Objective-C 中,给某一个对象发送消息(调用某个方法),检查这个对象的类是否声明这个方法(但并不会去检查这个方法是否实现,这个错误是在运行时进行检查的),如果有什么错误就会进行提示。因此可见,Xcode 对 clang 做了非常深度的集成,在编写代码的过程中它就会使用 clang 来对你的代码进行分析,并及时的对你的代码错误进行提示。

    3.3 生成 LLVM 代码

    当确认代码没有问题后(静态分析可分析出来的问题),前端就将进入最后一步:生成 LLVM 代码,并将代码递交给优化器。

    使用命令 clang -S -emit-llvm helloworld.c -o helloworld.ll 将生成 LLVM IR。

    The most important aspect of its design is the LLVM Intermediate Representation (IR), which is the form it uses to represent code in the compiler. LLVM IR is designed to host mid-level analyses and transformations that you find in the optimizer section of a compiler. It was designed with many specific goals in mind, including supporting lightweight runtime optimizations, cross-function/interprocedural optimizations, whole program analysis, and aggressive restructuring transformations, etc. The most important aspect of it, though, is that it is itself defined as a first class language with well-defined semantics.

    其设计的最重要的部分是 LLVM 中间表示(IR),它是一种在编译器中表示代码的形式。LLVM IR 旨在承载在编译器的优化器中间的分析和转换。它的设计考虑了许多特定的目标,包括支持轻量级运行时优化,跨功能/进程间优化,整个程序分析和积极的重组转换等等。但它最重要的方面是它本身被定义为具有明确定义的语义的第一类语言。

    例如我们上面的代码将会被生成为:

    ; ModuleID = 'helloworld.c'
    source_filename = "helloworld.c"
    target datalayout = "e-m:o-i64:64-f80:128-n8:16:32:64-S128"
    target triple = "x86_64-apple-macosx10.12.0"
    
    @.str = private unnamed_addr constant [14 x i8] c"Hello World!\0A\00", align 1
    
    ; Function Attrs: nounwind ssp uwtable
    define i32 @main(i32, i8**) #0 {
      %3 = alloca i32, align 4
      %4 = alloca i32, align 4
      %5 = alloca i8**, align 8
      store i32 0, i32* %3, align 4
      store i32 %0, i32* %4, align 4
      store i8** %1, i8*** %5, align 8
      %6 = call i32 (i8*, ...) @printf(i8* getelementptr inbounds ([14 x i8], [14 x i8]* @.str, i32 0, i32 0))
      ret i32 0
    }
    
    declare i32 @printf(i8*, ...) #1
    
    attributes #0 = { nounwind ssp uwtable "correctly-rounded-divide-sqrt-fp-math"="false" "disable-tail-calls"="false" "less-precise-fpmad"="false" "no-frame-pointer-elim"="true" "no-frame-pointer-elim-non-leaf" "no-infs-fp-math"="false" "no-jump-tables"="false" "no-nans-fp-math"="false" "no-signed-zeros-fp-math"="false" "stack-protector-buffer-size"="8" "target-cpu"="penryn" "target-features"="+cx16,+fxsr,+mmx,+sse,+sse2,+sse3,+sse4.1,+ssse3,+x87" "unsafe-fp-math"="false" "use-soft-float"="false" }
    attributes #1 = { "correctly-rounded-divide-sqrt-fp-math"="false" "disable-tail-calls"="false" "less-precise-fpmad"="false" "no-frame-pointer-elim"="true" "no-frame-pointer-elim-non-leaf" "no-infs-fp-math"="false" "no-nans-fp-math"="false" "no-signed-zeros-fp-math"="false" "stack-protector-buffer-size"="8" "target-cpu"="penryn" "target-features"="+cx16,+fxsr,+mmx,+sse,+sse2,+sse3,+sse4.1,+ssse3,+x87" "unsafe-fp-math"="false" "use-soft-float"="false" }
    
    !llvm.module.flags = !{!0}
    !llvm.ident = !{!1}
    
    !0 = !{i32 1, !"PIC Level", i32 2}
    !1 = !{!"Apple LLVM version 8.1.0 (clang-802.0.42)"}
    

    其实还是能实现我们功能的代码,在这一步,所有 LLVM 前端支持的语言都将会被转换成这样的代码,主要是为了后面的工作可以共用。下面就是 LVVM 中的优化器的工作。

    在这里简单介绍一些 LLVM IR 的指令:

    • %:局部变量
    • @:全局变量
    • alloca:分配内存堆栈
    • i32:32 位的整数
    • i32**:一个指向 32 位 int 值的指针的指针
    • align 4:向 4 个字节对齐,即便数据没有占用 4 个字节,也要为其分配四个字节
    • call:调用
    3.4 优化

    上面的代码是没有进行优化过的,在语言转换的过程中,有些代码是可以被优化以提升执行效率的。使用命令 clang -O3 -S -emit-llvm helloworld.c -o helloworld.ll,其实和上面的命令的区别只有 -O3 而已,注意,这里是大写字母 O 而不是数字 0。优化后的代码如下:

    ; ModuleID = 'helloworld.c'
    source_filename = "helloworld.c"
    target datalayout = "e-m:o-i64:64-f80:128-n8:16:32:64-S128"
    target triple = "x86_64-apple-macosx10.12.0"
    
    @str = private unnamed_addr constant [13 x i8] c"Hello World!\00"
    
    ; Function Attrs: nounwind ssp uwtable
    define i32 @main(i32, i8** nocapture readnone) local_unnamed_addr #0 {
      %3 = tail call i32 @puts(i8* getelementptr inbounds ([13 x i8], [13 x i8]* @str, i64 0, i64 0))
      ret i32 0
    }
    
    ; Function Attrs: nounwind
    declare i32 @puts(i8* nocapture readonly) #1
    
    attributes #0 = { nounwind ssp uwtable "correctly-rounded-divide-sqrt-fp-math"="false" "disable-tail-calls"="false" "less-precise-fpmad"="false" "no-frame-pointer-elim"="true" "no-frame-pointer-elim-non-leaf" "no-infs-fp-math"="false" "no-jump-tables"="false" "no-nans-fp-math"="false" "no-signed-zeros-fp-math"="false" "stack-protector-buffer-size"="8" "target-cpu"="penryn" "target-features"="+cx16,+fxsr,+mmx,+sse,+sse2,+sse3,+sse4.1,+ssse3,+x87" "unsafe-fp-math"="false" "use-soft-float"="false" }
    attributes #1 = { nounwind }
    
    !llvm.module.flags = !{!0}
    !llvm.ident = !{!1}
    
    !0 = !{i32 1, !"PIC Level", i32 2}
    !1 = !{!"Apple LLVM version 8.1.0 (clang-802.0.42)"}
    

    可以看到,即使是最简单的 helloworld 代码,也会被优化。这一步骤的优化是非常重要的,很多直接转换来的代码是不合适且消耗内存的,因为是直接转换,所以必然会有这样的问题,而优化放在这一步的好处在于前端不需要考虑任何优化过程,减少了前端的开发工作。

    如果想了解优化过程中到底进行了什么优化,可以查看这篇文章:编译器

    3.5 生成目标文件

    下面就是后端的工作了,将优化过的代码根据不同架构的 CPU 转化生成汇编代码,再生成对应的可执行文件,这样对应的 CPU 就可以执行了。

    使用命令 clang -S -o - helloworld.c | open -f 可以查看生成的汇编代码:

    “` 
    .section __TEXT,__text,regular,pure_instructions 
    .macosx_version_min 10, 12 
    .globl _main 
    .p2align 4, 0x90 
    _main: ## @main 
    .cfi_startproc
    
    BB#0:
    pushq   %rbp
    
    Ltmp0: 
    .cfi_def_cfa_offset 16 
    Ltmp1: 
    .cfi_offset %rbp, -16 
    movq %rsp, %rbp 
    Ltmp2: 
    .cfi_def_cfa_register %rbp 
    subq 32,leaqL.str(movl32,leaqL.str(movl0, -4(%rbp) 
    movl %edi, -8(%rbp) 
    movq %rsi, -16(%rbp) 
    movq %rax, %rdi 
    movb 0,callqprintfxorlmovlmovladdq0,callqprintfxorlmovlmovladdq32, %rsp 
    popq %rbp 
    retq 
    .cfi_endproc
    
    .section    __TEXT,__cstring,cstring_literals
    
    L_.str: ## @.str 
    .asciz “Hello World!\n”
    
    .subsections_via_symbols 
    “`
    
    

    注意代码中的 .section 指令,它指定了接下来会执行的代码段。在这篇文章中,详细解释了这些汇编指令或代码到底是如何工作的:Mach-O 可执行文件

    3.6 可执行文件

    在最后,LLVM 将会把这些汇编代码输出成二进制的可执行文件,使用命令 clang helloworld.c -o helloworld.out 即可查看,-o helloworld.out 如果不指定,将会被默认指定为 a.out。

    可执行文件会有多个部分,对应了汇编指令中的 .section,它的名字也叫做 section,每个 section 都会被转换进某个 segment 里。这种方式用来区分不同功能的代码。将相同属性的 section 集合在一起,就是一个 segment。

    使用 otool 工具可以查看生成的可执行文件的 section 和 segment:

    xcrun size -x -l -m helloworld.out

    Segment __PAGEZERO: 0x100000000 (vmaddr 0x0 fileoff 0)
    Segment __TEXT: 0x1000 (vmaddr 0x100000000 fileoff 0)
        Section __text: 0x34 (addr 0x100000f50 offset 3920)
        Section __stubs: 0x6 (addr 0x100000f84 offset 3972)
        Section __stub_helper: 0x1a (addr 0x100000f8c offset 3980)
        Section __cstring: 0xe (addr 0x100000fa6 offset 4006)
        Section __unwind_info: 0x48 (addr 0x100000fb4 offset 4020)
        total 0xaa
    Segment __DATA: 0x1000 (vmaddr 0x100001000 fileoff 4096)
        Section __nl_symbol_ptr: 0x10 (addr 0x100001000 offset 4096)
        Section __la_symbol_ptr: 0x8 (addr 0x100001010 offset 4112)
        total 0x18
    Segment __LINKEDIT: 0x1000 (vmaddr 0x100002000 fileoff 8192)
    total 0x100003000
    

    上面的代码中,每个 segment 的意义也不一样:

    • __PAGEZERO segment 它的大小为 4GB。这 4GB 并不是文件的真实大小,但是规定了进程地址空间的前 4GB 被映射为 不可执行、不可写和不可读。
    • __TEXT segment 包含了被执行的代码。它被以只读和可执行的方式映射。进程被允许执行这些代码,但是不能修改。
    • __DATA segment 以可读写和不可执行的方式映射。它包含了将会被更改的数据。
    • __LINKEDIT segment 指出了 link edit 表(包含符号和字符串的动态链接器表)的地址,里面包含了加载程序的元数据,例如函数的名称和地址。
      电脑如何读取代码
      深入剖析 iOS 编译 Clang LLVM

    本文转自 iOS App 的编译过程

    相关文章

      网友评论

        本文标题:xcode编译过程

        本文链接:https://www.haomeiwen.com/subject/bvmckqtx.html