Android-NDK/JNI

AndroidNDK——makefile语法详解

2020-02-12  本文已影响0人  GitLqr

一、编译流程详解

编译流程

1、预处理

完成宏替换、文件引入,以及去除空行、注释等,为下一步的编译做准备;也就是对各种预处理命令进行处理,包括头文件的包含、宏定义的扩展、条件编译的选择等。

// test.c文件内容
#include <stdio.h>
int main(){
    printf("hello world!\n");
    return 0;
}

对test.c文件进行预处理:

$ gcc -E test.c -o test.i

此时,test.i 就是 test.c 预编译后的产物,体积会增大,此时test.i还是一个文本文件,可以用文本编译器打开查看。

2、编译

extern int ftrylockfile (FILE *__stream) __attribute__ ((__nothrow__ , __leaf__)) ;

extern void funlockfile (FILE *__stream) __attribute__ ((__nothrow__ , __leaf__));
# 868 "/usr/include/stdio.h" 3 4

# 2 "test.c" 2

# 3 "test.c"
int main(){
 printf("hello world\n");
 return 0;
}

上面是预处理后test.i文件的部分内容,下面对test.i文件进行编译:

$ gcc -S test.i -o test.s

此时,test.s 就是 test.i 文件汇编后的产物,同样也可以用文本编译器打开查看。

3、汇编

汇编就是把编译阶段生成的".s"文件转成二进制目标代码,也就是机器代码(01序列)。

    .file   "test.c"
    .text
    .section    .rodata
.LC0:
    .string "hello world"
    .text
    .globl  main
    .type   main, @function
main:
.LFB0:
    .cfi_startproc
    pushq   %rbp
    .cfi_def_cfa_offset 16
    .cfi_offset 6, -16
    movq    %rsp, %rbp
    .cfi_def_cfa_register 6
    leaq    .LC0(%rip), %rdi
    call    puts@PLT
    movl    $0, %eax
    popq    %rbp
    .cfi_def_cfa 7, 8
    ret
    .cfi_endproc
.LFE0:
    .size   main, .-main
    .ident  "GCC: (Ubuntu 7.4.0-1ubuntu1~18.04.1) 7.4.0"
    .section    .note.GNU-stack,"",@progbits

上面是编译后生成的test.s文件里的汇编代码,下面对test.s文件进行汇编:

$ gcc -c test.s -o test.o

4、链接

链接就是将多个目标文件以及所需的库文件链接生成可执行目标文件的过程。

下面对test.o进行链接:

$ gcc test.o -o test
$ ./test
hello world!

5、简化

一般情况下,我们会使用gcc命令,一步生成可执行文件,简化编译流程:

$ gcc -o test test.c
$ ./test
hello world!

二、 静态库与动态库原理

1、 静态库

1) 什么是静态库

2) 生成静态库

# 生成目标文件
$ gcc -c test.c -o test.o
# 使用ar命令将目标文件打包成静态库
$ ar  libtest.a test.o
ar: creating libtest.a
# 使用ar t libtest.a 查看静态库内容
$ar t libtest.a
test.o

选项rcs各自的含义:

2、 动态库

1)什么是动态库

2)生成动态库

# 首先生成目标文件
$ gcc -c test.c -o test.o
# 使用-fPIC和-shared生成动态库
$ gcc -shared -fPIC -o libtest.so test.o

fPIC:全称是 Position Independent Code, 用于生成位置无关代码。

3、案例

编写一个工具方法(tool.h + tool.c文件),查找出数组的最大值:

// tool.h 文件
int find_max(int arr[], int n);

// tool.c 文件
#include "tool.h"
int find_max(int arr[], int n){
    int max = arr[0];
    int i;
    for(i = 0; i < n; i++){
        if(arr[i] > max){
            max = arr[i];
        }
    }
    return max;
}

在main.c文件中,调用tool.h的find_max函数:

// main.c 文件
#include <stdio.h>
#include "tool.h"

int main(){
    int arr[] = {1,3,5,8,2};
    int max = find_max(arr, 5);
    printf("max = %d\n", max);
    return 0;
}

1)编译&使用静态库

编译tool静态库:

# 编译tool.c。可以省略"-o tool.o",默认gcc会生成一个与tool.c同名的.o文件。
$ gcc -c tool.c

# 编译生成libtool.a静态库
$ ar rcs libtool.a tool.o

# 编译main可执行文件。
# -l用来指定要链接的库,后面接库的名字;-L表示编译程序根据指定路径寻找库文件。
$ gcc -o main main.c -L. -ltool

$ ./main
max = 8

