.a ios 编译部分代码

2018-12-23 20:59:26 Hello_Hwc 阅读数 14450

前言

两年前曾经写过一篇关于编译的文章《iOS编译过程的原理和应用》,这篇文章介绍了iOS编译相关基础知识和简单应用,但也很有多问题都没有解释清楚:

  • Clang和LLVM究竟是什么
  • 源文件到机器码的细节
  • Linker做了哪些工作
  • 编译顺序如何确定
  • 头文件是什么?XCode是如何找到头文件的?
  • Clang Module
  • 签名是什么?为什么要签名

为了搞清楚这些问题,我们来挖掘下XCode编译iOS应用的细节。

编译器

把一种编程语言(原始语言)转换为另一种编程语言(目标语言)的程序叫做编译器

大多数编译器由两部分组成:前端和后端。

  • 前端负责词法分析,语法分析,生成中间代码;
  • 后端以中间代码作为输入,进行行架构无关的代码优化,接着针对不同架构生成不同的机器码。

前后端依赖统一格式的中间代码(IR),使得前后端可以独立的变化。新增一门语言只需要修改前端,而新增一个CPU架构只需要修改后端即可。

Objective C/C/C++使用的编译器前端是clang,swift是swift,后端都是LLVM

LLVM

LLVM(Low Level Virtual Machine)是一个强大的编译器开发工具套件,听起来像是虚拟机,但实际上LLVM和传统意义的虚拟机关系不大,只不过项目最初的名字是LLVM罢了。

LLVM的核心库提供了现代化的source-target-independent优化器和支持诸多流行CPU架构的代码生成器,这些核心代码是围绕着LLVM IR(中间代码)建立的。

基于LLVM,又衍生出了一些强大的子项目,其中iOS开发者耳熟能详的是:ClangLLDB

clang

clang是C语言家族的编译器前端,诞生之初是为了替代GCC,提供更快的编译速度。一张图了解clang编译的大致流程:

接下来,从代码层面看一下具体的转化过程,新建一个main.c:

#include <stdio.h>
// 一点注释
#define DEBUG 1
int main() {
#ifdef DEBUG
  printf("hello debug\n");
#else
  printf("hello world\n");
#endif
  return 0;
}

预处理(preprocessor)

预处理会替进行头文件引入,宏替换,注释处理,条件编译(#ifdef)等操作

#include "stdio.h"就是告诉预处理器将这一行替换成头文件stdio.h中的内容,这个过程是递归的:因为stdio.h也有可能包含其头文件。

用clang查看预处理的结果:

xcrun clang -E main.c

预处理后的文件有400多行,在文件的末尾,可以找到main函数

int main() {
  printf("hello debug\n");
  return 0;
}

可以看到,在预处理的时候,注释被删除,条件编译被处理。

词法分析(lexical anaysis)

词法分析器读入源文件的字符流,将他们组织称有意义的词素(lexeme)序列,对于每个词素,此法分析器产生词法单元(token)作为输出。

$ xcrun clang -fmodules -fsyntax-only -Xclang -dump-tokens main.c

输出:

annot_module_include '#include <s'		Loc=<main.c:1:1>
int 'int'	 [StartOfLine]	Loc=<main.c:4:1>
identifier 'main'	 [LeadingSpace]	Loc=<main.c:4:5>
....

Loc=<main.c:1:1>标示这个token位于源文件main.c的第1行,从第1个字符开始。保存token在源文件中的位置是方便后续clang分析的时候能够找到出错的原始位置。

语法分析(semantic analysis)

词法分析的Token流会被解析成一颗抽象语法树(abstract syntax tree - AST)。

$ xcrun clang -fsyntax-only -Xclang -ast-dump main.c | open -f

main函数AST的结构如下:

`-FunctionDecl 0x7fcc188dc700 <main.c:4:1, line:11:1> line:4:5 main 'int ()'
  `-CompoundStmt 0x7fcc188dc918 <col:12, line:11:1>
    |-CallExpr 0x7fcc188dc880 <line:6:3, col:25> 'int'
    | |-ImplicitCastExpr 0x7fcc188dc868 <col:3> 'int (*)(const char *, ...)' <FunctionToPointerDecay>
    | | `-DeclRefExpr 0x7fcc188dc7a0 <col:3> 'int (const char *, ...)' Function 0x7fcc188c5160 'printf' 'int (const char *, ...)'
    | `-ImplicitCastExpr 0x7fcc188dc8c8 <col:10> 'const char *' <BitCast>
    |   `-ImplicitCastExpr 0x7fcc188dc8b0 <col:10> 'char *' <ArrayToPointerDecay>
    |     `-StringLiteral 0x7fcc188dc808 <col:10> 'char [13]' lvalue "hello debug\n"
    `-ReturnStmt 0x7fcc188dc900 <line:10:3, col:10>
      `-IntegerLiteral 0x7fcc188dc8e0 <col:10> 'int' 0

有了抽象语法树,clang就可以对这个树进行分析,找出代码中的错误。比如类型不匹配,亦或Objective C中向target发送了一个未实现的消息。

AST是开发者编写clang插件主要交互的数据结构,clang也提供很多API去读取AST。更多细节:Introduction to the Clang AST

CodeGen

CodeGen遍历语法树,生成LLVM LR代码。LLVM IR是前端的输出,后端的输入。

xcrun clang -S -emit-llvm main.c -o main.ll

main.ll文件内容:

...
@.str = private unnamed_addr constant [13 x i8] c"hello debug\0A\00", align 1

; Function Attrs: noinline nounwind optnone ssp uwtable
define i32 @main() #0 {
  %1 = alloca i32, align 4
  store i32 0, i32* %1, align 4
  %2 = call i32 (i8*, ...) @printf(i8* getelementptr inbounds ([13 x i8], [13 x i8]* @.str, i32 0, i32 0))
  ret i32 0
}
...

Objective C代码在这一步会进行runtime的桥接:property合成,ARC处理等。

LLVM会对生成的IR进行优化,优化会调用相应的Pass进行处理。Pass由多个节点组成,都是Pass类的子类,每个节点负责做特定的优化,更多细节:Writing an LLVM Pass

生成汇编代码

LLVM对LR进行优化后,会针对不同架构生成不同的目标代码,最后以汇编代码的格式输出:

生成arm 64汇编:

$ xcrun clang -S main.c -o main.s

查看生成的main.s文件,篇幅有限,对汇编感兴趣的同学可以看看我的这篇文章:iOS汇编快速入门

_main:                                  ## @main
        .cfi_startproc
## %bb.0:
        pushq   %rbp
        .cfi_def_cfa_offset 16
        .cfi_offset %rbp, -16
        movq    %rsp, %rbp
...

汇编器

汇编器以汇编代码作为输入,将汇编代码转换为机器代码,最后输出目标文件(object file)。

$ xcrun clang -fmodules -c main.c -o main.o

还记得我们代码中调用了一个函数printf么?通过nm命令,查看下main.o中的符号

$ xcrun nm -nm main.o
                 (undefined) external _printf
0000000000000000 (__TEXT,__text) external _main

_printf是一个是undefined external的。undefined表示在当前文件暂时找不到符号_printf,而external表示这个符号是外部可以访问的,对应表示文件私有的符号是non-external

Tips:什么是符号(Symbols)? 符号就是指向一段代码或者数据的名称。还有一种叫做WeakSymols,也就是并不一定会存在的符号,需要在运行时决定。比如iOS 12特有的API,在iOS11上就没有。

链接

连接器把编译产生的.o文件和(dylib,a,tbd)文件,生成一个mach-o文件。

$ xcrun clang main.o -o main

我们就得到了一个mach o格式的可执行文件

$ file main
main: Mach-O 64-bit executable x86_64
$ ./main 
hello debug

在用nm命令,查看可执行文件的符号表:

$ nm -nm main
                 (undefined) external _printf (from libSystem)
                 (undefined) external dyld_stub_binder (from libSystem)
0000000100000000 (__TEXT,__text) [referenced dynamically] external __mh_execute_header
0000000100000f60 (__TEXT,__text) external _main

_printf仍然是undefined,但是后面多了一些信息:from libSystem,表示这个符号来自于libSystem,会在运行时动态绑定。

XCode编译

通过上文我们大概了解了Clang编译一个C语言文件的过程,但是XCode开发的项目不仅仅包含了代码文件,还包括了图片,plist等。XCode中编译一次都要经过哪些过程呢?

新建一个单页面的Demo工程:CocoaPods依赖AFNetworking和SDWebImage,同时依赖于一个内部Framework。按下Command+B,在XCode的Report Navigator模块中,可以找到编译的详细日志:

详细的步骤如下:

  • 创建Product.app的文件夹
  • 把Entitlements.plist写入到DerivedData里,处理打包的时候需要的信息(比如application-identifier)。
  • 创建一些辅助文件,比如各种.hmap,这是headermap文件,具体作用下文会讲解。
  • 执行CocoaPods的编译前脚本:检查Manifest.lock文件。
  • 编译.m文件,生成.o文件。
  • 链接动态库,o文件,生成一个mach o格式的可执行文件。
  • 编译assets,编译storyboard,链接storyboard
  • 拷贝动态库Logger.framework,并且对其签名
  • 执行CocoaPods编译后脚本:拷贝CocoaPods Target生成的Framework
  • 对Demo.App签名,并验证(validate)
  • 生成Product.app

Tips: Entitlements.plist保存了App需要使用的特殊权限,比如iCloud,远程通知,Siri等。

编译顺序

编译的时候有很多的Task(任务)要去执行,XCode如何决定Task的执行顺序呢?

答案是:依赖关系。

还是以刚刚的Demo项目为例,整个依赖关系如下:

可以从XCode的Report Navigator看到Target的编译顺序:

XCode编译的时候会尽可能的利用多核性能,多Target并发编译。

那么,XCode又从哪里得到了这些依赖关系呢?

  • Target Dependencies - 显式声明的依赖关系
  • Linked Frameworks and Libraries - 隐式声明的依赖关系
  • Build Phase - 定义了编译一个Target的每一步

增量编译

日常开发中,一次完整的编译可能要几分钟,甚至几十分钟,而增量编译只需要不到1分钟,为什么增量编译会这么快呢?

因为XCode会对每一个Task生成一个哈希值,只有哈希值改变的时候才会重新编译。

比如,修改了ViewControler.m,只有图中灰色的三个Task会重新执行(这里不考虑build phase脚本)。

头文件

C语言家族中,头文件(.h)文件用来引入函数/类/宏定义等声明,让开发者更灵活的组织代码,而不必把所有的代码写到一个文件里。

头文件对于编译器来说就是一个promise。头文件里的声明,编译会认为有对应实现,在链接的时候再解决具体实现的位置。

当只有声明,没有实现的时候,链接器就会报错。

Undefined symbols for architecture arm64:
“_umimplementMethod”, referenced from:
-[ClassA method] in ClassA.o
ld: symbol(s) not found for architecture arm64
clang: error: linker command failed with exit code 1 (use -v to see invocation)

Objective C的方法要到运行时才会报错,因为Objective C是一门动态语言,编译器无法确定对应的方法名(SEL)在运行时到底有没有实现(IMP)。

日常开发中,两种常见的头文件引入方式:

#include "CustomClass.h" //自定义
#include <Foundation/Foundation.h> //系统或者内部framework

引入的时候并没有指明文件的具体路径,编译器是如何找到这些头文件的呢?

回到XCode的Report Navigator,找到上一个编译记录,可以看到编译ViewController.m的具体日志:

把这个日志整体拷贝到命令行中,然后最后加上-v,表示我们希望得到更多的日志信息,执行这段代码,在日志最后可以看到clang是如何找到头文件的:

#include "..." search starts here:
 /Users/.../Build/Intermediates.noindex/Demo.build/Debug-iphoneos/Demo.build/Demo-generated-files.hmap (headermap)
 /Users/.../Build/Intermediates.noindex/Demo.build/Debug-iphoneos/Demo.build/Demo-project-headers.hmap (headermap)
 /Users/.../Build/Products/Debug-iphoneos/AFNetworking/AFNetworking.framework/Headers
 /Users/.../Build/Products/Debug-iphoneos/SDWebImage/SDWebImage.framework/Headers
 
#include <...> search starts here:
 /Users/.../Build/Intermediates.noindex/Demo.build/Debug-iphoneos/Demo.build/Demo-own-target-headers.hmap (headermap)
 /Users/.../Build/Intermediates.noindex/Demo.build/Debug-iphoneos/Demo.build/Demo-all-non-framework-target-headers.hmap (headermap)
 /Users/.../Build/Intermediates.noindex/Demo.build/Debug-iphoneos/Demo.build/DerivedSources
 /Users/.../Build/Products/Debug-iphoneos (framework directory)
 /Users/.../Build/Products/Debug-iphoneos/AFNetworking (framework directory)
 /Users/.../Build/Products/Debug-iphoneos/SDWebImage (framework directory)
 /Applications/Xcode.app/Contents/Developer/Toolchains/XcodeDefault.xctoolchain/usr/lib/clang/10.0.0/include
 /Applications/Xcode.app/Contents/Developer/Toolchains/XcodeDefault.xctoolchain/usr/include
 $SDKROOT/usr/include
 $SDKROOT/System/Library/Frameworks (framework directory)
 
End of search list.

这里有个文件类型叫做heademap,headermap是帮助编译器找到头文件的辅助文件:存储这头文件到其物理路径的映射关系。

