Android.mk手把手实战

目录

一、实战目标

1.1. 创建本地代码文件

1.1.1. 导航到项目根目录

1.1.2. 创建jni目录

1.1.3. 创建C/C++源文件

1.2. 编写 native-utils.cpp

1.2.1. native-utils.cpp

 1.2.2. 注意事项

1.3. (可选)创建头文件

1.4. 编写Android.mk

1.4.1. Android.mk

1.4.2. 说明

1.5. 配置Android项目(如果使用Android Studio)

1.5.1. 步骤 1: 安装和配置NDK

1.5.2. 步骤 2: 配置local.properties

1.5.3. 步骤 3: 修改build.gradle

1.5.4. 注意

1.5.5. 替代方案

1.6. 构建本地库

1.6.1. 安装NDK

1.6.2. 设置环境变量

1.6.3. 导航到项目根目录

1.6.4. 运行ndk-build

1.6.5. 检查输出

1.6.6. 常见问题

1.7. 在Java代码中加载和使用库

二、注意事项

2.1. JNI方法签名匹配

2.2. 正确的库名和路径

2.3. ABI支持

2.4. 自动构建本地库

2.5. 调试和错误处理

2.6. 性能考虑

7. 安全性

三、总结


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 项目结构,并且计划在该项目中添加本地代码。

1.1. 创建本地代码文件

首先,需要在 Android 项目中创建一个新的目录来存放 C/C++ 源代码。通常,这个目录被命名为 jni 或 cpp(取决于具体项目设置)。在这个例子中,我们使用 jni 目录。在Android项目中创建本地代码文件的具体步骤如下,这里我们专注于使用jni目录来存放C/C++源代码,并创建一个名为native-utils.cpp的源文件。

1.1.1. 导航到项目根目录

  • 首先,确保已经打开了Android项目,并且可以通过文件管理器(如Android Studio的Project视图,或者操作系统的文件浏览器)访问到项目的根目录。项目的根目录是与src/libs/gradle/等目录同级的目录。

1.1.2. 创建jni目录

  • 如果项目中已经存在jni目录,则可以跳过这一步。
  • 如果不存在,需要在项目根目录下手动创建一个新的文件夹,并将其命名为jni。确保这个文件夹位于与src/main/同级的位置。

1.1.3. 创建C/C++源文件

  • 导航到刚刚创建的jni目录。
  • jni目录中,创建一个新的文本文件,并将其命名为native-utils.cpp。可以使用任何文本编辑器来完成这一步,但如果正在使用Android Studio,它提供了内置的文件创建和编辑功能。

1.2. 编写 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 类来调用这些本地方法。

1.2.1. native-utils.cpp

#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"

 1.2.2. 注意事项

  • extern "C": 由于 C++ 支持函数重载,编译器会为每个函数生成唯一的名称(称为名称修饰或名称改编)。然而,JNI 需要通过固定的名称来查找函数,因此需要使用 extern "C" 来告诉编译器这部分代码应该按照 C 语言的链接约定来处理,即不进行名称修饰。

  • JNI 函数命名规则: JNI 函数的命名遵循一定的规则,即 Java_ 后跟完全限定的 Java 类名(将 . 替换为 _),然后是方法名。例如,如果 Java 类名为 com.example.myapp.MainActivity,并且希望调用的本地方法名为 stringFromJNI,则 JNI 函数的名称应该是 Java_com_example_myapp_MainActivity_stringFromJNI

  • 参数和返回值: JNI 函数的参数和返回值类型需要与 Java 方法中的参数和返回值类型相匹配,并且需要使用 JNI 提供的类型(如 JNIEnv*jobjectjstringjint 等)。

  • 错误处理: 在实际的应用中,可能需要添加错误处理逻辑来处理 JNI 调用中可能出现的异常情况,例如内存分配失败等。

  • 加载本地库: 在 Java 代码中,需要使用 System.loadLibrary("native-utils");(假设本地库名为 libnative-utils.so)来加载包含这些本地方法的库。确保库文件已经正确构建并放置在应用的适当位置(通常是 libs/<ABI>/ 目录下,其中 <ABI> 是目标设备的 ABI,如 armeabi-v7aarm64-v8a 等)。

1.3. (可选)创建头文件

  • 虽然对于简单的项目来说可能不是必需的,但将函数声明放在一个头文件中通常是一个好习惯。可以创建一个名为native-utils.h的头文件,并在其中声明你的JNI函数。然后,在native-utils.cpp文件中包含这个头文件。

1.4. 编写Android.mk

在Android项目中,如果打算使用ndk-build来编译C/C++代码,需要在项目的jni目录下编写一个Android.mk文件。这个文件是Android NDK(Native Development Kit)构建系统的一部分,用于告诉构建系统如何编译和链接本地代码。

1.4.1. Android.mk

下面是一个简单的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)

