ndk-build 详细使用

4,208 阅读6分钟

ndk-build 脚本可用于编译采用 NDK 基于 Make 的编译系统的项目。提供两个脚本 Android.mk 和 Application.mk。虽然现在的 Android 项目都采用 CMake 来构建,但是很多老项目都采用 ndk-build 的方式。因此相当有必要了解和使用 ndk-build。后面会有两个例子,一个是基础的使用方式,另外一个是在 Android 中使用 lua。

首先分别介绍一下 Application.mk 和 Android.mk 常用的配置选项。

Application.mk

Application.mk 指定了 ndk-build 的项目范围设置。下面是项目常用到的变量表。

  1. APP_ABI
APP_ABI  编译代码输出的 ABI 列表, 可以指指定的值:
armeabi-v7a, 
arm64-v8a, 
x86_64, 
x86, 
mips(在 ndk r17 移除), 
mips64(ndk r17 移除),
all (当前 ndk 支持的所有 abi 列表)

值得注意的是:中间用过空格隔开,列入 armeabi-v7a x86,例如:

APP_ABI := armeabi-v7a arm64-v8a x86_x64 x86

  1. APP_MODULES
APP_MODULES: 编译模块列表, 指定 Android.mk 中模块的名称,控制那些模块需要被编译。

APP_MODULES := hello

  1. APP_OPTIM
APP_OPTIM  可选命令,如果没有设置,会根据应用是否属于调试模式而自动设置 debug 或 release。
  1. APP_CFLAGS
APP_CFLAGS 指定 (C/C++) 优化代码的参数,用于指定头文件路径便于编译器
包含文件(例如:APP_CFLAGS := -I$(LOCAL_PATH)/include)。

一般与优化相关的就是指定 Ox,其中 x 的优化级别为 0,1,2(或者-Os)
  1. APP_CPPFLAGS
APP_CPPFLAGS 和 APP_CFLAGS 类似,只指定 c++ 编译器的优化参数

  1. APP_LDFLAGS
APP_LDFLAGS 关联可执行文件和共享库时指定,例如可执行文件中依赖其他动态库,对静态库没有影响
  1. APP_PLATFORM
APP_PLATFORM 声明编译此应用所面向的 Android API 级别,对应于应用的 minSdkVersion。
如果未指定,ndk-build 将以 NDK 支持的最低 API 级别为目标。

需要注意的是,你当前的 ndk 可能不一定支持你所设置的平台,你可以参考你使用的 ndk 
版本 platform 下是否包含指定的平台。例如我这里是 ndk r20 最低为 android-16
  1. APP_STL
用于此应用的 C++ 标准库。默认情况下使用 system STL。
其他选项包括 c++_shared、c++_static 和 none。

值得注意的是: 
1. 系统运行时指的是 /system/lib/libstdc++.so。请勿将该库与 GNU 的全功能
libstdc++ 混淆。在 Android 系统中,libstdc++ 只是 new 和 delete。对于全功能 C++
标准库,请使用 libc++,libc++ 的共享库为 libc++_shared.so,静态库为 libc++_static.a。

2. 使用静态运行时(以及一般静态库)要特别小心,例如:

# Application.mk
    APP_STL := c++_static // 指定 c++_static 静态运行库
    
# Android.mk
    include $(CLEAR_VARS)
    LOCAL_MODULE := foo
    LOCAL_SRC_FILES := foo.cpp
    include $(BUILD_SHARED_LIBRARY) // 动态库 foo 

    include $(CLEAR_VARS)
    LOCAL_MODULE := bar
    LOCAL_SRC_FILES := bar.cpp
    LOCAL_SHARED_LIBRARIES := foo
    include $(BUILD_SHARED_LIBRARY) // 动态库 bar 依赖 foo
    
    
它会导致两个 foo 和 bar 都依赖 c++_static, 增加了应用体积不说,还会有如下风险:
. 内存在一个库中分配,而在另一个库中释放,从而导致内存泄漏或堆损坏。
. libfoo.so 中引发的异常在 libbar.so 中未被捕获,从而导致应用崩溃。
. std::cout 的缓冲未正常运行。
  1. APP_BUILD_SCRIPT
APP_BUILD_SCRIPT 要从其他位置加载 Android.mk 文件,请将 APP_BUILD_SCRIPT 设置为
Android.mk 文件的绝对路径。

Android.mk

Android.mk 文件位于项目 jni/ 目录的子目录中。Application.mk 是对于整个应用共同的配置,而 Android.mk 指定构建的模块如何构建。

  1. LOCAL_PATH
LOCAL_PATH Android.mk 路径,设置为 $(call my-dir)

