Android JNI/NDK环境的配置与Demo编译
一、背景
JNI(Java Native Interface)和NDK(Native Development Kit)在Android开发中扮演着重要的角色。
JNI,即Java本地接口,是Java平台的一部分,它允许Java代码与其他语言写的代码进行交互。通过JNI,Java代码可以调用本地应用程序或库中的代码,也可以被本地代码调用。这主要使得Java能够和本地其他类型语言(如C、C++)进行交互。JNI的一个主要用途是,当Java层的应用代码需要执行一些计算密集型或者对实时性要求高的任务时,可以通过JNI调用本地层的C/C++代码来提高性能。同时,JNI也允许Java调用由C/C++实现的驱动,从而扩展JAVA虚拟机的能力。
而NDK,即本地开发工具包,是一组工具的集合,它允许开发者使用C和C++代码来开发Android应用的部分组件。NDK提供了必要的工具链和库,帮助开发者编译和构建本地代码(C/C++代码),生成可以在Android应用中使用的本地库(.so文件)。NDK的主要用途包括:提高应用的性能,通过使用C/C++代码实现关键部分来优化性能;复用现有的C/C++代码库,无需将其重写为Java代码;访问低级系统API和硬件,例如直接操作图像数据或使用特定的硬件指令。
总的来说,JNI和NDK在Android开发中主要用于提高应用的性能、复用代码、访问底层硬件和操作系统特性等。例如,游戏开发可以通过NDK和JNI调用原生代码,实现高质量的游戏体验;使用C或C++语言可以更方便地调用系统提供的接口,实现更安全和更高效的文件操作。
二、NDK的环境搭建
-
Java环境:正常的classpath和javahome的配置。
-
ndk:在androidstudio中配置好了ndk环境
补充:ndk环境搭建
在 Android Studio 中配置环境以便能够开发本地代码(如 C 或 C++)涉及到几个步骤。
1. 下载 NDK
下载NDK包:https://developer.android.google.cn/ndk/downloads?hl=zh-cn
2. 在 Android Studio 中配置 NDK 路径
(1)点击菜单栏的 File -> Project Structure
(2)在左侧面板中,选择 SDK Location。(原来设置过就不用管)
(3)在 Android NDK location 字段中,点击右侧的 ... 按钮。(注:如果这里是灰色的就说明你没有下载ndk包)
(4)浏览并选择你之前解压 NDK 的目录。
(5)点击 OK 保存设置。
3. 在你的项目中配置 CMake 或 ndk-build
Android Studio 支持 CMake 和 ndk-build 作为构建本地代码的工具。你需要选择其中一个,并在你的项目中配置它。
(1)CMake 配置:
在你的项目根目录下创建一个名为 CMakeLists.txt 的文件(如果尚未创建)。
在这个文件中,定义你的本地源代码和构建规则。
在 app 模块的 build.gradle 文件中,添加对 CMake 的支持。例如:
gradle
android {
...
defaultConfig {
...
externalNativeBuild {
cmake {
cppFlags ""
}
}
}
externalNativeBuild {
cmake {
path "CMakeLists.txt"
version "3.10.2" // 使用你安装的 CMake 版本
}
}
}
(2)ndk-build 配置:【比较常用】
在你的项目根目录下创建一个名为 Android.mk 和一个名为 Application.mk 的文件(如果尚未创建)。
在这些文件中,定义你的本地源代码和构建规则。
在 app 模块的 build.gradle 文件中,添加对 ndk-build 的支持。例如:
gradle
android {
...
defaultConfig {
...
ndk {
moduleName "your_module_name" // 替换为你的模块名
}
}
sourceSets.main {
jni.srcDirs = [] // 禁用默认的 jni 源目录
jniLibs.srcDirs = ['libs'] // 指定你的 .so 文件目录,记得.so文件要放到这里
}
}
4. 同步你的项目
点击 Android Studio 工具栏中的 Sync Now 按钮,以确保你的项目配置已正确同步。
5. 编写和构建本地代码
现在你可以在你的项目中编写本地代码,并使用 Android Studio 的构建系统来编译它。你应该能够在 app/build/intermediates/cmake 或 app/build/intermediates/ndkBuild(取决于你使用的构建系统)目录下找到生成的 .so 文件。
三、JNI类的编译与链接流程
在Android开发中,编译JNI(Java Native Interface)的.so
(共享对象)库通常涉及几个步骤。以下是一个简化的流程,并附带一个示例来说明如何编译JNI库。
1. 编写Java类
首先,你需要一个Java类来声明本地方法。这些方法将由C/C++代码实现。
package com.example.testsdk.jni;
public class MyJniClass {
// 加载本地库
static {
System.loadLibrary("my-jni-lib");
}
// 声明本地方法
public native String nativeStringFromJNI();
}
2. 生成JNI头文件
使用javah
工具(在JDK中)从Java类生成JNI头文件。这个头文件将包含Java本地方法的C/C++签名。
C:\Android-project\testSdk\app\src\main\java> javah -jni com.example.testsdk.jni.MyJniClass
这将在jni
目录下生成一个名为com_example_testsdk_jni_MyJniClass.h
的头文件。
com_example_testsdk_jni_MyJniClass.h头文件内容:
/* DO NOT EDIT THIS FILE - it is machine generated */
#include <jni.h>
/* Header for class com_example_testsdk_jni_MyJniClass */
#ifndef _Included_com_example_testsdk_jni_MyJniClass
#define _Included_com_example_testsdk_jni_MyJniClass
#ifdef __cplusplus
extern "C" {
#endif
/*
* Class: com_example_testsdk_jni_MyJniClass
* Method: nativeStringFromJNI
* Signature: ()Ljava/lang/String;
*/
JNIEXPORT jstring JNICALL Java_com_example_testsdk_jni_MyJniClass_nativeStringFromJNI
(JNIEnv *, jobject);
#ifdef __cplusplus
}
#endif
#endif
3. 编写C/C++代码
实现Java中声明的本地方法。在C/C++代码中,你需要包含上面生成的头文件。
// jni/my_jni_lib.cpp .cpp源文件里
#include <jni.h>
#include "com_example_myjni_MyJniClass.h"
JNIEXPORT jstring JNICALL
Java_com_example_testsdk_jni_MyJniClass_nativeStringFromJNI(JNIEnv *env, jobject obj) {
return (*env)->NewStringUTF(env, "Hello from JNI!");
}
如你的com_serialno_ReadWriteSerialNo.cpp源文件如下:
#include <jni.h>
#include <string>
#include <termios.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <string.h>
#include "android/log.h"
#include <cstdint> // for uint8_t
#include <sstream>
#define LOGI(fmt, args...) __android_log_print(ANDROID_LOG_INFO, TAG, fmt, ##args)
#define LOGD(fmt, args...) __android_log_print(ANDROID_LOG_DEBUG, TAG, fmt, ##args)
#define LOGE(fmt, args...) __android_log_print(ANDROID_LOG_ERROR, TAG, fmt, ##args)
#define DEBUG_LOG true
static const char *TAG = "serial_no";
typedef unsigned short uint16;
typedef unsigned long uint32;
typedef unsigned char uint8;
#define VENDOR_REQ_TAG 0x56524551
#define VENDOR_READ_IO _IOW('v', 0x01, unsigned int)
#define VENDOR_WRITE_IO _IOW('v', 0x02, unsigned int)
#define VENDOR_SN_ID 1
#define VENDOR_WIFI_MAC_ID 2
#define VENDOR_LAN_MAC_ID 3
#define VENDOR_BLUETOOTH_ID 4
#define RKNAND_SYS_STORGAE_DATA_LEN 512 /* max read length to read*/
#define SERIALNO_BUF_LEN 33
/**此处加入你上面生成的头文件***/
#include <jni.h>
#include "com_example_testsdk_jni_MyJniClass.h"
extern "C" JNIEXPORT jstring JNICALL
Java_com_example_testsdk_jni_MyJniClass_nativeStringFromJNI(JNIEnv *env, jobject obj) {
return (*env).NewStringUTF("Hello from JNI!");
}
/*************************/
void hexToAscii(uint8_t *hexData, size_t dataSize, char *asciiBuffer);
int getNum(char arr[]);
struct rk_vendor_req {
uint32 tag;
uint16 id;
uint16 len;
uint8 data[512];
};
//十进制转十六进制
void hexToAscii(uint8_t *hexData, size_t dataSize, char *asciiBuffer) {
for (size_t i = 0; i < dataSize; ++i) {
sprintf(&asciiBuffer[i * 2], "%02X", hexData[i]);
}
}
//十六进制转十进制
int getNum(char arr[]){
int m=0;
int sz=strlen(arr);
int i=0;
for(i=0;i<sz;i++)
{
if(arr[i]>='0'&&arr[i]<='9'){
m=m*16+arr[i]-'0';
}
else
{
m=m*16+arr[i]-'A'+10;
}
}
LOGD("m = %d", m);
return m;
}
extern "C"
JNIEXPORT jstring JNICALL
Java_com_serialno_ReadWriteSerialNo_read(JNIEnv *env, jclass type,jint id){
int fd;
int ret = 0;
int len = 0;
char r_buf[RKNAND_SYS_STORGAE_DATA_LEN]={0};
uint8 error[]="error";
struct rk_vendor_req req;
struct rk_vendor_req *p_req = &req;
p_req->tag = VENDOR_REQ_TAG;
if(id == 1)
p_req->id = VENDOR_SN_ID;
else if(id == 3) p_req->id = VENDOR_LAN_MAC_ID;
p_req->len = RKNAND_SYS_STORGAE_DATA_LEN;
fd = open("/dev/vendor_storage", O_RDWR, 0);
LOGD("open() fd = %d", fd);
if (fd == -1) {
LOGE("Cannot open port");
ret = -1;
}else{
ret = ioctl(fd, VENDOR_READ_IO, p_req);
close(fd);
}
if (!ret) {
// if(id == 1)
len = strlen((char*)p_req->data);
// else if(id == 3)
//len = strlen((char*)p_req->data);
if(len <= 0){
len = strlen((char*)error);
memcpy(r_buf, error, len);
}else{
//sprintf(r_buf, "%d", p_req->data[0]);
if(id == 1)
memcpy(r_buf, p_req->data,len);
else if(id == 3)
hexToAscii(p_req->data,len,r_buf);
//memcpy(r_buf, p_req->data,len);
//sprintf(r_buf, "%s", r_buf);
//LOGD("vendor read sn1 = %c len = %c, \n",(char)p_req->data[0], (char)p_req->data[1]);
//LOGD("vendor read sn1 = %s len = %d, \n",r_buf, len);
}
}else{
len = strlen((char*)error);
memcpy(r_buf, error, len);
}
//convert(r_buf);
LOGD("vendor read sn2 = %s len = %d, \n", r_buf, len);
jclass strClass = (env)->FindClass("java/lang/String");
jmethodID ctorID = (env)->GetMethodID(strClass, "<init>", "([BLjava/lang/String;)V");
jbyteArray bytes = (env)->NewByteArray(len);
(env)->SetByteArrayRegion(bytes, 0, len, (jbyte*) r_buf);
jstring encoding = (env)->NewStringUTF("utf-8");
return (jstring) (env)->NewObject(strClass, ctorID, bytes,encoding);
}
extern "C"
JNIEXPORT jint JNICALL
Java_com_serialno_ReadWriteSerialNo_write(JNIEnv *env, jclass type,jint id,jstring sn){
int fd;
int ret = 0;
int len = 0;
struct rk_vendor_req req;
struct rk_vendor_req *p_req = &req;
jboolean iscopy;
const char *sn_utf = env->GetStringUTFChars(sn, &iscopy);
LOGD("write serial no = %s", sn_utf);
fd = open("/dev/vendor_storage", O_RDWR, 0);
LOGD("open() fd = %d", fd);
if (fd == -1) {
LOGE("Cannot open port");
ret = -1;
}
p_req->tag = VENDOR_REQ_TAG;
//p_req->id = VENDOR_SN_ID;
if(id == 1)
p_req->id = VENDOR_SN_ID;
else if(id == 3) p_req->id = VENDOR_LAN_MAC_ID;
p_req->len = RKNAND_SYS_STORGAE_DATA_LEN;
len = strlen(sn_utf);
LOGD("p_req->len = %d", len);
if(len > SERIALNO_BUF_LEN){
len = SERIALNO_BUF_LEN;
}
if(id == 3){
int k=0;
for(int i=0;i<len-1;i+=2){
char num[3] = {sn_utf[i],sn_utf[i+1],'\0'};
LOGD("num[2] = %d", strlen(num));
p_req->data[k++] = getNum(num);
}
}else if(id == 1){
memset(p_req->data, 0, RKNAND_SYS_STORGAE_DATA_LEN);
memcpy(p_req->data, sn_utf, len);
}
ret = ioctl(fd, VENDOR_WRITE_IO, p_req);
close(fd);
env->ReleaseStringUTFChars(sn, sn_utf);
LOGD("vendor write sn = %s, ret = %d\n", p_req->data, ret);
return ret;
}
注意:非常需要注意路径,如果编译报错一般是路径的原因。
4. 配置CMake或ndk-build
对于Android NDK项目,你需要配置CMake或ndk-build来编译C/C++代码。以下是一个简化的CMakeLists.txt示例:
# CMakeLists.txt
cmake_minimum_required(VERSION 3.4.1)
#添加自己的so库test-lib,设置一个名字,输出就是这个名字,还有源文件
add_library(
my-jni-lib
SHARED
com_serialno_ReadWriteSerialNo.cpp
)
find_library(
log-lib
log
)
#链接cpp文件
target_link_libraries(
my-jni-lib
${log-lib}
)
5. 在Android项目中配置NDK
在Android项目的build.gradle
文件中,确保你已经配置了NDK。例如:
android {
...
defaultConfig {
...
externalNativeBuild {
cmake {
cppFlags ""
}
}
ndk{
abiFilters "armeabi-v7a"
moduleName "my-jni-lib" // 生成so的名称
ldLibs "log", "z", "m", "jnigraphics", "android"
}
}
externalNativeBuild {
cmake {
path "C:\\Android-project\\testSdk\\app\\src\\main\\java\\jni\\CMakeLists.txt"
version "3.22.1"
}
}
//注意CMakeLists的路径,如果在根目录就直接写CMakeLists.txt
}
6. 编译项目
现在可以编译Android项目了。Gradle将使用CMake或ndk-build来编译C/C++代码,并生成.so
库。这些库将被包含在APK中,并在运行时由Java代码加载。(通过build->make project或者rebuild project)
7. 在Java代码中加载和使用库
在Java代码中,可以像之前那样使用System.loadLibrary("my-jni-lib")
来加载库,并调用本地方法。
注意:这个示例假设已经在Android项目中正确设置了NDK和CMake。如果还没有设置,需要先下载NDK并在Android Studio中配置它。
8.测试so的正确性
-
在native类中加载so文件
static { System.loadLibrary("my-jni-lib"); }
注意:我们生成的so文件名为libmy-jni-lib.so,但是我们不需要lib这个前缀,只需要把我们在cmake中配置的名字加载进去即可。
-
正常调用
testJni = new TestJni(); textView.setText(testJni.getMessage());
补充:
这段代码是Gradle构建脚本(通常用于Android应用或库的构建)中的一部分,特别是在配置Native Development Kit (NDK) 支持时。
这里,ndk 配置块定义了哪些ABI(Application Binary Interface)或CPU架构应该被编译和包含在最终的APK中。ABI决定了应用或库如何与设备的底层硬件进行交互。
具体解释如下:
x86_64:这是为基于x86架构的64位处理器编译的。这种架构主要在模拟器(如Android Studio的模拟器)和一些高端平板电脑或Chromebooks上使用。
armeabi-v7a:这是为基于ARM架构的32位处理器编译的,特别是那些支持ARMv7指令集的处理器。这是Android设备中最常见的架构之一,许多旧的和中端设备都支持它。
arm64-v8a:这是为基于ARM架构的64位处理器编译的,特别是那些支持ARMv8指令集的处理器。这种架构在现代高端Android设备上越来越普遍。
通过在ndk块中指定这些ABI,你可以确保你的应用或库在具有这些处理器架构的设备上运行时具有最佳性能。同时,这也允许你减少APK的大小,因为你不需要包含所有可能的ABI的二进制文件。
注意:从Android Gradle Plugin 3.0开始,Google推荐只包含armeabi-v7a和arm64-v8a,因为Google Play会提供x86的二进制文件翻译服务。然而,如果你的应用或库对性能非常敏感,或者你正在使用模拟器进行开发,那么包含x86_64可能仍然是有意义的。