Android Studio开发之JNI简单示例

JNI:Java Native Interface,java本地开发接口。Java和C/C++的通信接口,是一个用来沟通Java和本地代码(C/C++)的协议。

编译环境:android studio 3.1.4,ndk17

1、JNI基础

1.1 使用JNI的好处  

  • 1.效率上 C/C++比java更高效;
  • 2.代码移植,如果之前用C语言开发过模块,可以复用已经存在的c代码;
  • 3.java反编译比C语言容易,一般加密算法都是用C语言编写,不容易被反编译;

1.2 Java和JNI类型对照表

Java类型

JNI类型

C/C++类型

大小

boolean

Jboolean

unsigned char

无符号8位

Byte

jbyte

char

有符号8位

Char

jchar

unsigned short

无符号16位

Short

jshort

short

有符号16位

Integer

Jint

int

有符号32位

Long

jlong

long long

有符号64位

Float

jfloat

float

32位浮点值

Double

jdouble

double

64位双精度浮点值

1.3 Java和JNI引用类型对照表

与java基本类型不同,引用类型对开发人员是不透明的。java的内部数据结构并不直接向原生代码公开。也就是说C/C++代码不能直接访问java代码的字段和方法。

Java类型 C/C++类型
java.lang.Class jclass
java.lang.Throwable jthrowable
java.lang.Object jobject
java.util.Objects jobjects
java.lang.Object[] jobjectArray
Boolean[] jbooleanArray
Byte[] jbyteArray
Char[] jcharArray
Short[] jshortArray
int[] jintArray
long[] jlongArray
float[] jfloatArray
double[] jdoubleArray
通用数组 jarray

注:任何java数组在JNI里面都可以使用jarray来表示,例如:java的int[]数组,可以表示为jintArray,也可以表示为jarray。

1.4 java类型签名映射表

JNI获取java的方法ID和字段ID,都需要一个很重要的参数,就是java的方法和字段的签名,这个签名需要通过下面的表来获取,这个表很重要,建议最好记住。

Java类型 签名
Boolean Z
Byte B
Char C
Short S
Integer I
Long J
Float F
Double D
Void V
任何Java类的全名

L任何java类的全名;

比如:Java String类对应的签名是Ljava/lang/String;

type[]