my-dir 是一个宏函数,用来返回当前 Android.mk 的路径,例如:

LOCAL_PATH := $(call my-dir)

  1. CLEAR_VARS
CLEAR_VARS CLEAR_VARS 变量指向一个特殊的 GNU Makefile,后者会清除许多 LOCAL_XXX
变量,例如 LOCAL_MODULE、LOCAL_SRC_FILES 和 LOCAL_STATIC_LIBRARIES。请注意,GNU
Makefile 不会清除 LOCAL_PATH。

include $(CLEAR_VARS)
  1. LOCAL_MODULE
LOCAL_MODULE 模块名称

值得注意的是:每个模块名称必须唯一,且不含任何空格。编译系统在生成最终共享库文件时
,会对您分配给 LOCAL_MODULE的名称自动添加正确的前缀和后缀。
例如

LOCAL_MODULE := hello-jni

上述示例会生成名为 libhello-jni.so 的库。
  1. LOCAL_SRC_FILES
LOCAL_SRC_FILES  C 和/或 C++ 源文件列表

例如:

LOCAL_SRC_FILES := a.c b.c 

记住文件列表中间用空格隔开
  1. BUILD_SHARED_LIBRARY
BUILD_SHARED_LIBRARY 将所有内容连接到一起,BUILD_SHARED_LIBRARY 变量指向一个 GNU Makefile 脚本,
该脚本会收集您自最近 include 以来在 LOCAL_XXX变量中定义的所有信息。此脚本确定要编译的内容以及编译方式。

最终会生成一个共享库。 

include $(BUILD_SHARED_LIBRARY)
  1. BUILD_STATIC_LIBRARY
BUILD_STATIC_LIBRARY 生成的是一个静态库,可以用来链接到其他共享库中。

include $(BUILD_STATIC_LIBRARY)
  1. PREBUILT_SHARED_LIBRARY
PREBUILT_SHARED_LIBRARY 指向用于指定预编译共享库的编译脚本。与 BUILD_SHARED_LIBRARY 和
BUILD_STATIC_LIBRARY 的情况不同,这里的 LOCAL_SRC_FILES 值不能是源文件,而必须是指向预编译共享库
的一个路径,例如 foo/libfoo.so。使用此变量的语法为:

include $(PREBUILT_SHARED_LIBRARY)

  1. PREBUILT_STATIC_LIBRARY
PREBUILT_STATIC_LIBRARY 跟 PREBUILT_SHARED_LIBRARY 类似

  1. LOCAL_C_INCLUDES
LOCAL_C_INCLUDES  追加到 include 搜索列表路径,用法如下:

LOCAL_C_INCLUDES := sources/foo

  1. LOCAL_CFLAGS
LOCAL_CFLAGS 这个在前面 Application.mk 中已经讲过,传递的一组 (C/C++)可选编译器标记。 但是如果你只是
指定头文件,最好使用 LOCAL_C_INCLUDES。

LOCAL_CFLAGS := -I$(LOCAL_PATH)/myinclude

  1. LOCAL_CPPFLAGS
LOCAL_CPPFLAGS 和 LOCAL_CFLAGS 类似,不过只针对 C++ 的可选编译器标记。
  1. LOCAL_LDLIBS
此变量列出了在编译共享库或可执行文件时使用的额外链接器标记。利用此变量,
您可使用 -l 前缀传递特定系统库的名称。
例如,以下示例指示链接器生成在加载时链接到 /system/lib/libz.so 的模块: 

LOCAL_LDLIBS := -lz
  1. LOCAL_LDFLAGS
此变量列出了编译系统在编译共享库或可执行文件时使用的其他链接器标记。例如,要在 ARM/X86 上使用 ld.bfd 链接器:

LOCAL_LDFLAGS += -fuse-ld=bfd

值得注意的是:如果为静态库定义此变量,编译系统会忽略此变量,并且 ndk-build 会显示一则警告。
  1. LOCAL_SHARED_LIBRARIES
LOCAL_SHARED_LIBRARIES: 当前模块依赖的共享库列表
  1. LOCAL_STATIC_LIBRARIES
LOCAL_STATIC_LIBRARIES: 存储当前模块依赖的静态库模块列表

简单的例子

上面讲一些常用的配置选项介绍完毕,接下来通过一个列子来介绍怎样使用。首先我们常见一个工程名称为 HellJni,如果你的 Android Studio 版本大于 2.2的话,可以通过创建 Native C++ 工程。这样就可以默认为你生成 CMake 构建的工程。但是这里我们手动来配置。