可以通过一个辅助的小工具hmap查看hmap中的内容:

192:Desktop Leo$ ./hmap print Demo-project-headers.hmap 
AppDelegate.h -> /Users/huangwenchen/Desktop/Demo/Demo/AppDelegate.h
Demo-Bridging-Header.h -> /Users/huangwenchen/Desktop/Demo/Demo/Demo-Bridging-Header.h
Dummy.h -> /Users/huangwenchen/Desktop/Demo/Framework/Dummy.h
Framework.h -> Framework/Framework.h
TestView.h -> /Users/huangwenchen/Desktop/Demo/Demo/View/TestView.h
ViewController.h -> /Users/huangwenchen/Desktop/Demo/Demo/ViewController.h

Tips: 这就是为什么备份/恢复Mac后,需要clean build folder,因为两台mac对应文件的物理位置可能不一样。

clang发现#import "TestView.h"的时候,先在headermap(Demo-generated-files.hmap,Demo-project-headers.hmap)里查找,如果headermap文件找不到,接着在own target的framework里找:

/Users/.../Build/Products/Debug-iphoneos/AFNetworking/AFNetworking.framework/Headers/TestView.h
/Users/.../Build/Products/Debug-iphoneos/SDWebImage/SDWebImage.framework/Headers/TestView.h

系统的头文件查找的时候也是优先headermap,headermap查找不到会查找own target framework,最后查找SDK目录。

#import <Foundation/Foundation.h>为例,在SDK目录查找时:

首先查找framework是否存在

$SDKROOT/System/Library/Frameworks/Foundation.framework

如果framework存在,再在headers目录里查找头文件是否存在

$SDKROOT/System/Library/Frameworks/Foundation.framework/headers/Foundation.h

Clang Module

传统的#include/#import都是文本语义:预处理器在处理的时候会把这一行替换成对应头文件的文本,这种简单粗暴替换是有很多问题的:

  1. 大量的预处理消耗。假如有N个头文件,每个头文件又#include了M个头文件,那么整个预处理的消耗是N*M
  2. 文件导入后,宏定义容易出现问题。因为是文本导入,并且按照include依次替换,当一个头文件定义了#define std hello_world,而第另一个个头文件刚好又是C++标准库,那么include顺序不通,可能会导致所有的std都会被替换。
  3. 边界不明显。拿到一组.a和.h文件,很难确定.h是属于哪个.a的,需要以什么样的顺序导入才能正确编译。

clang module不再使用文本模型,而是采用更高效的语义模型。clang module提供了一种新的导入方式:@import,module会被作为一个独立的模块编译,并且产生独立的缓存,从而大幅度提高预处理效率,这样时间消耗从M*N变成了M+N

XCode创建的Target是Framework的时候,默认define module会设置为YES,从而支持module,当然像Foundation等系统的framwork同样支持module。

#import <Foundation/NSString.h>的时候,编译器会检查NSString.h是否在一个module里,如果是的话,这一行会被替换成@import Foundation

那么,如何定义一个module呢?答案是:modulemap文件,这个文件描述了一组头文件如何转换为一个module,举个例子:

framework module Foundation  [extern_c] [system] {
	umbrella header "Foundation.h" // 所有要暴露的头文件
 	export *
	module * {
 		export *
 	}
 	explicit module NSDebug { //submodule
 		header "NSDebug.h"
 		export *
 	}
 }

swift是可以直接import一个clang module的,比如你有一些C库,需要在Swift中使用,就可以用modulemap的方式。

Swift编译

现代化的语言几乎都抛弃了头文件,swift也不例外。问题来了,swift没有头文件又是怎么找到声明的呢?

编译器干了这些脏活累活。编译一个Swift头文件,需要解析module中所有的Swift文件,找到对应的声明

当开发中难免要有Objective C和Swfit相互调用的场景,两种语言在编译的时候查找符号的方式不同,如何一起工作的呢?

Swift引用Objective C

Swift的编译器内部使用了clang,所以swift可以直接使用clang module,从而支持直接import Objective C编写的framework。

swift编译器会从objective c头文件里查找符号,头文件的来源分为两大类:

  • Bridging-Header.h中暴露给swfit的头文件
  • framework中公开的头文件,根据编写的语言不通,可能从modulemap或者umbrella header查找

XCode提供了宏定义NS_SWIFT_NAME来让开发者定义Objective C => Swift的符号映射,可以通过Related Items -> Generate Interface来查看转换后的结果:

Objective引用swift

xcode会以module为单位,为swift自动生成头文件,供Objective C引用,通常这个文件命名为ProductName-Swift.h

swift提供了关键词@objc来把类型暴露给Objective C和Objective C Runtime。

@objc public class MyClass

深入理解Linker

链接器会把编译器编译生成的多个文件,链接成一个可执行文件。链接并不会产生新的代码,只是在现有代码的基础上做移动和补丁。

链接器的输入可能是以下几种文件:

  • object file(.o),单个源文件的编辑结果,包含了由符号表示的代码和数据。
  • 动态库(.dylib),mach o类型的可执行文件,链接的时候只会绑定符号,动态库会被拷贝到app里,运行时加载
  • 静态库(.a),由ar命令打包的一组.o文件,链接的时候会把具体的代码拷贝到最后的mach-o
  • tbd,只包含符号的库文件

这里我们提到了一个概念:符号(Symbols),那么符号是什么呢?

符号是一段代码或者数据的名称,一个符号内部也有可能引用另一个符号。

以一段代码为例,看看链接时究竟发生了什么?

源代码:

- (void)log{
	printf("hello world\n");
}

.o文件:

#代码
adrp    x0, l_.str@PAGE
add     x0, x0, l_.str@PAGEOFF
bl      _printf

#字符串符号
l_.str:                                 ; @.str
        .asciz  "hello world\n"

在.o文件中,字符串"hello world\n"作为一个符号(l_.str)被引用,汇编代码读取的时候按照l_.str所在的页加上偏移量的方式读取,然后调用printf符号。到这一步,CPU还不知道怎么执行,因为还有两个问题没解决:

  1. l_.str在可执行文件的哪个位置?
  2. printf函数来自哪里?

再来看看链接之后的mach o文件:

链接器如何解决这两个问题呢?

  1. 链接后,不再是以页+偏移量的方式读取字符串,而是直接读虚拟内存中的地址,解决了l_.str的位置问题。
  2. 链接后,不再是调用符号_printf,而是在DATA段上创建了一个函数指针_printf$ptr,初始值为0x0(null),代码直接调用这个函数指针。启动的时候,dyld会把DATA段上的指针进行动态绑定,绑定到具体虚拟内存中的_printf地址。更多细节,可以参考我之前的这篇文章:深入理解iOS App的启动过程

Tips: Mach-O有一个区域叫做LINKEDIT,这个区域用来存储启动的时dyld需要动态修复的一些数据:比如刚刚提到的printf在内存中的地址。

理解签名

基础回顾

非对称加密。在密码学中,非对称加密需要两个密钥:公钥和私钥。私钥加密的只能用公钥解密,公钥加密的只能用私钥解密。

数字签名。数字签名表示我对数据做了个标记,表示这是我的数据,没有经过篡改。

数据发送方Leo产生一对公私钥,私钥自己保存,公钥发给接收方Lina。Leo用摘要算法,对发送的数据生成一段摘要,摘要算法保证了只要数据修改,那么摘要一定改变。然后用私钥对这个摘要进行加密,和数据一起发送给Lina。

Lina收到数据后,用公钥解密签名,得到Leo发过来的摘要;然后自己按照同样的摘要算法计算摘要,如果计算的结果和Leo的一样,说明数据没有被篡改过。

但是,现在还有个问题:Lina有一个公钥,假如攻击者把Lina的公钥替换成自己的公钥,那么攻击者就可以伪装成Leo进行通信,所以Lina需要确保这个公钥来自于Leo,可以通过数字证书来解决这个问题。

数字证书由CA(Certificate Authority)颁发,以Leo的证书为例,里面包含了以下数据:签发者Leo的公钥Leo使用的Hash算法证书的数字签名;到期时间等。

有了数字证书后,Leo再发送数据的时候,把自己从CA申请的证书一起发送给Lina。Lina收到数据后,先用CA的公钥验证证书的数字签名是否正确,如果正确说明证书没有被篡改过,然后以信任链的方式判断是否信任这个证书,如果信任证书,取出证书中的数据,可以判断出证书是属于Leo的,最后从证书中取出公钥来做数据签名验证。

iOS App签名

为什么要对App进行签名呢?签名能够让iOS识别出是谁签名了App,并且签名后App没有被篡改过

除此之外,Apple要严格控制App的分发:

  1. App来自Apple信任的开发者
  2. 安装的设备是Apple允许的设备

证书

通过上文的讲解,我们知道数字证书里包含着申请证书设备的公钥,所以在Apple开发者后台创建证书的时候,需要上传CSR文件(Certificate Signing Request),用keychain生成这个文件的时候,就生成了一对公/私钥:公钥在CSR里,私钥在本地的Mac上。Apple本身也有一对公钥和私钥:私钥保存在Apple后台,公钥在每一台iOS设备上

Provisioning Profile

iOS App安装到设备的途径(非越狱)有以下几种:

  1. 开发包(插线,或者archive导出develop包)
  2. Ad Hoc
  3. App Store
  4. 企业证书

开发包和Ad Hoc都会严格限制安装设备,为了把设备uuid等信息一起打包进App,开发者需要配置Provisioning Profile。

可以通过以下命令来查看Provisioning Profile中的内容:

security cms -D -i embedded.mobileprovision > result.plist
open result.plist

本质上就是一个编码过后的plist

iOS签名

生成安装包的最后一步,XCode会调用codesign对Product.app进行签名。

创建一个额外的目录_CodeSignature以plist的方式存放安装包内每一个文件签名

<key>Base.lproj/LaunchScreen.storyboardc/01J-lp-oVM-view-Ze5-6b-2t3.nib</key>
<data>
T2g5jlq7EVFHNzL/ip3fSoXKoOI=
</data>
<key>Info.plist</key>
<data>
5aVg/3m4y30m+GSB8LkZNNU3mug=
</data>
<key>PkgInfo</key>
<data>
n57qDP4tZfLD1rCS43W0B4LQjzE=
</data>
<key>embedded.mobileprovision</key>
<data>
tm/I1g+0u2Cx9qrPJeC0zgyuVUE=
</data>
...

代码签名会直接写入到mach-o的可执行文件里,值得注意的是签名是以页(Page)为单位的,而不是整个文件签名:

验证

在安装App的时候,

  • 从embedded.mobileprovision取出证书,验证证书是否来自Apple信任的开发者
  • 证书验证通过后,从证书中取出Leo的公钥
  • 读取_CodeSignature中的签名结果,用Leo的公钥验证每个文件的签名是否正确
  • 文件embedded.mobileprovision验证通过后,读取里面的设备id列表,判断当前设备是否可安装(App Store和企业证书不做这步验证)
  • 验证通过后,安装App

启动App的时候:

  • 验证bundle id,entitlements和embedded.mobileprovision中的AppId,entitlements是否一致
  • 判断device id包含在embedded.mobileprovision里
    • App Store和企业证书不做验证
  • 如果是企业证书,验证用户是否信任企业证书
  • App启动后,当缺页中断(page fault)发生的时候,系统会把对应的mach-o页读入物理内存,然后验证这个page的签名是否正确。
  • 以上都验证通过,App才能正常启动

小结

如有内容错误,欢迎issue指正。

2016-12-10 11:58:12 Hello_Hwc 阅读数 29794

欢迎Follow我的Github,博客会同步在Github的Blog仓库更新。

前言

一般可以将编程语言分为两种,编译语言直译式语言

像C++,Objective C都是编译语言。编译语言在执行的时候,必须先通过编译器生成机器码,机器码可以直接在CPU上执行,所以执行效率较高。

像JavaScript,Python都是直译式语言。直译式语言不需要经过编译的过程,而是在执行的时候通过一个中间的解释器将代码解释为CPU可以执行的代码。所以,较编译语言来说,直译式语言效率低一些,但是编写的更灵活,也就是为啥JS大法好。

iOS开发目前的常用语言是:Objective和Swift。二者都是编译语言,换句话说都是需要编译才能执行的。二者的编译都是依赖于Clang(swift) + LLVM. 篇幅限制,本文只关注Objective C,因为原理上大同小异。

可能会有同学想问,我不懂编译的过程,写代码也没问题啊?这点我是不否定的。但是,充分理解了编译的过程,会对你的开发大有帮助。本文的最后,会以以下几个例子,来讲解如何合理利用XCode和编译

  • __attribute__
  • Clang警告处理
  • 预处理
  • 插入编译期脚本
  • 提高项目编译速度

对于不想看我啰里八嗦讲一大堆原理的同学,可以直接跳到本文的最后一个章节。


iOS编译

Objective C采用Clang作为前端,而Swift则采用swift()作为前端,二者LLVM(Low level vritual machine)作为编译器后端。所以简单的编译过程如图

其中,swift的编译命令可以在这里找到

 /Applications/Xcode.app/Contents/Developer/Toolchains/XcodeDefault.xctoolchain/usr/bin/swift

可以通过Clang,来查看一个文件的编译具体过程,新建Demo.m

#import <Foundation/Foundation.h>
  