可以用ldd命令查看main文件依赖了哪些库:

$ ldd main

2)编译&使用动态库

# 编译tool.c,生成tool.o
$ gcc -c tool.c

# 编译生成libtool.so动态库
$ gcc -shared -fPIC -o libtool.so tool.o

# 编译main可执行文件
$ gcc -o main main.c -L. -ltool

$ ./main
./main: error while loading shared libraries: libtool.so: cannot open shared object file: No such file or directory

注意,当静态库与动态库同名时,gcc会优先加载动态库。即,此时目录下即有libtool.a,又有libtool.so,编译main时指定了-ltool,gcc会链接libtool.so!

可以用ldd命令查看main文件依赖了哪些库:

$ ldd main

可以看到,libtool.so找不到,这是因为在系统的默认动态链接库路径下没有这个libtool.so文件,可以在执行之前,给main设置环境变量解决:

# 将当前目录设置到环境变量中
$ LD_LIBRARY_PATH=. ./main
max = 8

LD_LIBRARY_PATH 指定查找共享库,即动态链接库时,除默认路径以外,其他的路径。

4、区别总结

载入时刻不同:

三、makefile走读与语法基础

1、makefile是什么

在一个工程中,源文件很多,按类型、功能、模块分别被存放在若干个目录中,需要按一定的顺序、规则进行编译,这时就需要使用到makefile。

makefile是make工具的配置脚本,默认情况下,make命令会在当前目录下去寻找该文件(按顺序找寻文件名为“GNUmakefile”“makefile”“Makefile”的文件)。

在这三个文件名中,最好使用“Makefile”这个文件名,因为,这个文件名第一个字符为大写,这样有一种显目的感觉。
最好不要用“GNUmakefile”,这个文件是GNU的make识别的。有另外一些make只对全小写的“makefile”文件名敏感。
但是基本上来说,大多数的make都支持“makefile”和“Makefile”这两种默认文件名。

当然,配置文件的文件名也可以不是makefile,比如:config.debug,这时需要通过 -f--file 指定配置文件,即:

# 使用-f
$ make -f config.debug
# 使用--file
$ make --file config.debug

2、makefile里有什么

makefile包含以下五个:

3、makefile的规则

target ... : prerequisites ...
    command
或者:
target ... : prerequisites ... ; command

若prerequisites与command在同一行,需要用;分隔。
若prerequisites与command不在同一行,则command前面需要用tab键开头。
另外,如果命令太长,可以用\作为换行符。

makefile的作用:

告诉make,文件的依赖关系,以及如何生成目标文件。prerequisites中,如果有一个及以上的文件比target要新的话,target就会被认为是过时的,需要重新生成,command就会被执行,从而生成新的target。

4、makefile示例

# 当前目录存在main.c、tool.c、tool.h三个文件
# 下面是makefile文件内容
main: main.o tool.o
    gcc main.o tool.o -o main
.PHONY: clean
clean:
    -rm main *.o
-----------------------------
// 执行 make 后输出如下:
cc  -c -o main.o main.c
cc  -c -o tool.o tool.c
gcc main.o tool.o -o main
// 并且生成了一个可执行文件main

make会自动推导main.o、tool.o如何生成。
伪目标的名字不能和文件名重复,即当前目录下,不能有clean文件。
可以通过 make clean 执行删除命令。

5、makefile如何工作

默认方式下,输入make命令后:

6、makefile中使用变量

objects = main.o tool.o
main: $(objects)
    gcc $(objects) -o main
.PHONY: clean
clean:
    -rm main $(objects)
-----------------------------
// 执行 make 后输出如下:
cc  -c -o main.o main.c
cc  -c -o tool.o tool.c
gcc main.o tool.o -o main

7、makefile中引用其他的makefile

# 语法格式
include <filename>

# 举个例子,你有这样几个 Makefile:a.mk、b.mk、c.mk,还有一个文件叫 # foo.make,以及一个变量$(bar),其包含了 e.mk 和 f.mk

include foo.make *.mk $(bar)
# 等价于:
include foo.make a.mk b.mk c.mk e.mk f.mk

# 如果文件找不到,而你希望make时不理会那些无法读取的文件而继续执行
# 可以在include前加一个减号“-”,如:
-include <filename>

使用include关键字可以把其它Makefile包含进来,include语法格式:
include <filename>

8、环境变量MAKEFILES

MAKEFILES

