习惯了IDE以及各种现成的编译工具为我们提供便捷的编译方式,我们很少会操心编译工具的编译过程和原理,但是工具越高级,隐藏的细节就越多,这样编译遇到问题时我们难以定位,遇到复杂的项目(尤其跨平台项目难以用ide)时不知如何下手。所以准备写两篇关于编译器和编译工具的文章。本文先来介绍编译工具。
主要从事Android开发,本文主要介绍Android、iOS用到的编程语言及编译器。
Java/Kotlin/Groovy
这三种编程语言都是基于Java虚拟机的。由于JVM的存在,所以Java既是编译型语言,又是解释型语言。将JVM理解成操作系统,它是编译型语言;从物理操作系统的角度,它又是解释型的。JVM负责把编译成的.class解释成最终CPU理解的二进制字节。它为了实现跨平台牺牲了效率。
Java编译工具
我们还是先以一个最简单的HelloWorld开始。
public class HelloWorld{ public static void main(String[] args){ System.out.println("Hello, World!"); } }
命名成HelloWorld.java。还记得Java为我们分别提供了编译工具javac和执行工具java吗?我们使用javac编译:
javac HelloWrold.java
在与HelloWorld.java统计目录下看到生成了HelloWorld.class,我们继续执行该class文件:
qingkouwei:~/javaLinux/w1$ java HelloWorld.class Error: Could not find or load main class HelloWorld.class
报错了,我们回想一下java
的参数,传入的是main函数所在的类的名字,而不是class文件;java会根据类名自动去找class文件。我们改成java HelloWorld
就可以成功看到输出结果了。
带包名类编译
上面例子太简单了,我们加上包名来一遍:
package com.qingkouwei.demo; public class HelloWorld{ public static void main(String[] args){ System.out.println("Hello, World!"); } }
使用javac编译后在当前目录生成了HelloWorld.class,运行java HelloWorld
后报错:
Error: Could not find or load main class com.qingkouwei.demo.HelloWorld
这里包名需要和文件路径相对应,创建com/qingkouwei/demo
目录,将HelloWorld.class放进来,执行java com.qingkouwei.demo.HelloWorld
成功输出:
Hello, World!
这里说明两点:
- 增加了package名,所以class名也变了,执行时要使用包名+类名的方式。
- Java 会根据包名对应出目录结构,并从class path搜索该目录去找class文件。由于默认的class path是当前目录,所以com.qingkouwei.demo.HelloWorld必须存储在
./com/qingkouwei/demo/
下。
我们还可以使用javac命令的-d
参数指定编译路径:
qingkouwei@mac javac -d . HelloWorld.java qingkouwei@mac ls com HelloWorld.java qingkouwei@mac java com.qingkouwei.demo.HelloWorld Hello, World!
编译有依赖关系的class
我们将打印Hello World的方法封装成一个Hello工厂类HelloFactory:
package com.qingkouwei.demo; public class HelloFactory{ public void printHello(String name){ System.out.println("Hello, " + name + "!"); } }
HelloWorld调用该方法:
package com.qingkouwei.demo; public class HelloWorld{ public static void main(String[] args){ HelloFactory factory = new HelloFactory(); factory.printHello("World"); } }
这样HelloWorld依赖了HelloFactory,我们先编译HelloFactory.java,再编译HelloWorld.java:
qingkouwei@mac javac -d . HelloFactory.java qingkouwei@mac javac -d . HelloWorld.java qingkouwei@mac ls com HelloFactory.java HelloWorld.java qingkouwei@mac java com.qingkouwei.demo.HelloWorld Hello, World!
如果修改编译顺序呢:
qingkouwei@mac javac -d . HelloWorld.java HelloWorld.java:4: error: cannot find symbol HelloFactory service = new HelloFactory(); ^ symbol: class HelloFactory location: class HelloWorld HelloWorld.java:4: error: cannot find symbol HelloFactory service = new HelloFactory(); ^ symbol: class HelloFactory location: class HelloWorld 2 errors
如果编译的时候,还要我们手动管理依赖关系,代价太大了,当工程复杂度上来后,几乎就是不可维护的,我们一次性将两个java文件传给javac:
qingkouwei@mac javac -d . HelloWorld.java HelloFactory.java qingkouwei@mac ls com HelloFactory.java HelloWorld.java qingkouwei@mac java com.qingkouwei.demo.HelloWorld Hello, World!
javac是可以自动管理依赖关系的。
javac命令总结
javac的语法如下:
javac [ options ] [ sourcefiles ] [ classes] [ @argfiles ]
- options:是一些选项,比如-cp,-d
- sourcefiles:就是编译的java文件,如
HelloWorld.java
,可以是多个,并用空格隔开 - classes:用来处理处理注解。
- @argfiles,就是包含option或java文件列表的文件路径,用@符号开头
Kotlin
Kotlin 是一种在 Java 虚拟机上运行的静态类型编程语言,被称之为 Android 世界的Swift,由 JetBrains 设计开发并开源。Kotlin 可以编译成Java字节码,也可以编译成 JavaScript,方便在没有 JVM 的设备上运行。在Google I/O 2017中,Google 宣布 Kotlin 成为 Android 官方开发语言。它要运行在JVM中的时候其实和java是一个爹,很多东西都是类似的,只是在语法上做了一些改进。
简单介绍下Kotlin命令行编译。
Kotlin 命令行编译工具下载地址:github.com/JetBrains/k… bin 目录添加到系统环境变量。bin 目录包含编译和运行 Kotlin 所需的脚本。Mac上可以直接使用brew install kotlin
安装。
创建helloworld.kt文件:
fun main(args: Array<String>) { println("Hello, World!") }
使用 Kotlin 编译器编译应用:
kotlinc helloworld.kt -include-runtime -d hello.jar
-d
: 用来设置编译输出的名称,可以是 class 或 .jar 文件,也可以是目录。-include-runtime
: 让 .jar 文件包含 Kotlin 运行库,从而可以直接运行。
可以通过kotlinc -help
查看支持的编译选项。
运行:
java -jar hello.jar Hello, World!
kotlinc是和javac类似的作用。
Groovy
Groovy是一种基于JVM Java虚拟机的敏捷开发语言,它结合了Python、Ruby和Smalltalk的许多强大的特性,Groovy 代码能够与 Java 代码很好地结合,也能用于扩展现有代码。由于其运行在 JVM 上的特性,Groovy也可以使用其他非Java语言编写的库。具体使用不再展开。
C/C++
linux上主流的C/C++编译器是GCC与clang。
GCC
GCC是GNU(Gnu's Not Unix)编译器套装(GNU Compiler Collection,GCC),是一套编程语言编译器,以GPL及LGPL许可证所发行的自由软件,也是GNU项目的关键部分,也是GNU工具链的主要组成部分之一。GCC(特别是其中的C语言编译器)也常被认为是跨平台编译器的事实标准。1985年由理查德·马修·斯托曼开始发展,现在由自由软件基金会负责维护工作。GCC原本用C开发,后来因为LLVM、Clang的崛起,它更快地将开发语言转换为C++。
gcc/g++ 在执行编译工作的时候,总共需要4步:
- 预处理,生成 .i 的文件[预处理器cpp]
- 将预处理后的文件转换成汇编语言, 生成文件 .s [编译器egcs]
- 有汇编变为目标代码(机器代码)生成 .o 的文件[汇编器as]
- 连接目标代码, 生成可执行程序 [链接器ld]
gcc编译命令及示例
创建main文件:
#include <stdio.h> int main(int argc, const char * argv[]) { // insert code here... printf("Hello, World!\n"); return 0; }
- 预处理:
gcc -E hello.c > pianoapan.txt
,-E只激活预处理,不生成文件, 需要把它重定向到一个输出文件里面。 - 生成汇编:
gcc -S hello.c
-S只激活预处理和编译,就是指把文件编译成为汇编代码。 - 编译:
gcc -c hello.c
,-c只激活预处理,编译,和汇编,也就是他只把程序做成obj文件 - 连接:
gcc -o hello hello.c
,一步到位编译成可使用库。 -o指定目标名称, 默认的时候, gcc 编译出来的文件是 a.out。
gcc 命令的常用选项:
选项 | 解释 |
-ansi | 只支持 ANSI 标准的 C 语法。这一选项将禁止 GNU C 的某些特色, 例如 asm 或 typeof 关键词。 |
-c | 只编译并生成目标文件。 |
-DMACRO | 以字符串"1"定义 MACRO 宏。 |
-DMACRO=DEFN | 以字符串"DEFN"定义 MACRO 宏。 |
-E | 只运行 C 预编译器。 |
-g | 生成调试信息。GNU 调试器可利用该信息。 |
-IDIRECTORY | 指定额外的头文件搜索路径DIRECTORY。 |
-LDIRECTORY | 指定额外的函数库搜索路径DIRECTORY。 |
-lLIBRARY | 连接时搜索指定的函数库LIBRARY。 |
-m486 | 针对 486 进行代码优化。 |
-o FILE | 生成指定的输出文件。用在生成可执行文件时。 |
-O0 | 不进行优化处理。 |
-O 或 -O1 | 优化生成代码。 |
-O2 | 进一步优化。 |
-O3 | 比 -O2 更进一步优化,包括 inline 函数。 |
-shared | 生成共享目标文件。通常用在建立共享库时。 |
-static | 禁止使用共享连接。 |
-UMACRO | 取消对 MACRO 宏的定义。 |
-w | 不生成任何警告信息。 |
-Wall | 生成所有警告信息。 |
Clang
Clang:是一个C、C++、Objective-C和Objective-C++编程语言的编译器前端。它采用了底层虚拟机(LLVM)作为其后端。它的目标是提供一个GNU编译器套装(GCC)的替代品。作者是克里斯·拉特纳(Chris Lattner),在苹果公司的赞助支持下进行开发,而源代码授权是使用类BSD的伊利诺伊大学厄巴纳-香槟分校开源码许可。Clang主要由C++编写。
上面都提到了LLVM,LLVM是构架编译器的框架系统,以C++编写而成,用于优化以任意程序语言编写的程序的编译时间(compile-time)、链接时间(link-time)、运行时间(run-time)以及空闲时间(idle-time),对开发者保持开放,并兼容已有脚本。clang其实是LLVM项目的一个子项目,基于LLVM架构的C/C++/Objective-C编译器前端。
clang编译命令
1.无选项编译链接 用法:#clang hello.c
作用:将hello.c预处理、汇编、编译并链接形成可执行文件。这里未指定输出文件,默认输出为a.out。编译成功后可以看到生成了一个a.out的文件。在命令行输入./a.out 执行程序。./表示在当前目录,a.out为可执行程序文件名。
2.选项 -o 用法:#clang hello.c -o hello
作用:将hello.c预处理、汇编、编译并链接形成可执行文件hello.c。-o选项用来指定输出文件的文件名。输入./hello执行程序。
3.选项 -E 用法:#clang -E hello.c -o hello.i
作用:将hello.c预处理输出hello.i文件。
4.选项 -S 用法:#clang -S hello.i
作用:将预处理输出文件hello.i汇编成hello.s文件。
5.选项 -c 用法:#clang -c hello.s 作用:将汇编输出文件hello.s编译输出hello.o文件。
6.无选项链接 用法:#clang hello.o -o hello
作用:将编译输出文件hello.o链接成最终可执行文件hello。输入./hello执行程序。
如果想直接输入hello就运行,需要把hello复制到目录/usr/bin下
7.选项-O 用法:#clang -O1 hello.c -o hello
作用:使用编译优化级别1编译程序。级别为1~3,级别越大优化效果越好,但编译时间越长。输入./hello执行程序。
8.编译使用C++ std库的程序
用法:#clang hello.cpp -o hello -l std c++
作用:将hello.cpp编译链接成test可执行文件。-l std c++指定链接std c++库。
我们可以看到clang和gcc的编译选项是类似的。
NDK
在Android开发中我们使用的是NDK工具,通常使用ndk-build来跨平台编译Android c/c++库。ndk-build是种什么编译工具呢?
我们查看下ndk-build文件:
$ cat ndk-build #!/bin/sh DIR="$(cd "$(dirname "$0")" && pwd)" $DIR/build/ndk-build "$@"%
发现ndk-build只是个指向/build/ndk-build的脚本,查看后发现/build/ndk-build也是一个脚本:
$ cat build/ndk-build #!/bin/bash ... # Check that we have 64-bit binaries on 64-bit system, otherwise fallback # on 32-bit ones. This gives us more freedom in packaging the NDK. LOG_MESSAGE= if [ $HOST_ARCH = x86_64 ]; then if [ ! -d $ANDROID_NDK_ROOT/prebuilt/$HOST_TAG ]; then HOST_ARCH=x86 LOG_MESSAGE="(no 64-bit prebuilt binaries detected)" fi fi ... get_build_var () { local VAR=$1 local FLAGS=`gen_flags` $GNUMAKE --no-print-dir -f $PROGDIR/core/build-local.mk $FLAGS DUMP_${VAR} | tail -1 } ... APP_ABIS=`get_build_var APP_ABI` for ABI in $APP_ABIS; do perl ${LLVM_TOOLCHAIN_PREFIX}scan-build \ --use-cc $ANALYZER_CC \ --use-c++ $ANALYZER_CXX \ --status-bugs \ $ANALYZER_OUT_FLAG \ $GNUMAKE -f $PROGDIR/core/build-local.mk "$@" APP_ABI=$ABI done else $GNUMAKE -O -f $PROGDIR/core/build-local.mk "$@" fi
我们看到最终起作用的是GNUMAKE变量,我们看到GNUMAKE=$ANDROID_NDK_ROOT/prebuilt/$HOST_TAG/bin/make
,所以不管是ndk-build项目还是cmake项目,最终还是只用make编译工具来调用具体编译器来编译。
在r8c中 添加了Clang 3.1 编译器,在NDK r12中,ndk-build建议使用clang编译器,r13b中gcc不再支持,NDK_TOOLCHAIN_VERSION默认使用clang,r14b中gcc被弃用,使用gcc通过-D_ANDROID_API_=$API指定具体的API,在r18b中,移除gcc,gnustl,gabi++,stlport等。具体ndk修订历史记录可以参考官网:
developer.android.com/ndk/downloa…
总结一下,ndk是用于跨平台编译c/c++代码的工具集合,它提供了编译代码的三种方式:
- 基于Make的ndk-build
- CMake
- 独立工具链,用于与其他构建系统集成,或与基于
configure
的项目搭配使用。
最终它还是使用GCC/Clang编译器。
dart
Dart 是一个为全平台构建快速应用的客户端优化的编程语言。它主要用于谷歌退出的跨平台框架Flutter。Dart编译特点:
- Just-In-Time即时编译。在应用运行时同时执行代码编译,让flutter具备极速的开发体验。具备亚秒级的热重载(Hot Reloading)特性。
- Ahead-Of-Time运行前编译。将代码库直接编译成原生的ARM指令。为应用带来快速的启动速度和可预见的卓越性能。
Dart编译工具使用
官网dart.dev/tools/sdk/a…下载编译工具,解压配置环境变量后,可以通过dart --version
查看版本。
使用dart2native构建和部署命令行程序。我们准备main.dart源文件:
main(){ print('Hello World'); }
使用命令dart2native main.dart -o hello
编译为可执行文件。
oc/swift
oc
苹果通过Xcode来学习编译Objective-C编程语言,XCode的默认编译器是clang。
准备代码main.m
#import <Foundation/Foundation.h> int main(int argc, const char * argv[]) { @autoreleasepool { NSLog(@"Hello, World!"); } return 0; }
使用命令clang -fobjc-arc -framework Foundation main.m -o HelloWorld
,编译完成后即可运行:./hello
。
注意:
- -fobjc-arc表示编译器需要支持ARC特性
- -framework Foundation表示引用Foundation框架
- main.m为需要进行编译的源代码文件
- -o HelloWord表示输出的可执行文件的文件名
swift
Swift 是一种支持多编程范式和编译式的开源编程语言,苹果于2014年WWDC(苹果开发者大会)发布,用于开发 iOS,OS X 和 watchOS 应用程序。Swift 结合了 C 和 Objective-C 的优点并且不受 C 兼容性的限制。Swift 在 Mac OS 和 iOS 平台可以和 Object-C 使用相同的运行环境。
swift的开发工具仍然为xcode,我们在命令行使用xcrun swift -emit-executable -sdk $(xcrun --show-sdk-path --sdk macosx) sample.swift
编译swift程序。
总结
本文主要介绍了移动端相关的编译工具,都是基础的入门工具,但是对于我们日后面对复杂的大型项目提供帮助,特别是一些跨平台的C/C++项目,一份代码一个脚本编译出所有平台的程序,都需要我们能够熟练驾驭这些编译工具。