int main(){
    @autoreleasepool {
        NSLog(@"%@",@"Hello Leo");
    }
    return 0;
}

然后终端输入:

clang -ccc-print-phases -framework Foundation Demo.m -o Demo 
0: input, "Foundation", object 
1: input, "Demo.m", objective-c
2: preprocessor, {1}, objective-c-cpp-output//预处理
3: compiler, {2}, ir //编译生成IR(中间代码)
4: backend, {3}, assembler//汇编器生成汇编代码
5: assembler, {4}, object//生成机器码
6: linker, {0, 5}, image//链接
7: bind-arch, "x86_64", {6}, image//生成Image,也就是最后的可执行文件

接着,就可以在终端直接运行这个程序了:

./Demo
Leo$ ./Demo 
Demo[923:24816] Hello Leo

编译器前端

编译器前端的任务是进行:语法分析,语义分析,生成中间代码(intermediate representation )。在这个过程中,会进行类型检查,如果发现错误或者警告会标注出来在哪一行。

编译器后端

编译器后端会进行机器无关的代码优化,生成机器语言,并且进行机器相关的代码优化。iOS的编译过程,后端的处理如下

  • LVVM优化器会进行BitCode的生成,链接期优化等等
  • LLVM机器码生成器会针对不同的架构,比如arm64等生成不同的机器码

执行一次XCode build的流程

当你在XCode中,选择build的时候(快捷键command+B),会执行如下过程

  • 编译信息写入辅助文件,创建编译后的文件架构(name.app)
  • 处理文件打包信息,例如在debug环境下
Entitlements:
{
    "application-identifier" = "app的bundleid";
    "aps-environment" = development;
}
  • 执行CocoaPod编译前脚本
    • 例如对于使用CocoaPod的工程会执行CheckPods Manifest.lock
  • 编译各个.m文件,使用CompileCclang命令。
CompileC ClassName.o ClassName.m normal x86_64 objective-c com.apple.compilers.llvm.clang.1_0.compiler
export LANG=en_US.US-ASCII
export PATH="..."
clang -x objective-c -arch x86_64 -fmessage-length=0 -fobjc-arc... -Wno-missing-field-initializers ... -DDEBUG=1 ... -isysroot iPhoneSimulator10.1.sdk -fasm-blocks ... -I 上文提到的文件 -F 所需要的Framework  -iquote 所需要的Framework  ... -c ClassName.c -o ClassName.o

通过这个编译的命令,我们可以看到

clang是实际的编译命令
-x 		objective-c 指定了编译的语言
-arch 	x86_64制定了编译的架构,类似还有arm7等
-fobjc-arc 一些列-f开头的,指定了采用arc等信息。这个也就是为什么你可以对单独的一个.m文件采用非ARC编程。
-Wno-missing-field-initializers 一系列以-W开头的,指的是编译的警告选项,通过这些你可以定制化编译选项
-DDEBUG=1 一些列-D开头的,指的是预编译宏,通过这些宏可以实现条件编译
-iPhoneSimulator10.1.sdk 制定了编译采用的iOS SDK版本
-I 把编译信息写入指定的辅助文件
-F 链接所需要的Framework
-c ClassName.c 编译文件
-o ClassName.o 编译产物
  • 链接需要的Framework,例如Foundation.framework,AFNetworking.framework,ALiPay.fframework
  • 编译xib文件
  • 拷贝xib,图片等资源文件到结果目录
  • 编译ImageAssets
  • 处理info.plist
  • 执行CocoaPod脚本
  • 拷贝Swift标准库
  • 创建.app文件和对其签名

IPA包的内容

例如,我们通过iTunes Store下载微信,然后获得ipa安装包,然后实际看看其安装包的内容。

  • 右键ipa,重命名为.zip
  • 双击zip文件,解压缩后会得到一个文件夹。所以,ipa包就是一个普通的压缩包。
- 右键图中的`WeChat`,选择显示包内容,然后就能够看到实际的ipa包内容了。

二进制文件的内容

通过XCode的Link Map File,我们可以窥探二进制文件中布局。
在XCode -> Build Settings -> 搜索map -> 开启Write Link Map File

开启后,在编译,我们可以在对应的Debug/Release目录下看到对应的link map的text文件。
默认的目录在

~/Library/Developer/Xcode/DerivedData/<TARGET-NAME>-对应ID/Build/Intermediates/<TARGET-NAME>.build/Debug-iphoneos/<TARGET-NAME>.build/

例如,我的TargetName是EPlusPan4Phone,目录如下

/Users/huangwenchen/Library/Developer/Xcode/DerivedData/EPlusPan4Phone-eznmxzawtlhpmadnbyhafnpqpizo/Build/Intermediates/EPlusPan4Phone.build/Debug-iphonesimulator/EPlusPan4Phone.build

这个映射文件的主要包含以下部分:

###Object files

这个部分包括的内容

  • .o 文文件,也就是上文提到的.m文件编译后的结果。
  • .a文件
  • 需要link的framework

#! Arch: x86_64
#Object files:
[0] linker synthesized
[1] /EPlusPan4Phone.build/EPlusPan4Phone.app.xcent
[2]/EPlusPan4Phone.build/Objects-normal/x86_64/ULWBigResponseButton.o

[1175]/UMSocial_Sdk_4.4/libUMSocial_Sdk_4.4.a(UMSocialJob.o)
[1188]/iPhoneSimulator10.1.sdk/System/Library/Frameworks//Foundation.framework/Foundation

这个区域的存储内容比较简单:前面是文件的编号,后面是文件的路径。文件的编号在后续会用到

##Sections

这个区域提供了各个段(Segment)和节(Section)在可执行文件中的位置和大小。这个区域完整的描述克可执行文件中的全部内容。

其中,段分为两种

  • __TEXT 代码段
  • __DATA 数据段

例如,之前写的一个App,Sections区域如下,可以看到,代码段的

__text节的地址是0x1000021B0,大小是0x0077EBC3,而二者相加的下一个位置正好是__stubs的位置0x100780D74。

# Sections:
# 位置       大小        段       节
# Address	Size    	Segment	Section
0x1000021B0	0x0077EBC3	__TEXT	__text //代码
0x100780D74	0x00000FD8	__TEXT	__stubs
0x100781D4C	0x00001A50	__TEXT	__stub_helper
0x1007837A0	0x0001AD78	__TEXT	__const //常量
0x10079E518	0x00041EF7	__TEXT	__objc_methname //OC 方法名
0x1007E040F	0x00006E34	__TEXT	__objc_classname //OC 类名
0x1007E7243	0x00010498	__TEXT	__objc_methtype  //OC 方法类型
0x1007F76DC	0x0000E760	__TEXT	__gcc_except_tab 
0x100805E40	0x00071693	__TEXT	__cstring  //字符串
0x1008774D4	0x00004A9A	__TEXT	__ustring  
0x10087BF6E	0x00000149	__TEXT	__entitlements 
0x10087C0B8	0x0000D56C	__TEXT	__unwind_info 
0x100889628	0x000129C0	__TEXT	__eh_frame
0x10089C000	0x00000010	__DATA	__nl_symbol_ptr
0x10089C010	0x000012C8	__DATA	__got
0x10089D2D8	0x00001520	__DATA	__la_symbol_ptr
0x10089E7F8	0x00000038	__DATA	__mod_init_func
0x10089E840	0x0003E140	__DATA	__const //常量
0x1008DC980	0x0002D840	__DATA	__cfstring
0x10090A1C0	0x000022D8	__DATA	__objc_classlist // OC 方法列表
0x10090C498	0x00000010	__DATA	__objc_nlclslist 
0x10090C4A8	0x00000218	__DATA	__objc_catlist
0x10090C6C0	0x00000008	__DATA	__objc_nlcatlist
0x10090C6C8	0x00000510	__DATA	__objc_protolist // OC协议列表
0x10090CBD8	0x00000008	__DATA	__objc_imageinfo
0x10090CBE0	0x00129280	__DATA	__objc_const // OC 常量
0x100A35E60	0x00010908	__DATA	__objc_selrefs
0x100A46768	0x00000038	__DATA	__objc_protorefs 
0x100A467A0	0x000020E8	__DATA	__objc_classrefs 
0x100A48888	0x000019C0	__DATA	__objc_superrefs // OC 父类引用
0x100A4A248	0x0000A500	__DATA	__objc_ivar // OC iar
0x100A54748	0x00015CC0	__DATA	__objc_data
0x100A6A420	0x00007A30	__DATA	__data
0x100A71E60	0x0005AF70	__DATA	__bss
0x100ACCDE0	0x00053A4C	__DATA	__common

Symbols

Section部分将二进制文件进行了一级划分。而,Symbols对Section中的各个段进行了二级划分,
例如,对于__TEXT __text,表示代码段中的代码内容。

0x1000021B0	0x0077EBC3	__TEXT	__text //代码

而对应的Symbols,起始地址也是0x1000021B0。其中,文件编号和上文的编号对应

[2]/EPlusPan4Phone.build/Objects-normal/x86_64/ULWBigResponseButton.o

具体内容如下

# Symbols:
  地址     大小          文件编号    方法名
# Address	Size    	File       Name
0x1000021B0	0x00000109	[  2]     -[ULWBigResponseButton pointInside:withEvent:]
0x1000022C0	0x00000080	[  3]     -[ULWCategoryController liveAPI]
0x100002340	0x00000080	[  3]     -[ULWCategoryController categories]
....

到这里,我们知道OC的方法是如何存储的,我们再来看看ivar是如何存储的。
首先找到数据栈中__DATA __objc_ivar

0x100A4A248	0x0000A500	__DATA	__objc_ivar

然后,搜索这个地址0x100A4A248,就能找到ivar的存储区域。

0x100A4A248	0x00000008	[  3] _OBJC_IVAR_$_ULWCategoryController._liveAPI

值得一提的是,对于String,会显式的存储到数据段中,例如,

0x1008065C2	0x00000029	[ 11] literal string: http://sns.whalecloud.com/sina2/callback

所以,若果你的加密Key以明文的形式写在文件里,是一件很危险的事情。


dSYM 文件

我们在每次编译过后,都会生成一个dsym文件。dsym文件中,存储了16进制的函数地址映射。

在App实际执行的二进制文件中,是通过地址来调用方法的。在App crash的时候,第三方工具(Fabric,友盟等)会帮我们抓到崩溃的调用栈,调用栈里会包含crash地址的调用信息。然后,通过dSYM文件,我们就可以由地址映射到具体的函数位置。

XCode中,选择Window -> Organizer可以看到我们生成的archier文件

然后,

  • 右键 -> 在finder中显示。
  • 右键 -> 查看包内容。

关于如何用dsym文件来分析崩溃位置,可以查看我之前的一篇博客。


那些你想到和想不到的应用场景

###__attribute__
或多或少,你都会在第三方库或者iOS的头文件中,见到过__attribute__。
比如

__attribute__ ((warn_unused_result)) //如果没有使用返回值,编译的时候给出警告

__attribtue__ 是一个高级的的编译器指令,它允许开发者指定更更多的编译检查和一些高级的编译期优化。

分为三种:

  • 函数属性 (Function Attribute)
  • 类型属性 (Variable Attribute )
  • 变量属性 (Type Attribute )

语法结构

__attribute__ 语法格式为:__attribute__ ((attribute-list))
放在声明分号“;”前面。

比如,在三方库中最常见的,声明一个属性或者方法在当前版本弃用了

@property (strong,nonatomic)CLASSNAME * property __deprecated;

这样的好处是:给开发者一个过渡的版本,让开发者知道这个属性被弃用了,应当使用最新的API,但是被__deprecated的属性仍然可以正常使用。如果直接弃用,会导致开发者在更新Pod的时候,代码无法运行了。

__attribtue__的使用场景很多,本文只列举iOS开发中常用的几个:

//弃用API,用作API更新
#define __deprecated	__attribute__((deprecated)) 

//带描述信息的弃用
#define __deprecated_msg(_msg) __attribute__((deprecated(_msg)))

//遇到__unavailable的变量/方法,编译器直接抛出Error
#define __unavailable	__attribute__((unavailable))

//告诉编译器,即使这个变量/方法 没被使用,也不要抛出警告
#define __unused	__attribute__((unused))

//和__unused相反
#define __used		__attribute__((used))

//如果不使用方法的返回值,进行警告
#define __result_use_check __attribute__((__warn_unused_result__))

//OC方法在Swift中不可用
#define __swift_unavailable(_msg)	__attribute__((__availability__(swift, unavailable, message=_msg)))

Clang警告处理

你一定还见过如下代码:

#pragma clang diagnostic push
#pragma clang diagnostic ignored "-Wundeclared-selector"
///代码
#pragma clang diagnostic pop

这段代码的作用是

  1. 对当前编译环境进行压栈
  2. 忽略-Wundeclared-selector(未声明的)Selector警告
  3. 编译代码
  4. 对编译环境进行出栈

通过clang diagnostic push/pop,你可以灵活的控制代码块的编译选项。

我在之前的一篇文章里,详细的介绍了XCode的警告相关内容。本文篇幅限制,就不详细讲解了。

预处理

所谓预处理,就是在编译之前的处理。预处理能够让你定义编译器变量,实现条件编译。
比如,这样的代码很常见

#ifdef DEBUG
//...
#else
//...
#endif