如果当前环境中字义了环境变量 MAKEFILES,那么,make会把这个变量中的值做一个类似于 include 的动作。这个变量中的值是其它的 Makefile,用空格分隔。只是,它和include不同的是,从这个环境中引入的Makefile的“目标”不会起作用,如果环境变量中定义的文件发现错误,make也会不理。但是建议不要使用这个环境变量,因为只要这个变量一被定义,那么当你使用make时,所有的Makefile都会受到它的影响。

也许有时候Makefile出现了奇怪的事,那么可以查看当前环境中有没有定义这个变量。

9、Makefile预定义变量

变量名 描述 默认值
CC C语言编译器的名称 cc
CPP C语言预处理器的名称 $(CC) -E
CXX C++语言编译器的名称 g++
RM 删除文件程序的名称 rm -f
CFLAGS C语言编译器的编译选项
CPPFLAGS C语言预处理器的编译选项
CXXFLAGS C++语言编译器的编译选项

10、Makefile自动变量

自动变量 描述
$* 目标文件的名称,不包含扩展名
$@ 目标文件的名称,包含扩展名
$+ 所有的依赖文件,以空格隔开,可能含有重复的文件
$^ 所有的依赖文件,以空格隔开,不重复
$< 依赖项中第一个依赖文件的名称
$? 依赖项中所有比目标文件新的依赖文件

11、Makefile函数

define本质是定义一个多行的变量,没办法直接调用,但可以在call的作用下,当作函数来使用。

不带参数

define FUNC
$(info echo "hello")
endef

$(call FUNC)
--------------------
输出:hello

带参数

define FUNC1
$(info echo $(1)$(2))
endef

$(call FUNC1,hello,world)
--------------------
输出:hello world

12、make的工作流程

GNU的make工作时的执行步骤如下:

  1. 读入所有的Makefile。
  2. 读入被include的其它Makefile。
  3. 初始化文件中的变量。
  4. 推导隐晦规则,并分析所有规则。
  5. 为所有的目标文件创建依赖关系链。
  6. 根据依赖关系,决定哪些目标要重新生成。
  7. 执行生成命令。

15是第一阶段,67为第二阶段。在第一阶段中,如果定义的变量被使用了,那么make会把变量展开在使用的位置,但是make并不是完全的马上展开,如果变量出现在依赖关系的规则中,那么只有当这条依赖被决定要使用的时候,变量才会被展开。

三、Android.mk基础

1、Android.mk简介

Android.mk是一个向Android NDK构建系统描述NDK项目的GNU makefile片段。主要用来编译生成以下几种:

2、Android.mk基本格式

这是一个简单的Android.mk文件的内容:

# 定义模块当前路径(必须定义在文件开头,只需定义一次)
LOCAL_PATH := $(call my-dir)

# 清空当前环境变量(LOCAL_PATH除外)
include $(CLEAR_VARS)

# 当前模块名(这里会生成libhello-jni.so)
LOCAL_MODULE := hello-jni

# 当前模块包含的源代码文件
LOCAL_SRC_FILES := hello-jni.c

# 表示当前模块将被编译成一个共享库
include $(BUILD_SHARED_LIBRARY)

3、编译多个共享库

一个Android.mk可能编译产生多个共享库模块。

LOCAL_PATH := $(call my-dir)

# 模块1
include $(CLEAR_VARS)
LOCAL_MODULE := module1
LOCAL_SRC_FILES := module1.c
include $(BUILD_SHARED_LIBRARY)

# 模块2
include $(CLEAR_VARS)
LOCAL_MODULE := module2
LOCAL_SRC_FILES := module2.c
include $(BUILD_SHARED_LIBRARY)

这里会产生libmodule1.so和libmodule2.so两个动态库。

4、编译静态库

虽然Android应用程序不能直接使用静态库,但静态库可以用来编译动态库。比如在将第三方代码添加到原生项目中时,可以不用直接将第三方源码包括在原生项目中,而是将第三方源码编译成静态库,然后并入共享库。

LOCAL_PATH := $(call my-dir)

# 第三方AVI库
include $(CLEAR_VARS)
LOCAL_MODULE := avilib
LOCAL_SRC_FILES := avilib.c platform_posix.c
include $(BUILD_STATIC_LIBRARY)

# 原生模块
include $(CLEAR_VARS)
LOCAL_MODULE := module
LOCAL_SRC_FILES := module.c
# 将静态库模块名添加到LOCAL_STATIC_LIBRARIES变量
LOCAL_STATIC_LIBRARIES := avilib
include $(BUILD_SHARED_LIBRARY)

5、使用共享库共享通用模块