type[

这个就是java数组的签名,比如Java int[]的签名是[I,java long[]的签名是[J,Java String[]的签名是[Ljava/lang/String;

方法类型

(参数类型)返回值类型;

比如java方法void hello(String msg, String msg2)对应的签名就是(Ljava/lang/String; Ljava/lang/String;)V

比如java方法String getNewName(String Name)对应的签名是(Ljava/lang/String;)Ljava/lang/String;

比如Java方法long add(int a, int b)对应的签名是(II)J

2、加载so库

Android提供了3个使用的函数用于加载JNI库,分别是System.loadLibrary(libname), Runtime.getRuntime().loadLibrary(libname),以及Runtime.getRuntime().load(libFilePath)。

2.1 用loadLibrary函数加载

用System.LoadLibrary(libname)和Runtime.getRumtime().loadLibrary()这两个函数加载so库,不需要指定so库的路径,Android会默认从系统的共享目录中去查找,Android的共享目录是Vendor/lib和System/lib,如果共享库路径找到指定名字的so库,则立即加载这个so库。所以我们给so库起名时要尽量避免与android共享库里面的so库重名。如果在共享目录库中找不到,则在APP的安装目录里面查找APP的私有so库。如果找到,则加载该so库。

2.2 用load函数加载

用Runtime.getRumtime().load(libFilePath)加载so库,需要指定完整的so库路径,优点就是加载速度快,并且不会加载错误的so库,缺点就是需要指定完整的so库路径,使用的时候不大方便,因此大家常用的加载so库的方式还是用loadLibrary函数加载。

3、常规实现

1.在java类中声明native方法

2.使用javah命令生成native方法的头文件

3.新建.cpp文件实现native方法

3.1 代码示例

1.在Java目录下新建jniUtils目录,然后在该目录下新建HelloJni.java文件,并声明native方法。

public class HelloJni {
    static {
        System.loadLibrary("JniTestSample");
    }
    public static native String getJniString();
}

2.通过javah命令生成头文件。用AS的Terminal或Windows的cmd.exe,进入项目的Java目录。例如:cd E:\Android\AndroidProject\TestJni\app\src\main\java 然后用javah命令生成头文件,javah -jni com.example.liuz4.testjni.JniUtil.HelloJni 。生成的头文件在Java目录下。头文件com_example_liuz4_testjni_JniUtil_HelloJni.h

3.头文件的命名格式是:包名+类名,打开头文件,JNI调用的C语言函数是有固定格式的:java_包名_类名(JNIEnv *,)

#include <jni.h>
/* Header for class com_example_liuz4_testjni_JniUtil_HelloJni */

#ifndef _Included_com_example_liuz4_testjni_JniUtil_HelloJni
#define _Included_com_example_liuz4_testjni_JniUtil_HelloJni
#ifdef __cplusplus
extern "C" {
#endif
/*
 * Class:     com_example_liuz4_testjni_JniUtil_HelloJni
 * Method:    getJniString
 * Signature: ()Ljava/lang/String;
 */
JNIEXPORT jstring JNICALL Java_com_example_liuz4_testjni_JniUtil_HelloJni_getJniString
  (JNIEnv *, jclass);

#ifdef __cplusplus
}
#endif
#endif

4.右键点击module,新建jni目录

5.将c++依赖的头文件拷贝到jni目录下,并在该目录下新建C++源文件。例如:Hello_Jni.cpp

#include "com_example_liuz4_testjni_JniUtil_HelloJni.h"


JNIEXPORT jstring JNICALL Java_com_example_liuz4_testjni_JniUtil_HelloJni_getJniString
  (JNIEnv *env, jclass obj){
         return env->NewStringUTF("hello JNI ...");
}

4、CMakeLists.txt配置C++编译

1、修改c++源码对应module的build.gradle文件,在android{}中增加如下内容:

android {

    ...

    defaultConfig {
        
        ...
        
        externalNativeBuild{
            cmake{
                cppFlags ""
                //生成多个版本的so文件
                abiFilters 'arm64-v8a', 'armeabi-v7a'
            }
        }
        
        ...

    }

    ...

    externalNativeBuild {
        cmake {
            path 'CMakeLists.txt'
        }
    }
}

2、CMakeLists.txt文件

# Sets the minimum version of CMake required to build your native library.
# This ensures that a certain set of CMake features is available to
# your build.

cmake_minimum_required(VERSION 3.4.1)
# Specifies a library name, specifies whether the library is STATIC or
# SHARED, and provides relative paths to the source code. You can
# define multiple libraries by adding multiple add.library() commands,
# and CMake builds them for you. When you build your app, Gradle
# automatically packages shared libraries with your APK.

add_library( # Specifies the name of the library.
             JniTestSample

             # Sets the library as a shared library.
             SHARED

             # Provides a relative path to your source file(s).
             src/main/jni/Hello_Jni.cpp)
 # Specifies a path to native header files.
 # include_directories(src/main/jni/)
 find_library( # Defines the name of the path variable that stores the
               # location of the NDK library.
               log-lib

               # Specifies the name of the NDK library that
               # CMake needs to locate.
               log )
 # Links your native library against one or more other native libraries.
 target_link_libraries( # Specifies the target library.
                        JniTestSample

                        # Links the log library to the target library.
                        ${log-lib} )

其中:

add_library函数用来配置要生成的so库的基本信息,比如so库的名字,生成静态库还是共享的,以及so库的c/c++源文件列表。add_library函数参数说明:第一个参数是so库名字;第二个参数是生成的so库的类型,静态so库是STATIC,共享so库是SHARED;第三个参数是C/C++源文件,可以包含多个源文件。

find_library函数用来从NDK目录下面查找特定的so库。find_library参数说明:第一个参数是我们给查找到的so库取个自己的名字,名字可以随便写;第二个参数是要查找的so库的名字,这个名字是从真实的so库的名字去掉lib前缀和.so后缀的名字,例如liblog.so的名字就是log。

target_link_libraries函数用来把要生成的so库和依赖的其它so库进行链接,生成我们需要的so库文件。target_link_libraries参数说明:第一个参数是我们要生成的so库的名字去掉前缀和后缀后的名字;后面参数是需要生成的so库所要依赖的库。

include_directories设置so库的头文件目录

5、动态注册实现

5.1 常规实现的不足

传统的关于android使用JNI调用C/C++程序,首先javah 生产头文件,然后拷贝头文件里面的方法到C文件中进行映射调用。使用这种方法有两个缺陷:

1、这种方法生成的映射方法名不太规则也比较长

2、调用数据较慢

因此可以使用JNI动态注册方法的方式来解决这两个问题

5.2 基础知识

1、JNI_OnLoad()方法:当java层代码执行System.loadLibrary,Native中的JNI_OnLoad方法被调用,此时可以注册对应于Java层调用的navtive方法。

2、JNINativeMethod结构体:JNINativeMethod包含三个元素: 方法名, 方法签名, native函数指针。该结构体用于描述需要注册的方法信息。

typedef struct {
    const char* name;
    const char* signature;
    void*       fnPtr;
} JNINativeMethod;

3、RegisterNatives方法:该方法是JNI环境提供的用于注册Native方法的方法。

5.3 实现

4.3.1 Java代码实现

不需要调整,与传统调用方法一致。

4.3.2 C++代码实现

1、不需要用javah生成头文件。因为实现java的native方法,可以随意定义方法名,不用按固定格式进行命名。

2、定义JNINativeMethod数组,将native方法与c++实现的函数关联起来。

其中getJniString为java中待实现的native方法名。()Ljava/lang/String; 为方法的签名, ()表示该方法无参数, Ljava/lang/String;表示返回值为Java中的String类型。

3、实现源码示例:

#include <jni.h>
// 指定要注册的类,对应完整的java类名
#define JNIREG_CLASS "com/example/liuz4/testjni/JniUtil/HelloJni"
#ifndef NELEM
#define NELEM(x) ((int) (sizeof(x) / sizeof((x)[0])))
#endif
JNIEXPORT jstring JNICALL nativeHello(JNIEnv *env, jclass obj){
         return env->NewStringUTF("this is regist Native JNI ...");
  }

// Java和JNI函数的绑定表
static JNINativeMethod method_table[] = {
        { "getJniString", "()Ljava/lang/String;", (void*)nativeHello },//绑定
};
// 注册native方法到java中
static int registerNativeMethods(JNIEnv* env, const char* className,
                                 JNINativeMethod* gMethods, int numMethods)
{
    jclass clazz;
    clazz = env->FindClass(className);
    if (clazz == NULL) {
        return JNI_FALSE;
    }
    if (env->RegisterNatives(clazz, gMethods, numMethods) < 0) {
        return JNI_FALSE;
    }

    return JNI_TRUE;
}
JNIEXPORT jint JNI_OnLoad(JavaVM* vm, void* reserved)
{
    JNIEnv* env = NULL;
    jint result = -1;

    if (vm->GetEnv((void**) &env, JNI_VERSION_1_4) != JNI_OK) {
        return result;
    }

    registerNativeMethods(env, JNIREG_CLASS,
                          method_table, NELEM(method_table));

    // 返回jni的版本
    return JNI_VERSION_1_4;
}
JNIEXPORT void JNI_OnUnload(JavaVM *jvm, void *reserved){

}

6、JNI函数详解

6.1 C/C++字符串转JNI字符串

7、结束语

本文的基础知识介绍借鉴于https://blog.csdn.net/kgdwbb/article/details/72810251

更多流媒体资讯,请关注小牛安卓干货铺,将进行不定期推送。

 

猜你喜欢

转载自blog.csdn.net/liuzhi0724/article/details/82155999