同样,我们同样也可以定义其他预处理变量,在XCode-选中Target-build settings中,搜索proprecess。然后点击图中蓝色的加号,可以分别为debug和release两种模式设置预处理宏。
比如我们加上:TestServer,表示在这个宏中的代码运行在测试服务器

然后,配合多个Target(右键Target,选择Duplicate),单独一个Target负责测试服务器。这样我们就不用每次切换测试服务器都要修改代码了。

#ifdef TESTMODE
//测试服务器相关的代码
#else
//生产服务器相关代码
#endif

插入脚本

通常,如果你使用CocoaPod来管理三方库,那么你的Build Phase是这样子的:

其中:[CP]开头的,就是CocoaPod插入的脚本。

  • Check Pods Manifest.lock,用来检查cocoapod管理的三方库是否需要更新
  • Embed Pods Framework,运行脚本来链接三方库的静态/动态库
  • Copy Pods Resources,运行脚本来拷贝三方库的资源文件

而这些配置信息都存储在这个文件(.xcodeprog)里

到这里,CocoaPod的原理也就大致搞清楚了,通过修改xcodeproject,然后配置编译期脚本,来保证三方库能够正确的编译连接。

同样,我们也可以插入自己的脚本,来做一些额外的事情。比如,每次进行archive的时候,我们都必须手动调整target的build版本,如果一不小心,就会忘记。这个过程,我们可以通过插入脚本自动化。

buildNumber=$(/usr/libexec/PlistBuddy -c "Print CFBundleVersion" "${PROJECT_DIR}/${INFOPLIST_FILE}")
buildNumber=$(($buildNumber + 1))
/usr/libexec/PlistBuddy -c "Set :CFBundleVersion $buildNumber" "${PROJECT_DIR}/${INFOPLIST_FILE}"

这段脚本其实很简单,读取当前pist的build版本号,然后对其加一,重新写入。

使用起来也很简单:

  • Xcode - 选中Target - 选中build phase
  • 选择添加Run Script Phase
  • 然后把这段脚本拷贝进去,并且勾选Run Script Only When installing,保证只有我们在安装到设备上的时候,才会执行这段脚本。重命名脚本的名字为Auto Increase build number
  • 然后,拖动这个脚本的到Link Binary With Libraries下面

脚本编译打包

脚本化编译打包对于CI(持续集成)来说,十分有用。iOS开发中,编译打包必备的两个命令是:

//编译成.app
xcodebuild  -workspace $projectName.xcworkspace -scheme $projectName  -configuration $buildConfig clean build SYMROOT=$buildAppToDir
//打包
xcrun -sdk iphoneos PackageApplication -v $appDir/$projectName.app -o $appDir/$ipaName.ipa

通过info命令,可以查看到详细的文档
info xcodebuild

在本文最后的附录中,提供了我之前使用的一个自动打包的脚本。

提高项目编译速度

通常,当项目很大,源代码和三方库引入很多的时候,我们会发现编译的速度很慢。在了解了XCode的编译过程后,我们可以从以下角度来优化编译速度:

查看编译时间

我们需要一个途径,能够看到编译的时间,这样才能有个对比,知道我们的优化究竟有没有效果。
对于XCode 8,关闭XCode,终端输入以下指令

$ defaults write com.apple.dt.Xcode ShowBuildOperationDuration YES

然后,重启XCode,然后编译,你会在这里看到编译时间。

代码层面的优化

forward declaration

所谓forward declaration,就是@class CLASSNAME,而不是#import CLASSNAME.h。这样,编译器能大大提高#import的替换速度。

对常用的工具类进行打包(Framework/.a)

打包成Framework或者静态库,这样编译的时候这部分代码就不需要重新编译了。

常用头文件放到预编译文件里

XCode的pch文件是预编译文件,这里的内容在执行XCode build之前就已经被预编译,并且引入到每一个.m文件里了。

编译器选项优化

Debug模式下,不生成dsym文件

上文提到了,dysm文件里存储了调试信息,在Debug模式下,我们可以借助XCode和LLDB进行调试。所以,不需要生成额外的dsym文件来降低编译速度。

Debug开启Build Active Architecture Only

在XCode -> Build Settings -> Build Active Architecture Only 改为YES。这样做,可以只编译当前的版本,比如arm7/arm64等等,记得只开启Debug模式。这个选项在高版本的XCode中自动开启了。

Debug模式下,关闭编译器优化

编译器优化


##后续

本来这篇文章还有很多内容想写,篇幅限制,就先这样吧。最近发生了很多不开心的事,这里提醒自己一句:吃一堑,长一智。


##附录
自动编译打包脚本

export LC_ALL=zh_CN.GB2312;
export LANG=zh_CN.GB2312
buildConfig="Release" //这里是build模式
projectName=`find . -name *.xcodeproj | awk -F "[/.]" '{print $(NF-1)}'`
projectDir=`pwd`
wwwIPADir=~/Desktop/$projectName-IPA
isWorkSpace=true
echo "~~~~~~~~~~~~~~~~~~~开始编译~~~~~~~~~~~~~~~~~~~"
if [ -d "$wwwIPADir" ]; then
echo $wwwIPADir
echo "文件目录存在"
else
echo "文件目录不存在"
mkdir -pv $wwwIPADir
echo "创建${wwwIPADir}目录成功"
fi
cd $projectDir
rm -rf ./build
buildAppToDir=$projectDir/build
infoPlist="$projectName/Info.plist"
bundleVersion=`/usr/libexec/PlistBuddy -c "Print CFBundleShortVersionString" $infoPlist`
bundleIdentifier=`/usr/libexec/PlistBuddy -c "Print CFBundleIdentifier" $infoPlist`
bundleBuildVersion=`/usr/libexec/PlistBuddy -c "Print CFBundleVersion" $infoPlist`

if $isWorkSpace ; then  #是否用CocoaPod
echo  "开始编译workspace...."
xcodebuild  -workspace $projectName.xcworkspace -scheme $projectName  -configuration $buildConfig clean build SYMROOT=$buildAppToDir
else
echo  "开始编译target...."
xcodebuild  -target  $projectName  -configuration $buildConfig clean build SYMROOT=$buildAppToDir
fi

if test $? -eq 0
then
echo "~~~~~~~~~~~~~~~~~~~编译成功~~~~~~~~~~~~~~~~~~~"
else
echo "~~~~~~~~~~~~~~~~~~~编译失败~~~~~~~~~~~~~~~~~~~"
exit 1
fi

ipaName=`echo $projectName | tr "[:upper:]" "[:lower:]"` #将项目名转小写
findFolderName=`find . -name "$buildConfig-*" -type d |xargs basename` #查找目录
appDir=$buildAppToDir/$findFolderName/  #app所在路径
echo "开始打包$projectName.app成$projectName.ipa....."
xcrun -sdk iphoneos PackageApplication -v $appDir/$projectName.app -o $appDir/$ipaName.ipa

if [ -f "$appDir/$ipaName.ipa" ]
then
echo "打包$ipaName.ipa成功."
else
echo "打包$ipaName.ipa失败."
exit 1
fi

path=$wwwIPADir/$projectName$(date +%Y%m%d%H%M%S).ipa
cp -f -p $appDir/$ipaName.ipa $path   #拷贝ipa文件
echo "复制$ipaName.ipa到${wwwIPADir}成功"
echo "~~~~~~~~~~~~~~~~~~~结束编译,处理成功~~~~~~~~~~~~~~~~~~~"

2017-04-14 09:10:20 u012460084 阅读数 5300

来源:黄文臣 

blog.csdn.net/hello_hwc/article/details/53557308

前言

一般可以将编程语言分为两种,编译语言直译式语言

像C++,Objective C都是编译语言。编译语言在执行的时候,必须先通过编译器生成机器码,机器码可以直接在CPU上执行,所以执行效率较高。

JavaScript,Python都是直译式语言。直译式语言不需要经过编译的过程,而是在执行的时候通过一个中间的解释器将代码解释为CPU可以执行的代码。所以,较编译语言来说,直译式语言效率低一些,但是编写的更灵活,也就是为啥JS大法好。

iOS开发目前的常用语言是:Objective和Swift。二者都是编译语言,换句话说都是需要编译才能执行的。二者的编译都是依赖于Clang + LLVM. 篇幅限制,本文只关注Objective C,因为原理上大同小异。

可能会有同学想问,我不懂编译的过程,写代码也没问题啊?这点我是不否定的。但是,充分理解了编译的过程,会对你的开发大有帮助。本文的最后,会以以下几个例子,来讲解如何合理利用XCode和编译

  • __attribute__
  • Clang警告处理
  • 预处理
  • 插入编译期脚本
  • 提高项目编译速度

对于不想看我啰里八嗦讲一大堆原理的同学,可以直接跳到本文的最后一个章节。


iOS编译

不管是OC还是Swift,都是采用Clang作为编译器前端,LLVM(Low level vritual machine)作为编译器后端。所以简单的编译过程如图

编译器前端

编译器前端的任务是进行:语法分析,语义分析,生成中间代码(intermediate representation )。在这个过程中,会进行类型检查,如果发现错误或者警告会标注出来在哪一行。

编译器后端

编译器后端会进行机器无关的代码优化,生成机器语言,并且进行机器相关的代码优化。iOS的编译过程,后端的处理如下

  • LVVM优化器会进行BitCode的生成,链接期优化等等

  • LLVM机器码生成器会针对不同的架构,比如arm64等生成不同的机器码


执行一次XCode build的流程

当你在XCode中,选择build的时候(快捷键command+B),会执行如下过程

  • 编译信息写入辅助文件,创建编译后的文件架构(name.app)
  • 处理文件打包信息,例如在debug环境下
