前些天项目中有JNI开发的需求,突然发现竟然最基本的JNI的开发又忘了,只记得一些大概,还得搜半天。所以抽空再记录一下相关的内容,以及一些开发时候的想法(想法这东西像灵感一样,只有开发的时候才能想起来,时间长了就忘了,不记下来就浪费了),以便以后用到时可以快速的回忆起来。好记性不如烂笔头啊。

有JNI需求的项目中,首先,大多数都需要有Java层面的调用需求,也就是从Java层需要调用JNI层的方法,以实现部分功能。这些功能,可以是比较耗时的、耗内存的、与系统底层交互的(串口通信等)、Socket大数据量通信等。只要你觉得有必要,就使用JNI吧。

Java层代码

Java层代码分两部分:

public class JNITest{
	//第一部分,加载你需要的so文件,
    static
    {
        System.loadLibrary("HelloJni");
    }
    //第二部分,JNI接口方法
    private native int method1();
    private native void method2(int port,String ip);
    public  native String getResult();
    
	/**
	* 其他java层逻辑代码
	**/
	......
}
  • 第一部分中,加载的so文件的名称,实际为libHelloJni.so。在使用NDK生成so的时候,会在我们需要的名字HelloJni前面自动添加lib前缀。这种方式应该是为了让生成的so更具有标识性、统一性(这就是规矩)。loadLibrary的时候,直接去掉前面的lib前缀就好了。
  • 第二部分中,方法中需要添加一个native关键字;而且,这其实就是一个接口,没有方法体。实际的实现都在JNI代码中。

生成JNI层头文件

一般情况下,都是先写好java层代码,确定你需要什么样的接口,然后就生成JNI层的头文件,以便编写相应的方法体。
(在src/main目录下创建jni文件夹,步骤 Android Studio---->File----->new----->Folder----->JNI Folder)
我比较习惯用命令行生成头文件,当然也有其他方法。

cd src/main/java
javah -classpath . -d ../jni -jni com.xxxx.jnitest.JNITest
  • javah :要求必须配置好相应的java环境变量;
  • -classpath < path >: 从path中加载需要的类;
  • -d < dir >:生成的头文件的输出路径
  • -jni:生成 JNI 样式的标头文件 (默认值)

此时就生成了JNI头文件,文件名com_xxxx_jnitest_JNITest.h(包名+类名,以下划线分割每个层级),我们需要实现里面的方法:

/*
 * Class:     com_xxxx_jnitest_JNITest
 * Method:    method1
 * Signature: ()I
 */
JNIEXPORT jint JNICALL Java_com_xxxx_jnitest_JNITest_method1
  (JNIEnv *, jobject);

/*
 * Class:     com_xxxx_jnitest_JNITest
 * Method:    method2
 * Signature: (ILjava/lang/String;)V
 */
JNIEXPORT void JNICALL Java_com_xxxx_jnitest_JNITest_method2
  (JNIEnv *, jobject, jint, jstring);

实现相应的方法,在src/main/jni目录下再创建需要的源码文件,TestJni.cpp,名字可以随便取:

#include <string.h>
#include <jni.h>
#include <com_xxxx_jnitest_JNITest.h>

jstring Java_com_xxxx_jnitest_JNITest_getResult(JNIEnv *env,jclass thiz){
  return (*env)->NewStringUTF(env, "Hello from JNI !!!!!!!!!!!!!");
}
......

实现JNI方法体的过程,需要一定的C/C++基础。

生成SO文件

当完成JNI部分的代码开发后,就需要将我们的代码打包成so文件,这个时候,就需要介绍一下Android.mk和Applicaton.mk,以及CMakeLists.txt文件。Android.mk和Applicaton.mk两个需要一起使用,CMakeLists.txt文件可替代前面两者。.mk文件编写

Android.mk

LOCAL_PATH := $(call my-dir)
include $(CLEAR_VARS)
# c最终需要的so名称
LOCAL_MODULE    := HelloJni 
# c需要打入so的源码文件,这里需要确定源文件的路径
LOCAL_SRC_FILES := TestJni.cpp
# c生成的so的输出路径,这里会将so放置在 src/main/libs/armeabi-v7a/  文件目录下
NDK_APP_DST_DIR := ../libs/$(TARGET_ARCH_ABI)
include $(BUILD_SHARED_LIBRARY)

Applicaton.mk

# c支持的CPU架构
APP_ABI := armeabi-v7a
# c支持的Android SDK版本
APP_PLATFORM := android-24
APP_STL := c++_static
#APP_OPTIM := debug
APP_OPTIM := release
#NDK_TOOLCHAIN_VERSION := 4.9
NDK_TOOLCHAIN_VERSION := clang

上面的工作都完成了 之后,就可以使用NDK生成我们最终需要的so文件了。还是使用命令行:

cd src/main/jni
ndk-build

正常情况下,上述命令正常之后后,会在src/main/libs/armeabi-v7a/ 文件夹下面生成libHelloJni.so文件。

gradle 配置

生成了so文件,但是此时gradle还不知道so在哪,需要在app/build.gradle(也就是相应的module的build.gradle文件)配置文件中添加以下配置:

sourceSets {
        main {
            jniLibs.srcDirs = ['src/main/libs']
        }
    }

这样,在使用gradle打包APK的时候,就会把这个so打包进去。

另外,上面是手动使用ndk-build命令生成so的,这样比较低效,特别是在调试阶段,每改一点东西就需要手动生成so文件,太累了。
在上面的build.gradle文件中添加如下配置:

externalNativeBuild {
        ndkBuild {
            path file('src/main/jni/Android.mk')
        }
    }

这样,在生成APK的时候,gradle就会帮我们自动生成新的so至src/main/libs/armeabi-v7a/ 文件夹下,并打入新的apk内。