可以看到,这是一个没有配置 Android.mk 和 Application, 甚至也没有 JNI 本地 C/C++ 代码的工程。接下来就一步步来配置。

  1. 在 MainActivity 创建一个本地方法 messageFromJNI()

  1. 生成本地代码,在 app/src/main 目录下创建 jni 目录,并编写一个头文件 hello_jni.h

  1. 生成 messageFromJNI JNI 头文件声明,关于怎么生成头文件,请参考 NDK 开发入门。我这里就手动编写,而不借助 javac 或 javah (jdk10 移除)。

  1. 编写 Java_com_hxj_hellojni_messageFromJNI 函数的实现,新建一个 C 源文件 hello_jni.c

  1. 经过上面几个步骤,实现了 JNI 本地函数,现在就是交给 ndk 编译工具链去生成动态库 .so, 也就是我们上面描述的如何配置 Android.mk 和 可选的 Application.mk 文件。请记住脚本文件一定要在 jni 目录下。

先来看 Application.mk,里面很简单只指定了 APP_ABI := all

Android.mk 也是非常简单,它包括了一个模块最简单的构建方式。

  1. 接下来就是要在 MainActivity 中使用了,这一步很多人可能粗心大意忘记加载库导致 UnsatisfiedLinkError。

需要在静态代码块中加载库。

  1. 点击运行项目,查看打印日志

Android 中使用 Lua

上述只是一个非常简单的例子,但是它确实以及将 Java 世界和 JNI 世界联系起来了。我们在一些常见下需要 C/C++ 库来完成。因此掌握 JNI 开发是重要的技能。接下来看一个较为复杂的列子,曾经笔者在自己项目中需要用到 lua, 因为终端上运行的是 lua 解释器,那么 app 断就需要生成 lua 脚本然后交给终端去执行。

起初也是查找了很多 lua 相关的资料,编译 lua 源码到工程不难,难的是 Java 和 Lua 之间的桥梁的实现,好在也有这样的开源项目可以利用, 详情请参考 AndroidLua, 不过这个项目已经很久没有更新了,我还找到了比较近一些时间的项目 Android-Lua

知道了大概的实现方法,那么首先我们就去下载一下 Lua 源码,这里下载 lua5.3.5 版本,连同 luajava 拷进工程。项目结构图如下:

然后更改 Android.mk 脚本,其实比起简单的工程只是多了如何将指定目录下所有源文件加入到编译系统,以及头文件引入,还有就是系统库的使用,比如安卓日志库 log。

LOCAL_PATH := $(call my-dir)

include $(CLEAR_VARS)

# 模块名称 luajava
LOCAL_MODULE := luajava

# 导入头文件到编译系统
LOCAL_C_INCLUDES := $(LOCAL_PATH)/lua $(LOCAL_PATH)/luajava

# 定义查找源文件的目录
MY_FILES_PATH  :=  $(LOCAL_PATH)/lua $(LOCAL_PATH)/luajava
# 过滤规则
MY_FILES_SUFFIX := %.cpp %.c %.cc

# 查找源文件.
My_All_Files := $(foreach src_path,$(MY_FILES_PATH), $(shell find "$(src_path)" -type f) )
My_All_Files := $(My_All_Files:$(MY_CPP_PATH)/./%=$(MY_CPP_PATH)%)
MY_SRC_LIST  := $(filter $(MY_FILES_SUFFIX),$(My_All_Files))
MY_SRC_LIST  := $(MY_SRC_LIST:$(LOCAL_PATH)/%=%)
LOCAL_SRC_FILES := $(MY_SRC_LIST)
#打印编译信息
#$(warning 'src_list='$(LOCAL_SRC_FILES))

LOCAL_LDLIBS := -llog

# 生成动态库
include $(BUILD_SHARED_LIBRARY)

执行编译的时候还是有问题的,这是因为在新的 lua 5 以后版本移除了一些 api。笔者暂时也没有去研究这些 api 被什么取代了,有时间了我会自己去维护一个 luajava 版本。笔者为了使用现有的 luajava 版本,暂时将出现错误的方法如下处理,当然肯定是不能这样的,因为本文并非深入的去研究 lua 的使用。

jint  jni_equal
  (JNIEnv * env , jobject jobj , jobject cptr , jint idx1 , jint idx2)
{
   lua_State * L = getStateFromCPtr( env , cptr );

    // 新版本中没有 lua_equal,为了编译通过,直接返回了。
   //return ( jint ) lua_equal( L , idx1 , idx2 );
   return 1;
}

..... 报错的做类似处理

好啦!到这儿就介绍完毕,以后大家遇到项目中使用 ndk-build 的方式能有一些参考。对于新项目还是强烈建议使用 CMake 构建工具。

源码获取 点击