Android编译器及编译工具之编译器

·  阅读 504
Android编译器及编译工具之编译器

「这是我参与11月更文挑战的第4天,活动详情查看:2021最后一次更文挑战」。

习惯了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!
复制代码

这里说明两点:

  1. 增加了package名,所以class名也变了,执行时要使用包名+类名的方式。
  2. 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 ]
复制代码
  1. options:是一些选项,比如-cp,-d
  2. sourcefiles:就是编译的java文件,如HelloWorld.java,可以是多个,并用空格隔开
  3. classes:用来处理处理注解。
  4. @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步:

  1. 预处理,生成 .i 的文件[预处理器cpp]
  2. 将预处理后的文件转换成汇编语言, 生成文件 .s [编译器egcs]
  3. 有汇编变为目标代码(机器代码)生成 .o 的文件[汇编器as]
  4. 连接目标代码, 生成可执行程序 [链接器ld]

gcc编译命令及示例

创建main文件:

#include <stdio.h>

int main(int argc, const char * argv[]) {
    // insert code here...
    printf("Hello, World!\n");
    return 0;
}
复制代码
  1. 预处理:gcc -E hello.c > pianoapan.txt ,-E只激活预处理,不生成文件, 需要把它重定向到一个输出文件里面。
  2. 生成汇编:gcc -S hello.c -S只激活预处理和编译,就是指把文件编译成为汇编代码。
  3. 编译:gcc -c hello.c ,-c只激活预处理,编译,和汇编,也就是他只把程序做成obj文件
  4. 连接: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下

  1. 选项-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++代码的工具集合,它提供了编译代码的三种方式:

  1. 基于Make的ndk-build
  2. CMake
  3. 独立工具链,用于与其他构建系统集成,或与基于 configure 的项目搭配使用。

最终它还是使用GCC/Clang编译器。

dart

Dart 是一个为全平台构建快速应用的客户端优化的编程语言。它主要用于谷歌退出的跨平台框架Flutter。Dart编译特点:

  1. Just-In-Time即时编译。在应用运行时同时执行代码编译,让flutter具备极速的开发体验。具备亚秒级的热重载(Hot Reloading)特性。
  2. 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++项目,一份代码一个脚本编译出所有平台的程序,都需要我们能够熟练驾驭这些编译工具。

参考

分类:
Android
分类:
Android