编译流程html
Xcode常见编译报错分析前端
应用场景ios
拖更好久了,今天水文一篇。简单介绍下iOS底层编译的相关知识,帮助咱们充分理解了iOS编译的过程,相信会对咱们后续的开发有必定帮助。面试
首先看一下iOS代码是如何从源码变成可执行文件的,有助于咱们了解程序从编译到运行的全流程objective-c
编译器是将编程语言转换为目标语言的程序,大多数编译器由两部分组成:前端和后端。编程
先后端依赖统一格式的中间代码(IR),使得先后端能够独立的变化。新增一门语言只须要修改前端,而新增一个CPU架构只须要修改后端便可。swift
Objective C/C/C++使用的编译器前端是clang,swift是swift,后端都是LLVM。segmentfault
LLVM是一个模块化和可重用的编译器和工具链技术的集合,Clang 是 LLVM 的子项目,是 C,C++ 和 Objective-C 编译器,目的是提供惊人的快速编译,比 GCC 快3倍,后端
LLVM 还能够提供一种代码编写良好的中间表示 IR,这意味着它能够做为多种语言的后端,这样就可以提供语言无关的优化同时还可以方便的针对多种 CPU 的代码生成。缓存
Objective-C的编译器前端是Clang,诞生之初是为了替代GCC,提供更快的编译速度。咱们能够经过下面这张图来了解Clang编译的大体流程:
下面咱们经过clang命令来具体分析下源码编译的流程:
首先在命令行里输入
clang -ccc-print-phases main.m
能够看到源文件编译须要的几个不一样的阶段
➜ clang -ccc-print-phases main.m 0: input, "main.m", objective-c 1: preprocessor, {0}, objective-c-cpp-output //预编译 2: compiler, {1}, ir //编译成中间代码ir 3: backend, {2}, assembler //生成汇编 4: assembler, {3}, object //生成目标文件.O 5: linker, {4}, image //连接成可执行文件 6: bind-arch, "x86_64", {5}, image
接下来咱们新建一个main.m并详细来看下每一个步骤分别作了什么
main.m #include <stdio.h> int main() { printf("hello world\n"); return 0; }
咱们用下面的命令来查看clang预处理的结果:
clang -E main.m
注:若是main.m中用到了UIKit等类,能够在命令后添加-sysroot参数,记得将sdk换成你本机的版本,后续命令解决方法相同。以下所示:
clang -E main.m -isysroot /Applications/Xcode.app/Contents/Developer/Platforms/iPhoneSimulator.platform/Developer/SDKs/iPhoneSimulator13.0.sdk
能够看到预处理后的文件行数有不少,在最后能够找到main函数
# 13 "/Applications/Xcode.app/Contents/Developer/Platforms/iPhoneSimulator.platform/Developer/SDKs/iPhoneSimulator13.0.sdk/System/Library/Frameworks/UIKit.framework/Headers/ShareSheet.h" 2 3 # 17 "/Applications/Xcode.app/Contents/Developer/Platforms/iPhoneSimulator.platform/Developer/SDKs/iPhoneSimulator13.0.sdk/System/Library/Frameworks/UIKit.framework/Headers/UIKit.h" 2 3 # 10 "main.m" 2 # 1 "./AppDelegate.h" 1 # 11 "./AppDelegate.h" @interface AppDelegate : UIResponder <UIApplicationDelegate> @property (strong, nonatomic) UIWindow *window; @end # 11 "main.m" 2 int main(int argc, char * argv[]) { @autoreleasepool { return UIApplicationMain(argc, argv, ((void *)0), NSStringFromClass([AppDelegate class])); } }
预处理会替进行头文件引入(递归操做),宏替换#define,注释处理,条件编译(#ifdef),#pargma处理等操做。好比#include "stdio.h"就是告诉预处理器将这一行替换成头文件stdio.h中的内容,这个过程是递归的:由于stdio.h也有可能包含其头文件。
预处理完成后就会进行词法分析,这里会把代码切成一个个 Token,好比大小括号,等于号还有字符串等。
clang -fmodules -fsyntax-only -Xclang -dump-tokens main.m
语法分析会校验语法的正确性,而后将全部的节点组成抽象语法树AST。有了抽象语法树,clang就能够对这个树进行分析,找出代码中的错误。好比类型不匹配,亦或Objective C中向target发送了一个未实现的消息。
业内对Clang自定义插件或者开发静态检测插件都是基于AST语法树来分析。相关知识后续会学到。AST是开发者编写clang插件主要交互的数据结构,clang也提供不少API去读取AST。更多细节: Introduction to the Clang AST。
clang -fmodules -fsyntax-only -Xclang -ast-dump main.m
在输出里能够看到相关的AST结果,以下图:
CodeGen 会负责将语法树自顶向下遍历逐步翻译成 LLVM IR,IR 是编译过程的前端的输出,也是后端的输入。
Objective C代码也在这一步会进行runtime的桥接:property合成,ARC处理等。
clang -S -fobjc-arc -emit-llvm main.m -o main.ll
查看main.ll的内容以下:
... ; Function Attrs: noinline optnone 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 i8* @llvm.objc.autoreleasePoolPush() #1 %7 = load i32, i32* %4, align 4 %8 = load i8**, i8*** %5, align 8 %9 = load %struct._class_t*, %struct._class_t** @"OBJC_CLASSLIST_REFERENCES_$_", align 8 %10 = bitcast %struct._class_t* %9 to i8* %11 = call i8* @objc_opt_class(i8* %10) %12 = call %0* @NSStringFromClass(i8* %11) %13 = bitcast %0* %12 to i8* %14 = notail call i8* @llvm.objc.retainAutoreleasedReturnValue(i8* %13) #1 %15 = bitcast i8* %14 to %0* %16 = call i32 @UIApplicationMain(i32 %7, i8** %8, %0* null, %0* %15) store i32 %16, i32* %3, align 4 %17 = bitcast %0* %15 to i8* call void @llvm.objc.release(i8* %17) #1, !clang.imprecise_release !10 call void @llvm.objc.autoreleasePoolPop(i8* %6) %18 = load i32, i32* %3, align 4 ret i32 %18 } ; Function Attrs: nounwind ...
若是在项目配置中开启了 bitcode, 苹果还会作进一步的优化,有新的后端架构仍是能够用这份优化过的 bitcode 去生成。
clang -emit-llvm -c main.m -o main.bc
clang -S -fobjc-arc main.m -o main.s
汇编器以汇编代码做为输入,将汇编代码转换为机器代码,最后输出目标文件(object file)
clang -fmodules -c main.m -o main.o
接下来咱们用nm命令,查看下main.o中的符号
➜ BuildTest nm -nm main.o (undefined) external _printf 0000000000000000 (__TEXT,__text) external _main
这里能够看到_printf是一个是undefined external的。undefined表示在当前文件暂时找不到符号_printf,而external表示这个符号是外部能够访问的,对应表示文件私有的符号是non-external。
连接器能够把编译产生的.o文件和(dylib,a,tbd)文件,生成一个mach-o文件
clang main.o -o main
接着在命令行执行./main,能够看到输出告终果:hello world。
最后咱们用nm命令来分析下可执行文件的符号表:
➜ BuildTest 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 0000000100002008 (__DATA,__data) non-external __dyld_private
能够看到_printf仍然是undefined,可是后面多了一些信息:from libSystem,表示这个符号来自于libSystem,会在运行时动态绑定。
以上就是Clang编译源文件的完整流程了。
若是你想在 Xcode 中查看,能够经过 Show the report navigator 里对应 target 的 build 中查看每一个 .m 文件的 clang 编译信息,以下图:
随便找一个.m文件编译信息,能够看到Xcode会首先对任务进行描述:
CompileC /Users/chenaibin/Library/Developer/Xcode/DerivedData/PodIntegrationDemo-achbuytjuwbatqbzvlwflifarxwa/Build/Intermediates.noindex/Pods.build/Debug-iphonesimulator/podLibB.build/Objects-normal/x86_64/podClsB.o /Users/chenaibin/Work/DiDi/iOSDemo/BuildErrorDemo/podLibB/Classes/podClsB.m normal x86_64 objective-c com.apple.compilers.llvm.clang.1_0.compiler (in target 'podLibB' from project 'Pods')
接下来对会更新工做路径,同时设置 PATH
cd /Users/chenaibin/Work/DiDi/iOSDemo/BuildErrorDemo/PodIntegrationDemo/Pods export LANG=en_US.US-ASCII export PATH="/Applications/Xcode.app/Contents/Developer/Platforms/iPhoneSimulator.platform/Developer/usr/bin:/Applications/Xcode.app/Contents/Developer/usr/bin:/usr/local/bin:/usr/bin:/bin:/usr/sbin:/sbin"
接下来就是实际的编译命令
/Applications/Xcode.app/Contents/Developer/Toolchains/XcodeDefault.xctoolchain/usr/bin/clang -x objective-c -target x86_64-apple-ios9.0-simulator -fmessage-length=0 -fobjc-arc… -Wno-missing-field-initializers ... -DDEBUG=1 ... -isysroot /Applications/Xcode.app/Contents/Developer/Platforms/iPhoneSimulator.platform/Developer/SDKs/iPhoneSimulator13.0.sdk -iquote ... -I... -F...-c /.../podClsB.m -o /.../podClsB.o
clang 用到的命令参数以下:
-x 编译语言好比objective-c -arch 编译的架构,好比arm64 -f 以-f开头的。 -W 以-W开头的,能够经过这些定制编译警告 -D 以-D开头的,指的是预编译宏,经过这些宏能够实现条件编译 -iPhoneSimulator13.0.sdk 编译采用的iOS SDK版本 -I 把编译信息写入指定的辅助文件 -F 须要的Framework -c 标识符指明须要运行预处理器,语法分析,类型检查,LLVM生成优化以及汇编代码生成.o文件 -o 编译结果
第一个常见的编译报错缘由就是duplicate symbols,以下图就是由于咱们连接后的可执行文件存在了重复的类致使的。
注:因为咱们工程是由CocoaPods构建的,在xcconfig中OTHER_LINK_FLAG都会被默认设置成$(inherited) -ObjC ......,这会致使工程配置里Other Linker Flags会带上 -ObjC标记,若是咱们手动删除了-ObjC,就会发如今编译时不会有duplicate symbols的错误了。可是运行的时候可能会出现unrecognized selector sent to class XXX的错误,这是因为静态库中的分类并没被连接器连接进可执行文件中。
-ObjC会把静态库中全部的类和分类都连接进可执行文件,因此会出现duplicate symbols的错误。下面是官方描述:
This flag causes the linker to load every object file in the library that defines an Objective-C class or category. While this option will typically result in a larger executable (due to additional object code loaded into the application), it will allow the successful creation of effective Objective-C static libraries that contain categories on existing classes.
第二个常见报错是在某个架构下找不到相关符号,这是由于引用的某个静态库并无包含当前工程制式下的架构类型,解决方案是将静态库.a文件合并x86_64/arm64等架构为fat file,再集成到工程里使用。
报错缘由以下图:
提示:遇到这种状况时,有时候屡次pod update也不能解决报错缘由。这是由于你本地缓存了有问题的静态库文件,可在如下目录下找到相关类库并删除,再执行pod install下载fix后的静态库文件。CocoaPods官方缓存目录:~/Library/Caches/CocoaPods/Pods
这个错误还有另一种状况,当同一个pod在多个不一样的端集成时可能会遇到。报错信息大体以下:
问题缘由:在ProjectA中集成了podA和podB,podA使用了#if __has_include("podB中的cls.h")集成了podB中的类;当切换到ProjectB时,只会依赖podA一个库,这个时候编译就会上图中的错误。
解决方案:在ProjectB中将podA以源码从新编译一遍便可。
在平时开发中,咱们常常会遇到头文件里有__attribute__的用法,它是一个高级的的编译器指令,它容许开发者指定更更多的编译检查和一些高级的编译期优化。
__attribute__
语法格式为:__attribute__ ((attribute-list)) 放在声明分号“;”前面。
好比,在三方库中最多见的,声明一个属性或者方法在当前版本弃用了
@property (strong,nonatomic)CLASSNAME * property __deprecated;
下面是 iOS开发中常见的几个 __attribute__
用法:
//弃用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)))
当咱们在XCode中屏蔽部分Warning信息时,可使用下面的内容来解决。经过clang diagnostic push/pop来控制代码块的编译选项。
#pragma clang diagnostic push #pragma clang diagnostic ignored "-Wundeclared-selector" ///代码 #pragma clang diagnostic pop
预处理可让咱们让咱们自定义编译器变量,实现条件编译。 好比咱们经常使用的DEBUG宏:
#ifdef DEBUG //... #else //... #endif
咱们能够在XCode的Target中选中Build Setting选项,搜索proprecess,便可看到定义好的预处理宏。
目前iOS基本都是用CocoaPods来管理工程,咱们也能够在每一个Pod的podspec文件中配置预编译宏,CocoaPods会在构建工程时将这些信息写到Pod的xcconfig文件里。
# Pod.podspec示例 s.subspec 'YourSubSpec' do | ss | ss.source_files = 'Pod/Classes/**/*' ss.pod_target_xcconfig = { 'GCC_PREPROCESSOR_DEFINITIONS' => '$(inherited) YOUR_CUSTOM_DEFINE=1' } end
注意:podA定义的GCC_PREPROCESSOR_DEFINITIONS内容在podB中是不生效的!!!
若是想解决这个问题,推荐podB中单独定义一个subspec来配置预编译宏的值,在外层工程里经过区分是否引入podB的subspec来实现该预编译宏值的控制。
上面介绍到语法分析以后咱们能够拿到抽象语法树AST,接着就能够对这个树进行分析,作静态代码分析或者无用代码分析均可以,网上也有不少资料介绍这块的研究。感兴趣的能够搜索下或者看下 Introduction to the Clang AST
以上内容主要介绍了下iOS编译相关的知识,若有内容错误,欢迎指正。
做为一个开发者,有一个学习的氛围跟一个交流圈子特别重要,这是一个个人iOS交流群:789143298 ,无论你是小白仍是大牛欢迎入驻 ,分享BAT,阿里面试题、面试经验,讨论技术, 你们一块儿交流学习成长!