
Android.mk 文件是 Android 平台下用于定义如何编译和链接 Android 应用或库中的本地代码(如 C/C++)的 Makefile 脚本。虽然随着 Android Studio 和 CMake 的普及,CMake 逐渐成为更受欢迎的选择,但 Android.mk 仍然在某些老项目或特定需求下被使用。本篇将通过实战的方式,详细介绍如何编写一个用于构建本地库的Android.mk文件,并将其集成到Android项目中。
假设我们的目标是创建一个名为libnative-utils的本地库,该库包含一些通用的C/C++函数,这些函数将被Android应用的Java代码通过JNI(Java Native Interface)调用。需要遵循以下步骤。这里假设已经有一个基本的 Android 项目结构,并且计划在该项目中添加本地代码。
首先,需要在 Android 项目中创建一个新的目录来存放 C/C++ 源代码。通常,这个目录被命名为 jni 或 cpp(取决于具体项目设置)。在这个例子中,我们使用 jni 目录。在Android项目中创建本地代码文件的具体步骤如下,这里我们专注于使用jni目录来存放C/C++源代码,并创建一个名为native-utils.cpp的源文件。
src/, libs/, gradle/等目录同级的目录。jni目录,则可以跳过这一步。jni。确保这个文件夹位于与src/main/同级的位置。jni目录。jni目录中,创建一个新的文本文件,并将其命名为native-utils.cpp。可以使用任何文本编辑器来完成这一步,但如果正在使用Android Studio,它提供了内置的文件创建和编辑功能。native-utils.cpp在 native-utils.cpp 文件中,可以编写一些简单的 C/C++ 函数,这些函数将通过 JNI 被 Java 代码调用。例如:
在 native-utils.cpp 文件中,可以编写一系列 C/C++ 函数,这些函数通过 JNI (Java Native Interface) 暴露给 Java 代码。JNI 允许 Java 代码运行时调用本地方法(即用 C 或 C++ 编写的代码)。下面是一个简单的例子,展示了如何在 native-utils.cpp 中定义这样的函数。
首先,确保项目中已经正确设置了 JNI 环境,包括在 jni 目录下创建 native-utils.cpp 文件,并在 Android 项目的适当位置(如 src/main/java)有对应的 Java 类来调用这些本地方法。
#include <jni.h>
#include <string>
// 假设 Java 类名为 com.example.myapp.MainActivity
// 并且希望调用的本地方法名为 stringFromJNI
extern "C" {
JNIEXPORT jstring JNICALL
Java_com_example_myapp_MainActivity_stringFromJNI(JNIEnv *env, jobject /* this */) {
// 创建一个 std::string 对象
std::string hello = "Hello from C++";
// 将 std::string 转换为 JNI 字符串(jstring)
return env->NewStringUTF(hello.c_str());
}
// 可以继续添加更多的本地方法
JNIEXPORT jint JNICALL
Java_com_example_myapp_MainActivity_addNumbers(JNIEnv *env, jobject /* this */, jint a, jint b) {
return a + b;
}
// 注意:每个 JNI 方法都需要有 extern "C" 声明,
// 因为 JNI 查找函数时不支持 C++ 的函数重载和名称修饰。
} // extern "C"extern "C" 来告诉编译器这部分代码应该按照 C 语言的链接约定来处理,即不进行名称修饰。
Java_ 后跟完全限定的 Java 类名(将 . 替换为 _),然后是方法名。例如,如果 Java 类名为 com.example.myapp.MainActivity,并且希望调用的本地方法名为 stringFromJNI,则 JNI 函数的名称应该是 Java_com_example_myapp_MainActivity_stringFromJNI。
JNIEnv*, jobject, jstring, jint 等)。
System.loadLibrary("native-utils");(假设本地库名为 libnative-utils.so)来加载包含这些本地方法的库。确保库文件已经正确构建并放置在应用的适当位置(通常是 libs/<ABI>/ 目录下,其中 <ABI> 是目标设备的 ABI,如 armeabi-v7a、arm64-v8a 等)。
native-utils.h的头文件,并在其中声明你的JNI函数。然后,在native-utils.cpp文件中包含这个头文件。在Android项目中,如果打算使用ndk-build来编译C/C++代码,需要在项目的jni目录下编写一个Android.mk文件。这个文件是Android NDK(Native Development Kit)构建系统的一部分,用于告诉构建系统如何编译和链接本地代码。
下面是一个简单的Android.mk文件的例子,它定义了一个名为libnative-utils的共享库,该库包含了之前创建的native-utils.cpp文件:
LOCAL_PATH := $(call my-dir)
include $(CLEAR_VARS)
# 定义本地模块的名称
LOCAL_MODULE := libnative-utils
# 定义C/C++源文件列表
LOCAL_SRC_FILES := native-utils.cpp
# 定义任何需要的库
# 例如,如果你的代码依赖于log库,你可以取消注释下一行
# LOCAL_LDLIBS := -llog
# 包含构建共享库的规则
include $(BUILD_SHARED_LIBRARY)include $(CLEAR_VARS): 这行调用了NDK提供的CLEAR_VARS变量,它会清除之前设置的所有LOCAL_变量(例如LOCAL_MODULE,LOCAL_SRC_FILES等),以便可以为每个模块设置一组新的值。
LOCAL_MODULE := libnative-utils: 这行定义了想要构建的本地模块的名称。这个名字是唯一的,并且会被用作生成的动态库文件的前缀(例如,libnative-utils.so)。
LOCAL_SRC_FILES := native-utils.cpp: 这行指定了构建此模块时需要编译的C/C++源文件列表。可以列出多个源文件,它们之间用空格分隔。
LOCAL_LDLIBS := -llog: 这行是可选的,它指定了在链接模块时需要链接的库。在这个例子中,-llog表示链接Android的日志库,这样就可以在C/C++代码中使用Android的日志功能了。如果不需要链接任何额外的库,可以省略这行。
include $(BUILD_SHARED_LIBRARY): 最后,这行告诉NDK构建系统想要构建的是一个共享库。NDK提供了几种不同的构建类型(如BUILD_STATIC_LIBRARY表示静态库,BUILD_EXECUTABLE表示可执行文件),但在这个例子中我们使用的是BUILD_SHARED_LIBRARY。
对于使用Android Studio并基于Gradle的Android项目,如果打算使用Android.mk文件和ndk-build来编译本地代码(C/C++),需要确保NDK已经配置在项目中,并且Gradle脚本(如build.gradle)被正确设置以包含JNI目录和NDK的构建规则。
然而,从Android Gradle Plugin 3.0开始,Google推荐使用CMake或ndk-build的集成方式,但更推荐CMake,因为它提供了更好的跨平台支持和Gradle的集成。不过,如果仍想使用Android.mk,可以通过一些额外的步骤来实现。
首先,确保已经在Android Studio中安装了NDK。可以通过Android Studio的SDK Manager来安装NDK。
local.properties在项目的根目录下,可以修改或创建local.properties文件来指定NDK的路径(尽管Android Gradle Plugin通常会自动找到它)。
ndk.dir=/path/to/ndk-bundlebuild.gradle在模块级build.gradle文件中(通常是app/build.gradle),需要添加一个外部native构建系统的配置。但是,对于直接使用Android.mk,Gradle插件没有直接的支持,因此可能需要使用exec任务在构建过程中调用ndk-build。
不过,更常见的做法是使用cmake或配置一个自定义的Gradle任务来调用ndk-build。以下是一个使用自定义Gradle任务调用ndk-build的示例:
android {
...
externalNativeBuild {
// 这里原本是为CMake或ndk-build的集成配置
// 但由于我们直接使用ndk-build,我们将添加一个自定义任务
ndkBuild {
// 如果使用ndk-build的集成(虽然这里不直接使用),则进行配置
// 但我们不会这样做,而是添加一个自定义任务
}
}
// 添加一个自定义的Gradle任务来调用ndk-build
tasks.register('ndkBuildTask', Exec) {
commandLine 'ndk-build', '-C', file('src/main/jni').absolutePath
}
// 将自定义任务添加到构建过程中(例如,在assembleDebug之前)
tasks.named('preBuild').configure {
dependsOn 'ndkBuildTask'
}
}
// 注意:上面的代码只是一个示例,可能需要根据项目结构进行调整
// 而且,直接在preBuild中调用ndk-build可能不是最佳实践,因为它会在每次Gradle同步时都运行
// 你可能想要更精细地控制何时调用ndk-buildndk-build可能不是最高效或最可靠的方法,因为它绕过了Gradle的缓存和增量构建机制。Android.mk,并且想要更紧密地集成到Gradle构建系统中,可能需要考虑编写一个自定义的Gradle插件或使用现有的插件(如果有的话)。如果发现直接使用ndk-build和Android.mk在Android Studio中过于复杂,可以考虑将本地代码迁移到CMake,这样就可以利用Android Gradle Plugin提供的更好集成和更简单的配置。
要构建本地库(native library),特别是针对Android平台的,使用NDK(Native Development Kit)是一个常见的做法。
首先,确保已经安装了Android NDK。可以从Android的官方网站或Android Studio的SDK Manager中下载并安装NDK。
在构建之前,需要确保NDK_ROOT(或ANDROID_NDK_HOME,具体取决于系统和NDK版本)环境变量已设置,指向NDK安装目录。同时,NDK_PROJECT_PATH虽然不总是必需的(因为它通常指向项目目录,如果已经在项目根目录下,那么它默认就是当前目录),但如果需要在不同的项目之间切换,设置这个变量会很有用。
在Windows上,可以在命令行中这样设置环境变量(以命令行会话为单位):
set NDK_ROOT=C:\path\to\ndk
set PATH=%NDK_ROOT%;%PATH%在Linux或macOS上,可以在.bashrc或.bash_profile文件中添加:
export NDK_ROOT=/path/to/ndk
export PATH=$NDK_ROOT:$PATH打开命令行工具,并使用cd命令导航到项目根目录,即包含jni和Android.mk文件的目录。
在项目根目录下,运行以下命令来构建本地库:
ndk-build如果ndk-build命令不在PATH中,可能需要指定完整的路径,例如:
$NDK_ROOT/ndk-build构建过程会在libs/<ABI>/目录下生成本地库(如.so文件),其中<ABI>是目标架构(如armeabi-v7a、arm64-v8a等)。如果构建成功,应该在这些目录下看到.so文件。
Android.mk和Application.mk(如果有)文件中的路径和设置正确无误。NDK_MODULE_PATH环境变量是否已设置,以包含任何额外的库路径。在Android应用的Java代码中,使用System.loadLibrary("native-utils")来加载本地库。然后,可以像调用普通Java方法一样调用JNI方法。
public class MainActivity extends AppCompatActivity {
static {
System.loadLibrary("native-utils");
}
public native String stringFromNative();
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
String hello = stringFromNative();
Toast.makeText(this, hello, Toast.LENGTH_LONG).show();
}
}在Android应用中使用JNI和本地库时,有几个重要的注意事项需要牢记,以确保一切能够顺利工作。以下是一些关键的注意事项。
确保JNI方法签名与Java中声明的本地方法签名完全匹配。JNI方法签名包括方法名、参数类型和返回类型,并且这些类型需要转换为JNI可以理解的类型。如果签名不匹配,将导致运行时错误,通常是UnsatisfiedLinkError。
System.loadLibrary("name")时,Android会查找名为libname.so的库文件(注意前缀lib和后缀.so)。确保库文件名与Java中指定的名称相匹配。libs/<ABI>/目录下查找这些库文件。确保库文件已经放在正确的目录下,并且APK构建过程已经包含了这些文件。确保应用支持所有目标ABI。不同的设备可能使用不同的CPU架构,因此需要为这些架构提供相应的.so文件。可以使用NDK提供的工具(如ndk-build或CMake)来构建针对不同ABI的库。
build.gradle文件中配置CMake,指定CMakeLists.txt的位置和要构建的库。build.gradle中通过外部任务来调用ndk-build命令。UnsatisfiedLinkError或UnsupportedOperationException等异常。确保代码能够妥善处理这些异常,并给出有用的错误信息。在Android NDK开发中,Android.mk文件是关键配置文件,用于定义C/C++源码如何编译成动态库或静态库。实战中,需明确设置LOCAL_PATH指向源文件目录,通过CLEAR_VARS清除之前设置,定义LOCAL_MODULE为模块名,LOCAL_SRC_FILES列出源码文件。可选设置包括依赖库、编译器标志等。使用ndk-build命令在命令行编译,确保环境变量配置正确。对于大型项目,可考虑结合Gradle或CMake以提高构建效率和可维护性。Android.mk配置需细心,确保路径、名称无误,以顺利构建本地库。