静态库可以保证源代码模块化,但是当静态库与共享库相连时,它就变成了共享库的一部分。在多个共享库的情况下,多个共享库与静态库连接时,需要将通用模块的多个副本与不同的共享库重复相连,这样就增加了APP的大小。这种情况,可以将通用模块作为共享库。

LOCAL_PATH := $(call my-dir)

# 第三方AVI库
include $(CLEAR_VARS)
LOCAL_MODULE := avilib
LOCAL_SRC_FILES := avilib.c platform_posix.c
include $(BUILD_SHARED_LIBRARY)

# 原生模块1
include $(CLEAR_VARS)
LOCAL_MODULE := module1
LOCAL_SRC_FILES := module1.c
LOCAL_SHARED_LIBRARIES := avilib
include $(BUILD_SHARED_LIBRARY)

# 原生模块2
include $(CLEAR_VARS)
LOCAL_MODULE := module2
LOCAL_SRC_FILES := module2.c
LOCAL_SHARED_LIBRARIES := avilib
include $(BUILD_SHARED_LIBRARY)

以上的做法必须基于同一个NDK项目。

6、在多个NDK项目间共享模块

import-module函数宏在NDK版本r5以后才有。

# avilib模块自己的Android.mk文件
LOCAL_PATH := $(call my-dir)
include $(CLEAR_VARS)
LOCAL_MODULE := avilib
LOCAL_SRC_FILES := avilib.c platform_posix.c
include $(BUILD_SHARED_LIBRARY)
---------------------------------------------
# 使用共享模块的NDK项目1的Android.mk文件
LOCAL_PATH := $(call my-dir)
include $(CLEAR_VARS)
LOCAL_MODULE := module1
LOCAL_SRC_FILES := module1.c
LOCAL_SHARED_LIBRARIES := avilib
include $(BUILD_SHARED_LIBRARY)
$(call import-module,transcode/avilib)
---------------------------------------------
# 使用共享模块的NDK项目2的Android.mk文件
LOCAL_PATH := $(call my-dir)
include $(CLEAR_VARS)
LOCAL_MODULE := module2
LOCAL_SRC_FILES := module2.c
LOCAL_SHARED_LIBRARIES := avilib
include $(BUILD_SHARED_LIBRARY)
$(call import-module,transcode/avilib)

当心细的你在看到$(call import-module,transcode/avilib)这句时,一定会问,为什么NDK会知道要去C:\android\shared-modules\目录下面找transcode/avilib呢?是的,NDK并没有这么智能,默认情况下,import-module函数宏只会搜索AndroidNDK下面的sources目录。

如我的NDK路径是:C:\Users\lqr\AppData\Local\Android\Sdk\ndk-bundle,那么import-module函数宏默认的寻找目录就是C:\Users\lqr\AppData\Local\Android\Sdk\ndk-bundle\sources

要正确使用import-module,就需要对NDK_MODULE_PATH进行配置,把C:\android\shared-modules\配置到环境变量中即可,当有多个共享库目录时,用;隔开。

更多关于import-module的介绍,请翻到文末查看。

7、使用预编译库

现在我们手上有第三方预编译好的库libavilib.so,想集成到自己项目中使用,则需要在Android.mk中进行如下配置:

# 预编译共享模块的Android.mk文件
LOCAL_PATH := $(call my-dir)
# 第三方预编译的库
include $(CLEAR_VARS)
LOCAL_MODULE := avilib
LOCAL_SRC_FILES := libavilib.so
include $(PREBUILT_SHARED_LIBRARY)

可以看到,LOCAL_SRC_FILES指向的不再是源文件,而是预编译好的libavilib.so,相对于LOCAL_PATH的位置。

8、编译独立的可执行文件

为了方便测试和进行快速开发,可以编译成可执行文件。不用打包成APK就可以得到到Android设备上直接执行。

# 独立可执行模块的Android.mk文件
LOCAL_PATH := $(call my-dir)
include $(CLEAR_VARS)
LOCAL_MODULE := module
LOCAL_SRC_FILES := module.c
LOCAL_STATIC_LIBRARIES := avilib
include $(BUILD_EXECUTABLE)

9、注意事项

假如我们本地库libhello-jni.so依赖于libTest.so(可以使用NDK下的ndk-depends查看so的依赖关系)。

// Android 6.0版本之前:
System.loadlibrary("Test");
System.loadlibrary("hello-jni");

// Android 6.0版本之后:  
System.loadlibrary("hello-jni");

四、附加

1、import_module 详解

以下内容引用自 《import-module的注意事项与NDK_MODULE_PATH的配置》

欢迎关注微信公众号:全栈行动
上一篇下一篇

猜你喜欢

热点阅读