Entitlements:
{
    "application-identifier" = "app的bundleid";
    "aps-environment" = development;
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 1
  • 2
  • 3
  • 4
  • 5
  • 执行CocoaPod编译前脚本 
    • 例如对于使用CocoaPod的工程会执行CheckPods Manifest.lock
  • 编译各个.m文件,使用CompileCclang命令。
CompileC ClassName.o ClassName.m normal x86_64 objective-c com.apple.compilers.llvm.clang.1_0.compiler
export LANG=en_US.US-ASCII
export PATH="..."
clang -x objective-c -arch x86_64 -fmessage-length=0 -fobjc-arc... -Wno-missing-field-initializers ... -DDEBUG=1 ... -isysroot iPhoneSimulator10.1.sdk -fasm-blocks ... -I 上文提到的文件 -F 所需要的Framework  -iquote 所需要的Framework  ... -c ClassName.c -o ClassName.o
  • 1
  • 2
  • 3
  • 4
  • 1
  • 2
  • 3
  • 4

通过这个编译的命令,我们可以看到

clang是实际的编译命令
-x      objective-c 指定了编译的语言
-arch   x86_64制定了编译的架构,类似还有arm7等
-fobjc-arc 一些列-f开头的,指定了采用arc等信息。这个也就是为什么你可以对单独的一个.m文件采用非ARC编程。
-Wno-missing-field-initializers 一系列以-W开头的,指的是编译的警告选项,通过这些你可以定制化编译选项
-DDEBUG=1 一些列-D开头的,指的是预编译宏,通过这些宏可以实现条件编译
-iPhoneSimulator10.1.sdk 制定了编译采用的iOS SDK版本
-I 把编译信息写入指定的辅助文件
-F 链接所需要的Framework
-c ClassName.c 编译文件
-o ClassName.o 编译产物
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 链接需要的Framework,例如Foundation.framework,AFNetworking.framework,ALiPay.fframework
  • 编译xib文件
  • 拷贝xib,图片等资源文件到结果目录
  • 编译ImageAssets
  • 处理info.plist
  • 执行CocoaPod脚本
  • 拷贝Swift标准库
  • 创建.app文件和对其签名

IPA包的内容

例如,我们通过iTunes Store下载微信,然后获得ipa安装包,然后实际看看其安装包的内容。 

  • 右键ipa,重命名为.zip
  • 双击zip文件,解压缩后会得到一个文件夹。所以,ipa包就是一个普通的压缩包。

  • 右键图中的WeChat,选择显示包内容,然后就能够看到实际的ipa包内容了。

二进制文件的内容

通过XCode的Link Map File,我们可以窥探二进制文件中布局。 
在XCode -> Build Settings -> 搜索map -> 开启Write Link Map File

开启后,在编译,我们可以在对应的Debug/Release目录下看到对应的link map的text文件。 
默认的目录在

~/Library/Developer/Xcode/DerivedData/<TARGET-NAME>-对应ID/Build/Intermediates/<TARGET-NAME>.build/Debug-iphoneos/<TARGET-NAME>.build/
  • 1
  • 1

例如,我的TargetName是EPlusPan4Phone,目录如下

/Users/huangwenchen/Library/Developer/Xcode/DerivedData/EPlusPan4Phone-eznmxzawtlhpmadnbyhafnpqpizo/Build/Intermediates/EPlusPan4Phone.build/Debug-iphonesimulator/EPlusPan4Phone.build
  • 1
  • 1

这个映射文件的主要包含以下部分:

Object files

这个部分包括的内容 
- .o 文文件,也就是上文提到的.m文件编译后的结果。 
- .a文件  
- 需要link的framework

#! Arch: x86_64 
#Object files: 
[0] linker synthesized 
[1] /EPlusPan4Phone.build/EPlusPan4Phone.app.xcent 
[2]/EPlusPan4Phone.build/Objects-normal/x86_64/ULWBigResponseButton.o 
… 
[1175]/UMSocial_Sdk_4.4/libUMSocial_Sdk_4.4.a(UMSocialJob.o) 
[1188]/iPhoneSimulator10.1.sdk/System/Library/Frameworks//Foundation.framework/Foundation

这个区域的存储内容比较简单:前面是文件的编号,后面是文件的路径。文件的编号在后续会用到

Sections

这个区域提供了各个段(Segment)和节(Section)在可执行文件中的位置和大小。这个区域完整的描述克可执行文件中的全部内容。

其中,段分为两种

  • __TEXT 代码段
  • __DATA 数据段 

例如,之前写的一个App,Sections区域如下,可以看到,代码段的

__text节的地址是0x1000021B0,大小是0x0077EBC3,而二者相加的下一个位置正好是__stubs的位置0x100780D74。

# Sections:
# 位置       大小        段       节
# Address   Size        Segment Section
0x1000021B0 0x0077EBC3  __TEXT  __text //代码
0x100780D74 0x00000FD8  __TEXT  __stubs
0x100781D4C 0x00001A50  __TEXT  __stub_helper
0x1007837A0 0x0001AD78  __TEXT  __const //常量
0x10079E518 0x00041EF7  __TEXT  __objc_methname //OC 方法名
0x1007E040F 0x00006E34  __TEXT  __objc_classname //OC 类名
0x1007E7243 0x00010498  __TEXT  __objc_methtype  //OC 方法类型
0x1007F76DC 0x0000E760  __TEXT  __gcc_except_tab 
0x100805E40 0x00071693  __TEXT  __cstring  //字符串
0x1008774D4 0x00004A9A  __TEXT  __ustring  
0x10087BF6E 0x00000149  __TEXT  __entitlements 
0x10087C0B8 0x0000D56C  __TEXT  __unwind_info 
0x100889628 0x000129C0  __TEXT  __eh_frame
0x10089C000 0x00000010  __DATA  __nl_symbol_ptr
0x10089C010 0x000012C8  __DATA  __got
0x10089D2D8 0x00001520  __DATA  __la_symbol_ptr
0x10089E7F8 0x00000038  __DATA  __mod_init_func
0x10089E840 0x0003E140  __DATA  __const //常量
0x1008DC980 0x0002D840  __DATA  __cfstring
0x10090A1C0 0x000022D8  __DATA  __objc_classlist // OC 方法列表
0x10090C498 0x00000010  __DATA  __objc_nlclslist 
0x10090C4A8 0x00000218  __DATA  __objc_catlist
0x10090C6C0 0x00000008  __DATA  __objc_nlcatlist
0x10090C6C8 0x00000510  __DATA  __objc_protolist // OC协议列表
0x10090CBD8 0x00000008  __DATA  __objc_imageinfo
0x10090CBE0 0x00129280  __DATA  __objc_const // OC 常量
0x100A35E60 0x00010908  __DATA  __objc_selrefs
0x100A46768 0x00000038  __DATA  __objc_protorefs 
0x100A467A0 0x000020E8  __DATA  __objc_classrefs 
0x100A48888 0x000019C0  __DATA  __objc_superrefs // OC 父类引用
0x100A4A248 0x0000A500  __DATA  __objc_ivar // OC iar
0x100A54748 0x00015CC0  __DATA  __objc_data
0x100A6A420 0x00007A30  __DATA  __data
0x100A71E60 0x0005AF70  __DATA  __bss
0x100ACCDE0 0x00053A4C  __DATA  __common
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38

Symbols

Section部分将二进制文件进行了一级划分。而,Symbols对Section中的各个段进行了二级划分, 
例如,对于__TEXT __text,表示代码段中的代码内容。

0x1000021B0 0x0077EBC3  __TEXT  __text //代码
  • 1
  • 1

而对应的Symbols,起始地址也是0x1000021B0。其中,文件编号和上文的编号对应

[2]/EPlusPan4Phone.build/Objects-normal/x86_64/ULWBigResponseButton.o
  • 1
  • 1

具体内容如下

# Symbols:
  地址     大小          文件编号    方法名
# Address   Size        File       Name
0x1000021B0 0x00000109  [  2]     -[ULWBigResponseButton pointInside:withEvent:]
0x1000022C0 0x00000080  [  3]     -[ULWCategoryController liveAPI]
0x100002340 0x00000080  [  3]     -[ULWCategoryController categories]
....
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8

到这里,我们知道OC的方法是如何存储的,我们再来看看ivar是如何存储的。 
首先找到数据栈中__DATA __objc_ivar

0x100A4A248 0x0000A500  __DATA  __objc_ivar
  • 1
  • 1

然后,搜索这个地址0x100A4A248,就能找到ivar的存储区域。

0x100A4A248 0x00000008  [  3] _OBJC_IVAR_$_ULWCategoryController._liveAPI
  • 1
  • 1

值得一提的是,对于String,会显式的存储到数据段中,例如,

0x1008065C2 0x00000029  [ 11] literal string: http://sns.whalecloud.com/sina2/callback
  • 1
  • 2
  • 1
  • 2

所以,若果你的加密Key以明文的形式写在文件里,是一件很危险的事情。


dSYM 文件

我们在每次编译过后,都会生成一个dsym文件。dsym文件中,存储了16进制的函数地址映射。

在App实际执行的二进制文件中,是通过地址来调用方法的。在App crash的时候,第三方工具(Fabric,友盟等)会帮我们抓到崩溃的调用栈,调用栈里会包含crash地址的调用信息。然后,通过dSYM文件,我们就可以由地址映射到具体的函数位置。

XCode中,选择Window -> Organizer可以看到我们生成的archier文件

然后,

  • 右键 -> 在finder中显示。
  • 右键 -> 查看包内容。

关于如何用dsym文件来分析崩溃位置,可以查看我之前的一篇博客。


那些你想到和想不到的应用场景

__attribute__

或多或少,你都会在第三方库或者iOS的头文件中,见到过attribute。 
比如

__attribute__ ((warn_unused_result)) //如果没有使用返回值,编译的时候给出警告
  • 1
  • 1

__attribtue__ 是一个高级的的编译器指令,它允许开发者指定更更多的编译检查和一些高级的编译期优化。

分为三种:

  • 函数属性 (Function Attribute)
  • 类型属性 (Variable Attribute )
  • 变量属性 (Type Attribute )

语法结构

__attribute__ 语法格式为:__attribute__ ((attribute-list)) 
放在声明分号“;”前面。

比如,在三方库中最常见的,声明一个属性或者方法在当前版本弃用了

@property (strong,nonatomic)CLASSNAME * property __deprecated;
  • 1
  • 1

这样的好处是:给开发者一个过渡的版本,让开发者知道这个属性被弃用了,应当使用最新的API,但是被__deprecated的属性仍然可以正常使用。如果直接弃用,会导致开发者在更新Pod的时候,代码无法运行了。

__attribtue__的使用场景很多,本文只列举iOS开发中常用的几个:

//弃用API,用作API更新
#define __deprecated    __attribute__((deprecated)) 

//带描述信息的弃用
#define __deprecated_msg(_msg) __attribute__((deprecated(_msg)))

//遇到__unavailable的变量/方法,编译器直接抛出Error
#define __unavailable   __attribute__((unavailable))

//告诉编译器,即使这个变量/方法 没被使用,也不要抛出警告
#define __unused    __attribute__((unused))

//和__unused相反
#define __used      __attribute__((used))

//如果不使用方法的返回值,进行警告
#define __result_use_check __attribute__((__warn_unused_result__))

//OC方法在Swift中不可用
#define __swift_unavailable(_msg)   __attribute__((__availability__(swift, unavailable, message=_msg)))
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21

Clang警告处理

你一定还见过如下代码:

#pragma clang diagnostic push
#pragma clang diagnostic ignored "-Wundeclared-selector"
///代码
#pragma clang diagnostic pop
  • 1
  • 2
  • 3
  • 4
  • 1
  • 2
  • 3
  • 4

这段代码的作用是

  1. 对当前编译环境进行压栈
  2. 忽略-Wundeclared-selector(未声明的)Selector警告
  3. 编译代码
  4. 对编译环境进行出栈

通过clang diagnostic push/pop,你可以灵活的控制代码块的编译选项。

我在之前的一篇文章里,详细的介绍了XCode的警告相关内容。本文篇幅限制,就不详细讲解了。

预处理

所谓预处理,就是在编译之前的处理。预处理能够让你定义编译器变量,实现条件编译。 
比如,这样的代码很常见

#ifdef DEBUG
//...
#else
//...
#endif
  • 1
  • 2
  • 3
  • 4
  • 5
  • 1
  • 2
  • 3
  • 4
  • 5

同样,我们同样也可以定义其他预处理变量,在XCode-选中Target-build settings中,搜索proprecess。然后点击图中蓝色的加号,可以分别为debug和release两种模式设置预处理宏。 
比如我们加上:TestServer,表示在这个宏中的代码运行在测试服务器

然后,配合多个Target(右键Target,选择Duplicate),单独一个Target负责测试服务器。这样我们就不用每次切换测试服务器都要修改代码了。

#ifdef TESTMODE
//测试服务器相关的代码
#else
//生产服务器相关代码
#endif
  • 1
  • 2
  • 3
  • 4
  • 5
  • 1
  • 2
  • 3
  • 4
  • 5

插入脚本

通常,如果你使用CocoaPod来管理三方库,那么你的Build Phase是这样子的:

其中:[CP]开头的,就是CocoaPod插入的脚本。

  • Check Pods Manifest.lock,用来检查cocoapod管理的三方库是否需要更新
  • Embed Pods Framework,运行脚本来链接三方库的静态/动态库
  • Copy Pods Resources,运行脚本来拷贝三方库的资源文件 

而这些配置信息都存储在这个文件(.xcodeprog)里

到这里,CocoaPod的原理也就大致搞清楚了,通过修改xcodeproject,然后配置编译期脚本,来保证三方库能够正确的编译连接。

同样,我们也可以插入自己的脚本,来做一些额外的事情。比如,每次进行archive的时候,我们都必须手动调整target的build版本,如果一不小心,就会忘记。这个过程,我们可以通过插入脚本自动化。

buildNumber=$(/usr/libexec/PlistBuddy -c "Print CFBundleVersion" "${PROJECT_DIR}/${INFOPLIST_FILE}")
buildNumber=$(($buildNumber + 1))
/usr/libexec/PlistBuddy -c "Set :CFBundleVersion $buildNumber" "${PROJECT_DIR}/${INFOPLIST_FILE}"
  • 1
  • 2
  • 3
  • 1
  • 2
  • 3

这段脚本其实很简单,读取当前pist的build版本号,然后对其加一,重新写入。

使用起来也很简单:

  • Xcode - 选中Target - 选中build phase 
  • 选择添加Run Script Phase

  • 然后把这段脚本拷贝进去,并且勾选Run Script Only When installing,保证只有我们在安装到设备上的时候,才会执行这段脚本。重命名脚本的名字为Auto Increase build number

  • 然后,拖动这个脚本的到Link Binary With Libraries下面

脚本编译打包

脚本化编译打包对于CI(持续集成)来说,十分有用。iOS开发中,编译打包必备的两个命令是:

//编译成.app
xcodebuild  -workspace $projectName.xcworkspace -scheme $projectName  -configuration $buildConfig clean build SYMROOT=$buildAppToDir
//打包
xcrun -sdk iphoneos PackageApplication -v $appDir/$projectName.app -o $appDir/$ipaName.ipa

通过info命令,可以查看到详细的文档
info xcodebuild
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7

在本文最后的附录中,提供了我之前使用的一个自动打包的脚本。

提高项目编译速度

通常,当项目很大,源代码和三方库引入很多的时候,我们会发现编译的速度很慢。在了解了XCode的编译过程后,我们可以从以下角度来优化编译速度:

查看编译时间

我们需要一个途径,能够看到编译的时间,这样才能有个对比,知道我们的优化究竟有没有效果。 
对于XCode 8,关闭XCode,终端输入以下指令

$ defaults write com.apple.dt.Xcode ShowBuildOperationDuration YES
  • 1
  • 1

然后,重启XCode,然后编译,你会在这里看到编译时间。

代码层面的优化

forward declaration

所谓forward declaration,就是@class CLASSNAME,而不是#import CLASSNAME.h。这样,编译器能大大提高#import的替换速度。

对常用的工具类进行打包(Framework/.a)

打包成Framework或者静态库,这样编译的时候这部分代码就不需要重新编译了。

常用头文件放到预编译文件里

XCode的pch文件是预编译文件,这里的内容在执行XCode build之前就已经被预编译,并且引入到每一个.m文件里了。

编译器选项优化

Debug模式下,不生成dsym文件

上文提到了,dysm文件里存储了调试信息,在Debug模式下,我们可以借助XCode和LLDB进行调试。所以,不需要生成额外的dsym文件来降低编译速度。

Debug开启Build Active Architecture Only

在XCode -> Build Settings -> Build Active Architecture Only 改为YES。这样做,可以只编译当前的版本,比如arm7/arm64等等,记得只开启Debug模式。这个选项在高版本的XCode中自动开启了。

Debug模式下,关闭编译器优化

编译器优化 


后续

本来这篇文章还有很多内容想写,篇幅限制,就先这样吧。最近发生了很多不开心的事,这里提醒自己一句:吃一堑,长一智。

后面有时间了,会介绍一些编译期黑科技:

  • 写入额外的编译信息
  • 函数的调用过程和运行时找到函数在二进制文件中的的地址
  • ……

附录

自动编译打包脚本

export LC_ALL=zh_CN.GB2312;
export LANG=zh_CN.GB2312
buildConfig="Release" //这里是build模式
projectName=`find . -name *.xcodeproj | awk -F "[/.]" '{print $(NF-1)}'`
projectDir=`pwd`
wwwIPADir=~/Desktop/$projectName-IPA
isWorkSpace=true
echo "~~~~~~~~~~~~~~~~~~~开始编译~~~~~~~~~~~~~~~~~~~"
if [ -d "$wwwIPADir" ]; then
echo $wwwIPADir
echo "文件目录存在"
else
echo "文件目录不存在"
mkdir -pv $wwwIPADir
echo "创建${wwwIPADir}目录成功"
fi
cd $projectDir
rm -rf ./build
buildAppToDir=$projectDir/build
infoPlist="$projectName/Info.plist"
bundleVersion=`/usr/libexec/PlistBuddy -c "Print CFBundleShortVersionString" $infoPlist`
bundleIdentifier=`/usr/libexec/PlistBuddy -c "Print CFBundleIdentifier" $infoPlist`
bundleBuildVersion=`/usr/libexec/PlistBuddy -c "Print CFBundleVersion" $infoPlist`

if $isWorkSpace ; then  #是否用CocoaPod
echo  "开始编译workspace...."
xcodebuild  -workspace $projectName.xcworkspace -scheme $projectName  -configuration $buildConfig clean build SYMROOT=$buildAppToDir
else
echo  "开始编译target...."
xcodebuild  -target  $projectName  -configuration $buildConfig clean build SYMROOT=$buildAppToDir
fi

if test $? -eq 0
then
echo "~~~~~~~~~~~~~~~~~~~编译成功~~~~~~~~~~~~~~~~~~~"
else
echo "~~~~~~~~~~~~~~~~~~~编译失败~~~~~~~~~~~~~~~~~~~"
exit 1
fi

ipaName=`echo $projectName | tr "[:upper:]" "[:lower:]"` #将项目名转小写
findFolderName=`find . -name "$buildConfig-*" -type d |xargs basename` #查找目录
appDir=$buildAppToDir/$findFolderName/  #app所在路径
echo "开始打包$projectName.app成$projectName.ipa....."
xcrun -sdk iphoneos PackageApplication -v $appDir/$projectName.app -o $appDir/$ipaName.ipa

if [ -f "$appDir/$ipaName.ipa" ]
then
echo "打包$ipaName.ipa成功."
else
echo "打包$ipaName.ipa失败."
exit 1
fi

path=$wwwIPADir/$projectName$(date +%Y%m%d%H%M%S).ipa
cp -f -p $appDir/$ipaName.ipa $path   #拷贝ipa文件
echo "复制$ipaName.ipa到${wwwIPADir}成功"
echo "~~~~~~~~~~~~~~~~~~~结束编译,处理成功~~~~~~~~~~~~~~~~~~~"
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46
  • 47
  • 48
  • 49
  • 50
  • 51
  • 52
  • 53
  • 54
  • 55
  • 56
  • 57
  • 58
  • 59
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46
  • 47
  • 48
  • 49
  • 50
  • 51
  • 52
  • 53
  • 54
  • 55
  • 56
  • 57
  • 58
  • 59

2017-11-22 17:45:56 aas319 阅读数 6185

在 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. 对包进行签名
  13. 完成打包

在上述流程中: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(movl0, -4(%rbp)
movl %edi, -8(%rbp)
movq %rsi, -16(%rbp)
movq %rax, %rdi
movb 0,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 表(包含符号和字符串的动态链接器表)的地址,里面包含了加载程序的元数据,例如函数的名称和地址。

关于 section 中的内容的研究可以查看:

Mach-O 可执行文件

PARSING MACH-O FILES

关于更详细的编译过程可以查看:

深入剖析 iOS 编译 Clang LLVM

这里还有一个讲解编译过程的视频,有兴趣的可以看看:

电脑如何读取代码?

3.7 Swift 文件 的编译过程

Swift 编译器结构 的官方文档中描述了 Swift 编译器是如何工作的,分为如下步骤:

  • 解析:解析器是一个简单的递归下降解析器(在 lib / Parse 中实现),带有集成的手动编码词法分析器。解析器负责生成没有任何语义或类型信息的抽象语法树(AST),并针对输入源的语法问题发出警告或错误。
  • 语意分析:语义分析(在 lib / Sema 中实现)负责解析 AST 并将其转换为格式良好的完全检查形式的 AST,并在源代码中发出语义问题的警告或错误。语义分析包括类型推断,如果成功,则所得到的代码是类型检查安全的 AST 。
  • **Clang导入器:**Clang导入器(在 lib / ClangImporter 中实现)导入Clang模块,并将它们导出的 C 或 Objective-C API 映射到相应的 Swift API中。结果导入的 AST 可以通过语义分析来引用。
  • **SIL生成:**Swift中间语言(Swift Intermediate Language,简称SIL)是一种高级的,Swift特有的中间语言,适用于 Swift 代码的进一步分析和优化。SIL 生成阶段(在 lib / SILGen 中实现)将类型检查的 AST 降低到所谓的 “原始” SIL。SIL的设计描述在 docs/ SIL.rst 中可以看到。
  • SIL优化:在SIL优化(在 lib/Analysislib/ ARClib/LoopTransforms,和 lib/Transforms 中实现)执行额外的高级别,Swift 特有的优化的程序,包括(例如)自动引用计数优化,虚拟化和通用专业化。
  • **LLVM IR生成:**IR生成(在 lib/IRGen 中实现)将 SIL 降到 LLVM IR,此时LLVM可以继续对其进行优化并生成机器码。

在 LLVM IR 生成后,就会和 3.3 的结果一致了,后面的事情就是优化器和 LLVM 后端进行的,所有 LLVM 支持的语言都是一样的。

下面将详细解释以下每一步的操作,首先创建一个 swift 文件 swiftFile.swift

func double(number: Int) {
    print(number*2)
}

在终端里,我们使用 swiftc 来调用 swift 的编译器,命令 swiftc -help 可以看到其支持的命令。

3.7.1 解析

使用命令 swiftc -dump-parse swiftFile.swift 可以查看初步解析后的 AST:

(source_file
  (func_decl "double(number:)"
    (parameter_list
      (parameter "number" apiName=number))
    (brace_stmt
      (call_expr type='<null>' arg_labels=_:
        (unresolved_decl_ref_expr type='<null>' name=print specialized=no function_ref=unapplied)
        (paren_expr type='<null>'
          (sequence_expr type='<null>'
            (declref_expr type='<null>' decl=swiftFile.(file).func decl.number@swiftFile.swift:1:13 function_ref=unapplied specialized=yes)
            (unresolved_decl_ref_expr type='<null>' name=* specialized=no function_ref=unapplied)
            (integer_literal_expr type='<null>' value=2)))))))

这是 Swift 编辑器自己使用的 AST,相对于 clang 的 AST 只是符号不一致。它也有 func_decl 相较于 clang 的 FunctionDecl。这里的参数表现方式不一样,clang 的参数是在外层的,这里使用了 parameter_list 来做了一个分叉,包含了 parameter。不过再改变,其设计原则也不会变,都是为了更加方便的使用一颗代码树来分析代码。这里所有的 type 都是 <null>,而且中间的很多操作都是未定义的,比如说 *,说明还没有做过类型检查,这需要下在一步中操作。

3.7.2 语意分析

使用命令 swiftc -dump-ast swiftFile.swift 可以查看进行过类型检查后的 AST:

(source_file
  (func_decl "double(number:)" interface type='(Int) -> ()' access=internal
    (parameter_list
      (parameter "number" apiName=number type='Int' interface type='Int'))
    (brace_stmt
      (call_expr type='()' location=swiftFile.swift:2:5 range=[swiftFile.swift:2:5 - line:2:19] nothrow arg_labels=_:
        (declref_expr type='(Any..., String, String) -> ()' location=swiftFile.swift:2:5 range=[swiftFile.swift:2:5 - line:2:5] decl=Swift.(file).print(_:separator:terminator:) function_ref=single specialized=no)
        (tuple_shuffle_expr implicit type='(Any..., separator: String, terminator: String)' location=swiftFile.swift:2:17 range=[swiftFile.swift:2:10 - line:2:19] source_is_scalar elements=[-2, -1, -1] variadic_sources=[0] default_args_owner=Swift.(file).print(_:separator:terminator:)
          (paren_expr type='Any' location=swiftFile.swift:2:17 range=[swiftFile.swift:2:10 - line:2:19]
            (erasure_expr implicit type='Any' location=swiftFile.swift:2:17 range=[swiftFile.swift:2:11 - line:2:18]
              (binary_expr type='Int' location=swiftFile.swift:2:17 range=[swiftFile.swift:2:11 - line:2:18] nothrow
                (declref_expr type='(Int, Int) -> Int' location=swiftFile.swift:2:17 range=[swiftFile.swift:2:17 - line:2:17] decl=Swift.(file).* function_ref=unapplied specialized=no)
                (tuple_expr implicit type='(Int, Int)' location=swiftFile.swift:2:11 range=[swiftFile.swift:2:11 - line:2:18]
                  (declref_expr type='Int' location=swiftFile.swift:2:11 range=[swiftFile.swift:2:11 - line:2:11] decl=swiftFile.(file).func decl.number@swiftFile.swift:1:13 function_ref=unapplied specialized=no)
                  (call_expr implicit type='Int' location=swiftFile.swift:2:18 range=[swiftFile.swift:2:18 - line:2:18] nothrow arg_labels=_builtinIntegerLiteral:
                    (constructor_ref_call_expr implicit type='(Int2048) -> Int' location=swiftFile.swift:2:18 range=[swiftFile.swift:2:18 - line:2:18] nothrow
                      (declref_expr implicit type='(Int.Type) -> (Int2048) -> Int' location=swiftFile.swift:2:18 range=[swiftFile.swift:2:18 - line:2:18] decl=Swift.(file).Int.init(_builtinIntegerLiteral:) function_ref=single specialized=no)
                      (type_expr implicit type='Int.Type' location=swiftFile.swift:2:18 range=[swiftFile.swift:2:18 - line:2:18] typerepr='Int'))
                    (tuple_expr implicit type='(_builtinIntegerLiteral: Int2048)' location=swiftFile.swift:2:18 range=[swiftFile.swift:2:18 - line:2:18] names=_builtinIntegerLiteral
                      (integer_literal_expr type='Int2048' location=swiftFile.swift:2:18 range=[swiftFile.swift:2:18 - line:2:18] value=2))))))))))))

仔细观察这一步和上一步的区别,我们所有的 type 都被填充了,这就是进行过类型检查后的 AST,这个时候编译器就可以对源代码中的语意问题进行检查并对错误或者警告进行提示。

检查后的 AST 可以完整表述我们的代码,有了完整的类型和操作。例如这里的 declref_exprtuple_shuffle_expr 指明了我们的打印方法:

public func print(_ items: Any..., separator: String = default, terminator: String = default)

paren_exprerasure_exprbinary_expr 指定了我们传入 print() 方法的值类型。

declref_expr type='(Int, Int) -> Int' 指明了我们的 * 方法。

tuple_expr implicit type='(Int, Int)' 指明了 * 方法的参数元组。

还有后面的标识符说明了我们的各个参数、使用方法、返回值以及各种值的类型。

3.7.3 clang 导入器

这一步骤是进行 c 和 Objective-C 的 API 映射。

3.7.4 SIL(Swift中间语言 Swift Intermediate Language,简称SIL)生成

使用命令 swiftc -emit-sil swiftFile.swift | open -f 可以查看生成的 SIL 代码,输出比较长,贴出部分来查看:

sil_scope 1 {  parent @main : $@convention(c) (Int32, UnsafeMutablePointer<Optional<UnsafeMutablePointer<Int8>>>) -> Int32 }

// main
sil @main : $@convention(c) (Int32, UnsafeMutablePointer<Optional<UnsafeMutablePointer<Int8>>>) -> Int32 {
bb0(%0 : $Int32, %1 : $UnsafeMutablePointer<Optional<UnsafeMutablePointer<Int8>>>):
  %2 = integer_literal $Builtin.Int32, 0, scope 1 // user: %3
  %3 = struct $Int32 (%2 : $Builtin.Int32), scope 1 // user: %4
  return %3 : $Int32, scope 1                     // id: %4
} // end sil function 'main'

sil_scope 2 { loc "swiftFile.swift":1:6 parent @_TF9swiftFile6doubleFT6numberSi_T_ : $@convention(thin) (Int) -> () }
sil_scope 3 { loc "swiftFile.swift":3:1 parent 2 }

// double(number : Int) -> ()
sil hidden @_TF9swiftFile6doubleFT6numberSi_T_ : $@convention(thin) (Int) -> () {
// %0                                             // users: %11, %1
bb0(%0 : $Int):
  // 定义了方法,以及参数 $0 就是参数 Int
  debug_value %0 : $Int, let, name "number", argno 1, loc "swiftFile.swift":1:13, scope 2 // id: %1
  // function_ref print([Any], separator : String, terminator : String) -> ()
  // Print 方法
  %2 = function_ref @_TFs5printFTGSaP__9separatorSS10terminatorSS_T_ : $@convention(thin) (@owned Array<Any>, @owned String, @owned String) -> (), loc "swiftFile.swift":2:5, scope 3 // user: %23
  // 字符 1
  %3 = integer_literal $Builtin.Word, 1, loc "swiftFile.swift":2:17, scope 3 // user: %5
  // function_ref specialized _allocateUninitializedArray<A> (Builtin.Word) -> ([A], Builtin.RawPointer)
  // 一个数组的初始化方法,里面包含其值和值对应的指针
  %4 = function_ref @_TTSgq5P____TFs27_allocateUninitializedArrayurFBwTGSax_Bp_ : $@convention(thin) (Builtin.Word) -> (@owned Array<Any>, Builtin.RawPointer), loc "swiftFile.swift":2:17, scope 3 // user: %5
  // 使用一个默认字符 1 初始化这个值
  %5 = apply %4(%3) : $@convention(thin) (Builtin.Word) -> (@owned Array<Any>, Builtin.RawPointer), loc "swiftFile.swift":2:17, scope 3 // users: %6, %7
  // 拿到了数组
  %6 = tuple_extract %5 : $(Array<Any>, Builtin.RawPointer), 0, loc "swiftFile.swift":2:17, scope 3 // user: %23
  // 拿到了数组的指针
  %7 = tuple_extract %5 : $(Array<Any>, Builtin.RawPointer), 1, loc "swiftFile.swift":2:17, scope 3 // user: %8
  // 拿到指针的地址
  %8 = pointer_to_address %7 : $Builtin.RawPointer to [strict] $*Any, loc "swiftFile.swift":2:17, scope 3 // user: %9
  // 使用指针的指向地址初始化一个存在的地址
  %9 = init_existential_addr %8 : $*Any, $Int, loc "swiftFile.swift":2:17, scope 3 // user: %18
  // 数字 2
  %10 = integer_literal $Builtin.Int64, 2, loc "swiftFile.swift":2:18, scope 3 // user: %13
  // number
  %11 = struct_extract %0 : $Int, #Int._value, loc "swiftFile.swift":2:17, scope 3 // user: %13
  // 数字 -1,在 smul_with_overflow_Int64 方法中传入,后修改,用来做操作成功失败的标识
  %12 = integer_literal $Builtin.Int1, -1, loc "swiftFile.swift":2:17, scope 3 // user: %13
  // 做乘法
  %13 = builtin "smul_with_overflow_Int64"(%11 : $Builtin.Int64, %10 : $Builtin.Int64, %12 : $Builtin.Int1) : $(Builtin.Int64, Builtin.Int1), loc "swiftFile.swift":2:17, scope 3 // users: %15, %14
  // 获取结果
  %14 = tuple_extract %13 : $(Builtin.Int64, Builtin.Int1), 0, loc "swiftFile.swift":2:17, scope 3 // user: %17
  // 获取操作结果:成功或者失败
  %15 = tuple_extract %13 : $(Builtin.Int64, Builtin.Int1), 1, loc "swiftFile.swift":2:17, scope 3 // user: %16
  // 判定是成功还是失败
  cond_fail %15 : $Builtin.Int1, loc "swiftFile.swift":2:17, scope 3 // id: %16
  // 将获取来的结果转为一个 struct
  %17 = struct $Int (%14 : $Builtin.Int64), loc "swiftFile.swift":2:17, scope 3 // user: %18
  // 将结果保存到 %9 的地址,这样 %6 的值就将是这个值
  store %17 to %9 : $*Int, loc "swiftFile.swift":2:17, scope 3 // id: %18
  // function_ref (print([Any], separator : String, terminator : String) -> ()).(default argument 1)
  // 获取第二个参数 `separator` 的默认值方法
  %19 = function_ref @_TIFs5printFTGSaP__9separatorSS10terminatorSS_T_A0_ : $@convention(thin) () -> @owned String, loc "swiftFile.swift":2:17, scope 3 // user: %20
  // 调用获取第二个参数 `separator` 的默认值方法
  %20 = apply %19() : $@convention(thin) () -> @owned String, loc "swiftFile.swift":2:17, scope 3 // user: %23
  // function_ref (print([Any], separator : String, terminator : String) -> ()).(default argument 2)
  // 获取第三个参数 `terminator` 的默认值方法
  %21 = function_ref @_TIFs5printFTGSaP__9separatorSS10terminatorSS_T_A1_ : $@convention(thin) () -> @owned String, loc "swiftFile.swift":2:17, scope 3 // user: %22
  // 调用获取第三个参数 `terminator` 的默认值方法
  %22 = apply %21() : $@convention(thin) () -> @owned String, loc "swiftFile.swift":2:17, scope 3 // user: %23
  // 调用 print 方法 %6 就是运算后的结果
  %23 = apply %2(%6, %20, %22) : $@convention(thin) (@owned Array<Any>, @owned String, @owned String) -> (), loc "swiftFile.swift":2:19, scope 3
  // 创建一个空元组
  %24 = tuple (), loc "swiftFile.swift":3:1, scope 3 // user: %25
  // 返回这个元组
  return %24 : $(), loc "swiftFile.swift":3:1, scope 3 // id: %25
} // end sil function '_TF9swiftFile6doubleFT6numberSi_T_'

SIL 中间语言看起来就顺眼多了,居然还有注释(英文注释为原注释,中文注释是我后来加的)。在原文件中是没有 main 的,这里可以看到,给代码中添加了 main 函数,运行入口依然是 main 函数。这里可以解释 Xcode Playground 的实现原理,可以不用考虑 main 函数,它在编译期会被添加。

和 LLVM IR 一样 % 是局部变量,@ 是全局变量。代码中有一些是 SIL 的基础语法,还有一些是自己定义的方法,可以直接在文件中搜索得到。不难发现,这些代码非常多,有一些冗余代码,在下一步将会对这些代码进行优化。

3.7.4 SIL 优化

使用命令 swiftc -emit-silgen swiftFile.swift | open -f 可以看到优化后的代码:

sil_stage raw

import Builtin
import Swift
import SwiftShims

sil_scope 1 {  parent @main : $@convention(c) (Int32, UnsafeMutablePointer<Optional<UnsafeMutablePointer<Int8>>>) -> Int32 }

// main
sil @main : $@convention(c) (Int32, UnsafeMutablePointer<Optional<UnsafeMutablePointer<Int8>>>) -> Int32 {
bb0(%0 : $Int32, %1 : $UnsafeMutablePointer<Optional<UnsafeMutablePointer<Int8>>>):
  %2 = integer_literal $Builtin.Int32, 0, scope 1 // user: %3
  %3 = struct $Int32 (%2 : $Builtin.Int32), scope 1 // user: %4
  return %3 : $Int32, scope 1                     // id: %4
} // end sil function 'main'

sil_scope 2 { loc "swiftFile.swift":1:6 parent @_TF9swiftFile6doubleFT6numberSi_T_ : $@convention(thin) (Int) -> () }
sil_scope 3 { loc "swiftFile.swift":3:1 parent 2 }

// double(number : Int) -> ()
sil hidden @_TF9swiftFile6doubleFT6numberSi_T_ : $@convention(thin) (Int) -> () {
// %0                                             // users: %15, %1
bb0(%0 : $Int):
  // 定义了方法
  debug_value %0 : $Int, let, name "number", argno 1, loc "swiftFile.swift":1:13, scope 2 // id: %1
  // function_ref print([Any], separator : String, terminator : String) -> ()
  // print 方法
  %2 = function_ref @_TFs5printFTGSaP__9separatorSS10terminatorSS_T_ : $@convention(thin) (@owned Array<Any>, @owned String, @owned String) -> (), loc "swiftFile.swift":2:5, scope 3 // user: %21
  // 初始化一个默认值
  %3 = integer_literal $Builtin.Word, 1, loc "swiftFile.swift":2:17, scope 3 // user: %5
  // function_ref _allocateUninitializedArray<A> (Builtin.Word) -> ([A], Builtin.RawPointer)
  // 存储数据方法
  %4 = function_ref @_TFs27_allocateUninitializedArrayurFBwTGSax_Bp_ : $@convention(thin)_0_0> (Builtin.Word) -> (@owned Array<τ_0_0>, Builtin.RawPointer), loc "swiftFile.swift":2:17, scope 3 // user: %5
  // 使用默认值初始化值
  %5 = apply %4<Any>(%3) : $@convention(thin)_0_0> (Builtin.Word) -> (@owned Array<τ_0_0>, Builtin.RawPointer), loc "swiftFile.swift":2:17, scope 3 // users: %7, %6
  // 获取值
  %6 = tuple_extract %5 : $(Array<Any>, Builtin.RawPointer), 0, loc "swiftFile.swift":2:17, scope 3 // user: %21
  // 获取值对应的指针
  %7 = tuple_extract %5 : $(Array<Any>, Builtin.RawPointer), 1, loc "swiftFile.swift":2:17, scope 3 // user: %8
  // 获取指针指向的地址
  %8 = pointer_to_address %7 : $Builtin.RawPointer to [strict] $*Any, loc "swiftFile.swift":2:17, scope 3 // user: %9
  // 根据指针指向的地址初始化一个地址
  %9 = init_existential_addr %8 : $*Any, $Int, loc "swiftFile.swift":2:17, scope 3 // user: %16
  // function_ref * infix(Int, Int) -> Int
  // 乘法方法
  %10 = function_ref @_TFsoi1mFTSiSi_Si : $@convention(thin) (Int, Int) -> Int, loc "swiftFile.swift":2:17, scope 3 // user: %15
  // function_ref Int.init(_builtinIntegerLiteral : Builtin.Int2048) -> Int
  // 一个 Int2048 到 Int 的转换类型
  %11 = function_ref @_TFSiCfT22_builtinIntegerLiteralBi2048__Si : $@convention(method) (Builtin.Int2048, @thin Int.Type) -> Int, loc "swiftFile.swift":2:18, scope 3 // user: %14
  // 设定目标类型
  %12 = metatype $@thin Int.Type, loc "swiftFile.swift":2:18, scope 3 // user: %14
  // Int2048 类型的数字 2
  %13 = integer_literal $Builtin.Int2048, 2, loc "swiftFile.swift":2:18, scope 3 // user: %14
  // 转换数字 2 为 Int 类型
  %14 = apply %11(%13, %12) : $@convention(method) (Builtin.Int2048, @thin Int.Type) -> Int, loc "swiftFile.swift":2:18, scope 3 // user: %15
  // 调用乘法
  %15 = apply %10(%0, %14) : $@convention(thin) (Int, Int) -> Int, loc "swiftFile.swift":2:17, scope 3 // user: %16
  // 存入结果数据
  store %15 to [trivial] %9 : $*Int, loc "swiftFile.swift":2:17, scope 3 // id: %16
  // function_ref (print([Any], separator : String, terminator : String) -> ()).(default argument 1)
  // 获取第二个参数 `separator` 的默认值方法
  %17 = function_ref @_TIFs5printFTGSaP__9separatorSS10terminatorSS_T_A0_ : $@convention(thin) () -> @owned String, loc "swiftFile.swift":2:17, scope 3 // user: %18
  // 调用获取第二个参数 `separator` 的默认值方法
  %18 = apply %17() : $@convention(thin) () -> @owned String, loc "swiftFile.swift":2:17, scope 3 // user: %21
  // function_ref (print([Any], separator : String, terminator : String) -> ()).(default argument 2)
  // 获取第三个参数 `terminator` 的默认值方法
  %19 = function_ref @_TIFs5printFTGSaP__9separatorSS10terminatorSS_T_A1_ : $@convention(thin) () -> @owned String, loc "swiftFile.swift":2:17, scope 3 // user: %20
  // 调用获取第三个参数 `terminator` 的默认值方法
  %20 = apply %19() : $@convention(thin) () -> @owned String, loc "swiftFile.swift":2:17, scope 3 // user: %21
  // 调用 print 方法
  %21 = apply %2(%6, %18, %20) : $@convention(thin) (@owned Array<Any>, @owned String, @owned String) -> (), loc "swiftFile.swift":2:19, scope 3
  // 创建一个空元组
  %22 = tuple (), loc "swiftFile.swift":3:1, scope 3 // user: %23
  // 返回元组
  return %22 : $(), loc "swiftFile.swift":3:1, scope 3 // id: %23
} // end sil function '_TF9swiftFile6doubleFT6numberSi_T_'


// print([Any], separator : String, terminator : String) -> ()
sil [noinline] [_semantics "stdlib_binary_only"] @_TFs5printFTGSaP__9separatorSS10terminatorSS_T_ : $@convention(thin) (@owned Array<Any>, @owned String, @owned String) -> ()


// _allocateUninitializedArray<A> (Builtin.Word) -> ([A], Builtin.RawPointer)
sil [fragile] [always_inline] @_TFs27_allocateUninitializedArrayurFBwTGSax_Bp_ : $@convention(thin)_0_0> (Builtin.Word) -> (@owned Array<τ_0_0>, Builtin.RawPointer)


// * infix(Int, Int) -> Int
sil [transparent] [fragile] @_TFsoi1mFTSiSi_Si : $@convention(thin) (Int, Int) -> Int


// Int.init(_builtinIntegerLiteral : Builtin.Int2048) -> Int
sil [transparent] [fragile] @_TFSiCfT22_builtinIntegerLiteralBi2048__Si : $@convention(method) (Builtin.Int2048, @thin Int.Type) -> Int


// (print([Any], separator : String, terminator : String) -> ()).(default argument 1)
sil [noinline] [_semantics "stdlib_binary_only"] @_TIFs5printFTGSaP__9separatorSS10terminatorSS_T_A0_ : $@convention(thin) () -> @owned String


// (print([Any], separator : String, terminator : String) -> ()).(default argument 2)
sil [noinline] [_semantics "stdlib_binary_only"] @_TIFs5printFTGSaP__9separatorSS10terminatorSS_T_A1_ : $@convention(thin) () -> @owned String

只剩下了这么一点,而且这些方法都是会被调用的,将优化前的 builtin "smul_with_overflow_Int64" 乘法方法修改成了 sil [transparent] [fragile] @_TFsoi1mFTSiSi_Si : $@convention(thin) (Int, Int) -> Int 方法,猜测也是因为优化前的方法有失败的风险,使用了一个类型转化代码和稳定的乘法方法也提高了代码的稳定性。

3.7.5 LLVM IR生成

使用命令 swiftc -emit-ir swiftFile.swift | open -f 可以查看生成的 LLVM IR 中间码,这里看到了一些熟悉的身影:

; ModuleID = '-'
source_filename = "-"
target datalayout = "e-m:o-i64:64-f80:128-n8:16:32:64-S128"
target triple = "x86_64-apple-macosx10.9"

这些代码就是之前 clang 编译的 LLVM IR 代码。这些代码将被发送到 LLVM 的优化器,被优化后再由 LLVM 后端生成汇编代码,然后再转成可执行文件就可以在 CPU 上运行了。

3.8 关于混编

知道了 Swift 和 Objective-C 的文件编译流程,对于两个混编的问题就变成了如何将两种代码链接起来的问题,在 Swift 有 clang 导入器,将 Objective-C 的 API 导入,而对于 Objective-C 为主要语言的项目中,Swift 会被 Xcode 编译为 .o.h 文件,头文件供 Objective-C 文件使用。下面这几篇文章详细讲述了这个过程包括最后的代码链接是如何进行的:

4. 静态库和动态库

说起来编译,就不得不说起动态库和静态库。这两个东西可是和编译过程息息相关的,这里有几篇文章的比较透彻,可以查看,想要了解整个编译过程,库是逃不开的:

本来想展开讲一下这两个东西,但是上面这两篇文章讲的太好了,直接贴出来更好点。

5. Xcode 编译设置

了解了这么多编译原理,除了写一个自动化编译脚本以外,还可以看懂很多之前完全看不明白的编译错误。在 Xcode 中,也可以对编译过程进行完整的设置,很多时候编译错误的解决就是在这里进行的。

5.1 Build Settings

这里是编译设置,针对编译流程中的各个过程进行参数和工具的配置:

  • Architectures:编译目标 CPU 架构,这里比较常见的是 Build Active Architectures Only(只编译为当前架构,是指你在 scheme 中选定的设备的 CPU 架构),debug 设置为 YESRelease 设置为 NO
  • Assets:Assets.xcassets 资源组的配置。
  • Build Locations:查看 Build 日志可以看到在编译过程中的目标文件夹。
  • Build Options:这里是一些编译的选项设定,包含:

    • 是否总是嵌入 Swift 标准库,这个在静态库和动态库的第一篇文章中有讲,iOS 系统目前是不包含 Swift 标准库的,都是被打包在项目中。
    • c/c++/objective-c 编译器:Apple LLVM 9.0
    • 是否打开 Bitcode
  • Deployment:iOS 部署设置。说白了就是安装到手机的设置。

  • Headers:头文件?具体作用不详,知道的可以说一下。
  • Kernel Module:内核模块,作用不详。
  • Linking:链接设置,链接路径、链接标记、Mach-O 文件类型。
  • Packaging:打包设置,info.plist 的路径设置、Bundle ID 、App 显示名称的设置。
  • Search Paths:库的搜索路径、头文件的搜索路径。
  • Signing:签名设置,开发、生产的签名设置,这些都和你在开发者网站配置的证书相关。
  • Testing:测试设置,作用不详。
  • Text-Based API:基于文本的 API,字面翻译,作用不详。
  • Versioning:版本管理。
  • Apple LLVM 9.0 系列:LLVM 的配置,包含路径、编译器每一步的设置、语言设置。在这里 Apple LLVM 9.0 - Warnings 可以选择在编译的时候将哪些情况认定为错误(Error)和警告(Warning),可以开启困难模式,任何一个小的警告都会被认定为错误。
  • Asset Catalog Compiler - Options:Asset 文件的编译设置。
  • Interface Builder Storyboard Compiler - Options:Storyboard 的编译设置。
  • 以及一些静态分析和 Swift 编译器的设定。

5.2 Build Phases

编译阶段,编译的时候将根据顺序来进行编译。这里固定的有:

  • Compile Sources:编译源文件。
  • Link Binary With Libraries:相关的链接库。
  • Copy Bundle Resources:要拷贝的资源文件,有时候如果一个资源文件在开发过程中发现找不到,可以在这里找一下,看看是不是加进来了。

如果使用了 Cocoapods,那么将会被添加:

  • [CP] Check Pods Manifest.lock:检查 Podfile.lock 和 Manifest.lock 文件的一致性,这个会再后面的 Cocoapods 原理中详细解释。
  • [CP] Embed Pods Frameworks:将所有 cocoapods 打的 framework 拷贝到包中。
  • [CP] Copy Pods Resources:将所有 cocoapods 的资源文件拷贝到包中。

5.3 Build Rules

编译规则,这里设定了不同文件的处理方式,例如:

  • Copy Plist File:在编译打包的时候,将 info.plist 文件拷贝。
  • Compress PNG File:在编译打包的时候,将 PNG 文件压缩。
  • Swift Compiler:Swift 文件的编译方式,使用 Swift 编译器。
  • ….

6. Cocoapods 原理

使用了 Cocoapods 后,我们的编译流程会多出来一些,虽然每个 target 的编译流程都是一致的,但是 Cocoapods 是如何将这些库导入我们的项目、原项目和其他库之间的依赖又是如何实现的仍然是一个需要了解的知识点。下面这几篇文章从不同角度解释了 Cocoapods 是如何工作的:

这些文章大部分都是讲述了 Objective-C 语言下 Cocoapods 是如何使用静态库动态库并添加依赖的。特别说明一下 Swift 的实现:

Cocoapods 对于 Swift 和 Objective-C 的操作区别其实并不是很大,Swift 是必须使用动态库的,因此在 podfile 中我们必须添加代码 use_frameworks! 来指明 Cocoapods 所有管理的库都将被编译成 framework 动态库。这些库的依赖过程和 Objective-C 一致,我们的主项目依赖于 Pods-ProjectName.framework target,而这个 pods 的 target 则依赖于其他我们使用的库。

需要说明一个概念,就是 Swift 的命名空间和 Module。

  • Module:在 Swift 中,项目里的每个 target、framework 都是一个 Module。
  • 命名空间:每个 Module 都拥有独立的命名空间。

因此,我们在使用 Swift 库的时候,例如 Alamofire,在某个文件中 import Alamofire,其实就是在引入这个 Module,在代码提示中可以看到,这样其中的代码才可以被我们使用。

在同一个 Module 内我们的 Swift 文件是不需要被引用的,可以直接使用其中 publicinternal 描述的类属性和方法。这也说明一个问题,在同一个 Module 中,命名空间也是同一个,重复的类都会在编译的时候报错。

7. 更多

LLVM 的编译过程是相当复杂的,中间牵扯到的技术和语言要比我们做一个简单的移动开发要复杂的多,本文也只是非常浅显的描述了相关的编译过程,其主要目的是为了在 iOS 开发中更加得心应手。

研究编译原理、深究编译过程以及其相关内容,在开发过程中是非常重要的一个环节,任何开发者都应该做深入研究。如果你看不懂相关代码,也不要觉得困难,很多代码的命名就能很清楚的表明它到底是干什么的,英语差的话翻译一下就好了。只是相对于英语好,底层代码也略懂的情况来说,学习成本比较高,不过这不应该是打败你的理由。

原理是我们构建整个代码世界的基础,不懂原理(无论哪方面)都只能让我们做一个垒砖的,而不是整个工程的控制者。

2015-02-04 09:52:27 dragoncheng 阅读数 17882

苹果要求老的app需要在6月份后支持64位,新的app从2.1开始就必须支持64bit。由于我们用了luajit,而luajit2.0.x版本只支持32bit。在2.1版本开始支持64bit了,但目前只是alpha版本。

下面的luajit库编译后支持arm64,armv7和模拟器。即一个库支持64bit编译的所有cpu,不需要针对新老iphone做特殊处理。


下载LuaJit

通过GIT下载Luajit代码
git clone http://repo.or.cz/luajit-2.0.git
进入Luajit下载代码目录签出分支v2.1
git checkout v2.1

编译脚本
在luajit-2.0的上一层目录运行
LUAJIT=./luajit-2.1
DEVDIR=`xcode-select -print-path`/Platforms
IOSVER=iPhoneOS8.1.sdk
SIMVER=iPhoneSimulator.sdk
IOSDIR=$DEVDIR/iPhoneOS.platform/Developer
SIMDIR=$DEVDIR/iPhoneSimulator.platform/Developer
IOSBIN=$DEVDIR/../usr/bin/
SIMBIN=$SIMDIR/usr/bin/

BUILD_DIR=$LUAJIT/build

rm -rf $BUILD_DIR
mkdir -p $BUILD_DIR
rm *.a 1>/dev/null 2>/dev/null

echo =================================================
echo ARMV7 Architecture
ISDKF="-arch armv7 -isysroot $IOSDIR/SDKs/$IOSVER"
make -j -C $LUAJIT HOST_CC="gcc -m32 " CROSS=$IOSBIN TARGET_FLAGS="$ISDKF" TARGET=armv7 TARGET_SYS=iOS clean
make -j -C $LUAJIT HOST_CC="gcc -m32 " CROSS=$IOSBIN TARGET_FLAGS="$ISDKF" TARGET=armv7 TARGET_SYS=iOS 
mv $LUAJIT/src/libluajit.a $BUILD_DIR/libluajitA7.a

echo =================================================
echo ARM64 Architecture
ISDKF="-arch arm64 -isysroot $IOSDIR/SDKs/$IOSVER"
make -j -C $LUAJIT HOST_CC="gcc " CROSS=$IOSBIN TARGET_FLAGS="$ISDKF" TARGET=arm64 TARGET_SYS=iOS clean
make -j -C $LUAJIT HOST_CC="gcc " CROSS=$IOSBIN TARGET_FLAGS="$ISDKF" TARGET=arm64 TARGET_SYS=iOS 
mv $LUAJIT/src/libluajit.a $BUILD_DIR/libluajit64bit.a

echo =================================================
echo IOS Simulator Architecture
ISDKF="-arch x86_64 -isysroot $SIMDIR/SDKs/$SIMVER -miphoneos-version-min=7.0"
make -j -C $LUAJIT HOST_CFLAGS="-arch x86_64" HOST_LDFLAGS="-arch x86_64" TARGET_SYS=iOS TARGET=x86_64 clean
make -j -C $LUAJIT HOST_CFLAGS="-arch x86_64" HOST_LDFLAGS="-arch x86_64" TARGET_SYS=iOS TARGET=x86_64 amalg CROSS=$SIMBIN TARGET_FLAGS="$ISDKF"


mv $LUAJIT/src/libluajit.a $BUILD_DIR/libluajitx86_64.a

libtool -o $BUILD_DIR/libluajit21.a $BUILD_DIR/*.a 2> /dev/null


mkdir -p $BUILD_DIR/Headers
cp $LUAJIT/src/lua.h $BUILD_DIR/Headers
cp $LUAJIT/src/lauxlib.h $BUILD_DIR/Headers
cp $LUAJIT/src/lualib.h $BUILD_DIR/Headers
cp $LUAJIT/src/luajit.h $BUILD_DIR/Headers
cp $LUAJIT/src/lua.hpp $BUILD_DIR/Headers
cp $LUAJIT/src/luaconf.h $BUILD_DIR/Headers

mv $BUILD_DIR/libluajit21.a ../lib/ios


注意在编译模拟器的库时:
ISDKF="-arch x86_64 -isysroot $SIMDIR/SDKs/$SIMVER -miphoneos-version-min=7.0"
上面红色字体部分,该段主要用于编译模拟器的luajit库。由于xcode5.0后的更改,必须用这种方式指定编译为模拟器库,否则默认编译为macos.本人在这里折腾了很久。

集成

将luajit/build目录下的libluajit21.a链接到工程。

由于iphone5s以上虚拟机需要x86_64支持,luajit为了支持此模式需要在other linker flags中增加参数(注意,只需要对模拟器添加参数,针对ios不能添加,否则apple不会通过审核)

-pagezero_size 10000 -image_base 100000000

可能陷阱

1: 目前luajit 2.1只是alpha版本,有没有大的bug不清楚

2:在arm64平台下的luajit的bytecode与早前的bytecode有区别无法直接在mac下编译后在arm64平台使用。它使用了最新的lj_gc64与lj_fr2。所以直接在macos下编译的lua代码不能在ios上运行。需要上传源代码在ios下编译



iOS编译VLC SDK

阅读数 1579