Code 是怎么变成 .ipa 包的
作为一名 iOS 开发工程师,“把代码变成.ipa
的可安装到手机上的应用发生了什么”应该是要了解的。提出这么几个问题,去探索一下背后的原理。
这个过程做了什么?
程序员可以在这个过程中做什么?
第一个问题:做了什么
- Archive
- 打包 .ipa
Archive
- 1.进行编译
- 2.生成一个 DSYM 文件(存储了16进制的函数地址映射)
奔溃日志中的地址通过此文件由地址映射到具体的函数位置。
Xcode 的编译器是由 Clang + LLVM 组成
编译器是做什么的?
编译器是用来把源代码文件转换为更为低级的语言的(同时还有语句的静态分析),而 xcode 使用的clang 编译器的作用就是把源代码转换为更为低级的 LLVM IR(Intermedia Representation),这个 LLVM IR 是操作系统无关的,然后 LLVM 通过这个中间语言来进行下一步的二进制文件的产出
- Clang 编译器
作为编译器前端,作用是:语法分析,语义分析,生成中间代码.在这个过程中,会进行类型检查,如果发现错误或者警告会标注出来在哪一行。
通过$ clang -ccc-print-phases <file name>.m
观察单个文件的编译过程
2.编译生成IR(中间代码)
语法分析,语义分析(这个过程就是 Compiling)
3.汇编器生成汇编代码
4.生成机器码
5.链接
6.生成Image,也就是最后的可执行文件
预处理
会处理源文件中的宏定义,将代码中的宏用其对应定义的具体内容进行替换,展开头文件:
#import <Foundation/Foundation.h>
#define MY_CONSTANT 4
#ifdef DEBUG
//...
#else
//...
#endif
每次 ⌘B 发生的事情(使用cocoa pods)
- 编译信息写入辅助文件,创建文件架构 .app 文件
- 处理文件打包信息
- 执行 CocoaPod 编译前脚本,checkPods Manifest.lock
- 编译.m文件,使用 CompileC 和 clang 命令
- 链接需要的 Framework
- 编译 xib
- 拷贝 xib ,资源文件
- 编译 ImageAssets
- 处理 info.plist
- 执行 CocoaPod 脚本
Run custom shell script 'Embed Pods Frameworks'
Run custom shell script 'Copy Pods Resources'
- 拷贝标准库
- 创建 .app 文件和签名
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 编译产物
可以通过 XCode的Link Map File 窥探二进制文件中的布局
- 在此目录下可以找到文件
~/Library/Developer/Xcode/DerivedData/<TARGET-NAME>-对应ID/Build/Intermediates/<TARGET-NAME>.build/Debug-iphoneos/<TARGET-NAME>.build/
文件内容包含 Object files,Sections,Symbols
- Object files
这个部分的内容都是 .m 文件编译后的 .o 和需要 link 的 .a 文件。前面是文件编号,后面是文件路径。
- Sections
这里描述的是每个 Section 在可执行文件中的位置和大小。每个 Section 的 Segment 的类型分为 __TEXT 代码段和 __DATA 数据段两种。
- Symbols
Symbols 是对 Sections 进行了再划分。这里会描述所有的 methods,ivar 和字符串,及它们对应的地址,大小,文件编号信息。
第二个问题:程序员可以做什么
以 attribute(xx) 的语法格式出现,是 Clang 提供的一些能够让开发者在编译过程中参与一些源码控制的方法。下面列一些会用到的用法:
attribute((format(NSString, F, A)))
格式化字符串
- (void)preMethod:( NSString *)string __attribute__((deprecated("preMethod已经被弃用,请使用newMethod")));
- (void)deprecatedMethod DEPRECATED_ATTRIBUTE; //也可以直接使用DEPRECATED_ATTRIBUTE这个系统定义的宏
//如果没有使用返回值,编译的时候给出警告
#define __unused_result __attribute__ ((warn_unused_result))
//指定不能有子类
attribute((objc_subclassing_restricted))
//子类继承必须调用 super
attribute((objc_requires_super))
//带描述信息的弃用
#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
这段代码的作用是
对当前编译环境进行压栈
忽略-Wundeclared-selector(未声明的)Selector警告
编译代码
对编译环境进行出栈
通过clang diagnostic push/pop,你可以灵活的控制代码块的编译选项。
在 Xcode 的 Project editor 中的 Build Setting,Build Phases 和 Build Rules 能够控制编译的过程
Build Setting
- 配合多个Target(右键Target,选择Duplicate),单独一个Target负责测试服务器。这样我们就不用每次切换测试服务器都要修改代码了
-
warnings是编码中很重要的一个环节,编译器给出合理的warning能帮助开发者找到自己代码的问题,防止很多bug产生。
在search里搜索Warnings,就可以看到如图,这是为所有语言开启的warnings
默认用XCode创建一个工程,会自动开启一些重要的warnings,但是更多的时候,我们需要编译器更完整的提醒
默认的Warning可以在Build Settings里找到
当然,也可以为不同语言开启warning,也在Build Settings里
详情看这里
Build Phases
在 Compile Source 中指定所有必须编译的文件,这些文件会根据 Build Setting 和 Build Rules 里的设置来处理
在 Link Binary With Libraries 里会列出所有的静态库和动态库,它们会和编译生成的目标文件进行链接
build phase 还会把静态资源拷贝到 bundle 里
通过右上角的新增,可以添加自己的运行脚本。
Build Rules
指定不同文件类型如何编译。每条 build rule 指定了该类型如何处理以及输出在哪。可以增加一条新规则对特定文件类型添加处理方法
build 过程控制的这些设置都会被保存在工程文件 .pbxproj 里
提高项目编译速度
通过$ defaults write com.apple.dt.Xcode ShowBuildOperationDuration YES
可以看到 Xcode 编译时间(首先要关闭 Xcode)
forward declaration
选择@class CLASSNAME
,而不是#import CLASSNAME.h
预处理器对 #import 处理是用 CLASSNAME.h 文件中的内容去替换这行代码
@class 是告诉编译器 CLASSNAME 是一个类,并且在 .m 实现文件中可以通过 import CLASSNAME.h 的方式来使用它。
对常用的工具类进行打包(Framework/.a)
打包成Framework或者静态库,这样编译的时候这部分代码就不需要重新编译了。
常用头文件放到预编译文件里
XCode的pch文件是预编译文件,这里的内容在执行XCode build之前就已经被预编译,并且引入到每一个.m文件里了。
二. 生成 .ipa
包
- Mach-O文件
是Mac和iOS可以执行文件的格式。进程就是系统根据该格式将执行文件加载到内存后得到的结果。系统通过解析文件,建立依赖(动态库),初始化运行时环境,才能真正开始执行该App(进程)
参考文章:
https://segmentfault.com/a/1190000003101087
https://blog.csdn.net/Hello_Hwc/article/details/53557308
https://blog.csdn.net/forwardto9/article/details/51656274
https://juejin.im/post/5a352bb0f265da433562d5e3
https://blog.csdn.net/bjtufang/article/details/50628310
https://blog.csdn.net/vincentiss/article/details/54617915
https://objccn.io/issue-6-3/