1.4.2. 说明

  • LOCAL_PATH := $(call my-dir): 这行设置了一个变量LOCAL_PATH,它包含了当前Android.mk文件所在的目录的路径。$(call my-dir)是一个NDK提供的宏,用于获取当前文件的目录路径。

  • include $(CLEAR_VARS): 这行调用了NDK提供的CLEAR_VARS变量,它会清除之前设置的所有LOCAL_变量(例如LOCAL_MODULELOCAL_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

1.5. 配置Android项目(如果使用Android Studio)

对于使用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,可以通过一些额外的步骤来实现。

1.5.1. 步骤 1: 安装和配置NDK

首先,确保已经在Android Studio中安装了NDK。可以通过Android Studio的SDK Manager来安装NDK。

1.5.2. 步骤 2: 配置local.properties

在项目的根目录下,可以修改或创建local.properties文件来指定NDK的路径(尽管Android Gradle Plugin通常会自动找到它)。

ndk.dir=/path/to/ndk-bundle

1.5.3. 步骤 3: 修改build.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-build

1.5.4. 注意

  • 直接在Gradle构建脚本中调用ndk-build可能不是最高效或最可靠的方法,因为它绕过了Gradle的缓存和增量构建机制。
  • 更好的做法是使用CMake(如果可能的话),因为它提供了更好的Gradle集成和跨平台支持。
  • 如果确实需要使用Android.mk,并且想要更紧密地集成到Gradle构建系统中,可能需要考虑编写一个自定义的Gradle插件或使用现有的插件(如果有的话)。

1.5.5. 替代方案

如果发现直接使用ndk-buildAndroid.mk在Android Studio中过于复杂,可以考虑将本地代码迁移到CMake,这样就可以利用Android Gradle Plugin提供的更好集成和更简单的配置。

1.6. 构建本地库

要构建本地库(native library),特别是针对Android平台的,使用NDK(Native Development Kit)是一个常见的做法。

1.6.1. 安装NDK

首先,确保已经安装了Android NDK。可以从Android的官方网站或Android Studio的SDK Manager中下载并安装NDK。

1.6.2. 设置环境变量

在构建之前,需要确保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

1.6.3. 导航到项目根目录

打开命令行工具,并使用cd命令导航到项目根目录,即包含jniAndroid.mk文件的目录。

1.6.4. 运行ndk-build

在项目根目录下,运行以下命令来构建本地库:

ndk-build

如果ndk-build命令不在PATH中,可能需要指定完整的路径,例如:

$NDK_ROOT/ndk-build

1.6.5. 检查输出

构建过程会在libs/<ABI>/目录下生成本地库(如.so文件),其中<ABI>是目标架构(如armeabi-v7aarm64-v8a等)。如果构建成功,应该在这些目录下看到.so文件。

1.6.6. 常见问题

  • 确保Android.mkApplication.mk(如果有)文件中的路径和设置正确无误。
  • 如果遇到关于找不到头文件或库的错误,检查NDK_MODULE_PATH环境变量是否已设置,以包含任何额外的库路径。
  • 确保NDK版本与项目需求相匹配。

1.7. 在Java代码中加载和使用库

在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和本地库时,有几个重要的注意事项需要牢记,以确保一切能够顺利工作。以下是一些关键的注意事项。

2.1. JNI方法签名匹配

确保JNI方法签名与Java中声明的本地方法签名完全匹配。JNI方法签名包括方法名、参数类型和返回类型,并且这些类型需要转换为JNI可以理解的类型。如果签名不匹配,将导致运行时错误,通常是UnsatisfiedLinkError

2.2. 正确的库名和路径

  • 库名:当调用System.loadLibrary("name")时,Android会查找名为libname.so的库文件(注意前缀lib和后缀.so)。确保库文件名与Java中指定的名称相匹配。
  • 路径:Android会在应用的libs/<ABI>/目录下查找这些库文件。确保库文件已经放在正确的目录下,并且APK构建过程已经包含了这些文件。

2.3. ABI支持

确保应用支持所有目标ABI。不同的设备可能使用不同的CPU架构,因此需要为这些架构提供相应的.so文件。可以使用NDK提供的工具(如ndk-build或CMake)来构建针对不同ABI的库。

2.4. 自动构建本地库

  • 如果在Android Studio中工作,并且希望自动构建本地库,需要正确配置CMake或ndk-build。Android Studio的Gradle插件支持通过CMakeLists.txt或Android.mk文件自动构建本地库。
  • CMake:在build.gradle文件中配置CMake,指定CMakeLists.txt的位置和要构建的库。
  • ndk-build:对于较旧的项目或特定需求,可能需要使用ndk-build。这通常需要在build.gradle中通过外部任务来调用ndk-build命令。

2.5. 调试和错误处理

  • 调试:JNI调试可能比纯Java调试更具挑战性,因为需要同时处理Java和C/C++代码。使用Android Studio的Logcat可以帮助查看从C/C++代码输出的日志。
  • 错误处理:当JNI调用失败时,Android可能会抛出UnsatisfiedLinkErrorUnsupportedOperationException等异常。确保代码能够妥善处理这些异常,并给出有用的错误信息。

2.6. 性能考虑

  • 本地方法调用开销:虽然本地代码(如C/C++)通常比Java代码更快,但JNI调用本身也会带来一定的开销。在设计应用时,请考虑这一点,并只在确实需要时才使用本地代码。
  • 内存管理:在C/C++中,需要自己管理内存。确保代码没有内存泄漏,并且正确处理了所有分配的内存。

7. 安全性

  • 代码注入:确保本地库不会受到代码注入攻击。特别是当从外部源加载代码或数据到本地库时,需要格外小心。
  • 权限管理:确保应用不会通过JNI绕过Android的安全模型,如权限检查。

三、总结

在Android NDK开发中,Android.mk文件是关键配置文件,用于定义C/C++源码如何编译成动态库或静态库。实战中,需明确设置LOCAL_PATH指向源文件目录,通过CLEAR_VARS清除之前设置,定义LOCAL_MODULE为模块名,LOCAL_SRC_FILES列出源码文件。可选设置包括依赖库、编译器标志等。使用ndk-build命令在命令行编译,确保环境变量配置正确。对于大型项目,可考虑结合Gradle或CMake以提高构建效率和可维护性。Android.mk配置需细心,确保路径、名称无误,以顺利构建本地库。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

byte轻骑兵

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值