1. Java文件生成头文件
先编写一个Java文件 HelloJNI.java
public class HelloJNI {
static {
System.loadLibrary("hello");
}
public native String sayHello();
}
接着生成 C/C++ 头文件 HelloJNI.h,
其中“ . ”是指当前目录(生成的头文件放在当前目录)
javac -h . HelloJNI.java
该命令会生成一个 HelloJNI.h,这个头文件描述了我们需要实现的函数
/* DO NOT EDIT THIS FILE - it is machine generated */
#include <jni.h>
/* Header for class com_example_hellojnidemo_HelloJNI */
#ifndef _Included_com_example_hellojnidemo_HelloJNI
#define _Included_com_example_hellojnidemo_HelloJNI
#ifdef __cplusplus
extern "C" {
#endif
/*
* Class: com_example_hellojnidemo_HelloJNI
* Method: sayHello
* Signature: ()Ljava/lang/String;
*/
JNIEXPORT jstring JNICALL Java_com_example_hellojnidemo_HelloJNI_sayHello
(JNIEnv *, jobject);
#ifdef __cplusplus
}
#endif
#endif
- 生成的函数中有两个参数:
-
JNIEnv:JNIEnv 内部提供了很多函数,方便我们进行 JNI 编程。
C 代码中,JNIEnv 是指向 JNINativeInterface 结构的指针,为了访问任何一个 JNI 函数,该指针需要首先被解引用。因为 C 代码中的 JNI 函数不了解当前的 JNI 环境, JNIEnv
实例应该作为第一个参数传递给每一个 JNI 函数调用调用者,调用格式如下:
(*env)->NewStringUTF(env,"Hello from JNI !");
在 C++代码中,JNIEnv实际上是 C++ 类实例,JNI 函数以成员函数的形式存在,因此 JNI 函数调用不要求 JNIEnv 实例作参数。在C++中,完成同样功能的调用代码格式如下:
env->NewstringUTF ( "Hello from JNI ! ");
-
jobject: 指向 “this” 的 Java 对象
如果 java 中的 native 函数是 static 的,那第二个参数是 jclass,代表了 java 中的 Class 类
-
extern “C”
告诉 C++ 编译器以 C 的方式来编译这个函数,以方便其他 C 程序链接和访问该函数。C 和 C++ 有着不同的命名协议,因为 C++ 支持函数重载,用了不同的命名协议来处理重载的函数。在 C 中函数是通过函数名来识别的,而在 C++ 中,由于存在函数的重载问题,函数的识别方式通过函数名,函数的返回类型,函数参数列表三者组合来完成的。因此两个相同的函数,经过C,C++编绎后会产生完全不同的名字。所以,如果把一个用 C 编绎器编绎的目标代码和一个用 C++ 编绎器编绎的目标代码进行链接,就会出现链接失败的错误。 -
JNIEXPORT、JNICALL 两个宏在 linux 平台的定义如下:
//该声明的作用是保证在本动态库中声明的方法 , 能够在其他项目中可以被调用
#define JNIEXPORT __attribute__ ((visibility ("default")))
//一个空定义
#define JNICALL
2. 调用
MainActivity
HelloJNI helloJNI = new HelloJNI();
String s = helloJNI.sayHello();
Log.d("JNI", s);
3. 编写C文件,native的具体实现
新建HelloJNI.c
#include "com_example_hellojnidemo_HelloJNI.h"
#include <jni.h>
#include <stdio.h>
JNIEXPORT jstring JNICALL Java_com_example_hellojnidemo_HelloJNI_sayHello
(JNIEnv *env, jobject obj) {
return (*env)->NewStringUTF(env, "Hello From JNI");
}
4. 编译生成.so库
gcc -fpic -I"$JAVA_HOME/include" -I"$JAVA_HOME/include/linux" -shared -o libhello.so HelloJNI.c
-fpic:表示生成位置无关的代码。这样一来,产生的代码中就没有绝对地址了,全部使用相对地址,所以代码可以被加载器加载到内存的任意位置,都可以正确的执行。这是动态链接库所必需的。
-I:用于指定头文件的搜索路径。该参数后面跟着一个目录路径,编译器在编译源文件时会在指定的目录中查找头文件。
-I"$JAVA_HOME/include":表示包含JAVA的头文件。
-I"$JAVA_HOME/include/linux":表示包含JAVA在linux下的特定的头文件。
-shared:表示生成共享库。
-o 自定义 原文件:自定义生成可执行程序名称
5. 编写CMakeLists.txt
在app/下新建CMakeLists.txt
# 设置cmake最低版本
cmake_minimum_required(VERSION 3.4.1)
# add_library(库文件名称 STATIC/SHARED 文件)
# 静态库的扩展名一般为 *.a 或 *.lib;动态库的扩展名一般为 *.so 或 *.dll
# 注意,库文件名称通常为libxxx.so,在这里只要写xxx即可
add_library(
hello
SHARED
src/main/java/com/example/hellojnidemo/HelloJNI.c)
# find_library用于查找使用android ndk中系统库
# 查找log库,且重新别名为log-lib
find_library(
log-lib
log
)
# link_libraries 关联需链接的库
# 在linux中c/c++的编译一般都是用gcc来编译的,c/c++编译时会产生.o文件要通过make工具来把这些.o文件链接起来,
# 这样才能得一个可执行程序。所以.so在编译时要把所有库链接起来才能编
target_link_libraries(hello ${log-lib})
将其放入build编译的大环境中,这样代码才会生效,在app/build.gradle文件的android{}函数作用域中添加externalNativeBuild
externalNativeBuild {
cmake {
path "CMakeLists.txt"
}
}