Android Studio下的Jni编程总结

文章目录

    • 1. jni的注册以及编译
      • 1.1 静态注册jni
        • 1.1.1 使用Android Studio创建NDK开发工程
        • 1.1.2 手动创建jni源文件
      • 1.2 动态注册jni
      • 1.3 jni编译的重要文件CMakeLists.txt
    • 2. Jni接口编写
      • 2.1 Jni的特殊描述符以及主要变量
        • 2.1.1 Jni中的特殊描述符
        • 2.1.2 JNIEnv和jobject类型总结
      • 2.2 Jni的主要函数
      • 2.3 Jni的函数签名
      • 2.4 Jni访问字符串
        • 2.4.1 关于编码方式
        • 2.4.2 需要用到的函数
    • 3 Jni中的反射
      • 3.1 访问成员属性
      • 3.2 访问成员方法

1. jni的注册以及编译

1.1 静态注册jni

1.1.1 使用Android Studio创建NDK开发工程

使用Android Studio直接创建jni接口比较简单,集成开发工具为我们做了大量工作,且默认为静态注册,下面首先简单梳理一下使用最新版本Android Studio(4.0.1)创建NDK开发工程的方法:

  1. 首先新建工程,选择C++工程;

  2. 填写工程信息后选择C++可支持的版本以及编译选项,这里暂时选择默认的toolchain;

  3. 完成后即可看到AS为我们自动生成NDK开发的c++目录,对应的CMakeLists.txt以及cpp源文件示例。接下来只需要在cpp目录下创建需要的其他c++源文件即可。在Android Studio自动配置的静态注册中,Java文件新定义任何接口都可以自动在cpp文件中生成对应的接口声明。

Android Studio自动生成的c++接口声明示例:

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

extern "C" JNIEXPORT jstring JNICALL
Java_com_example_jnitest_jniutil_JniManager_stringFromJNI(
        JNIEnv* env,
        jobject /* this */) {
    std::string hello = "Hello from C++";
    return env->NewStringUTF(hello.c_str());
}
extern "C"
JNIEXPORT jint JNICALL
Java_com_example_jnitest_jniutil_JniManager_getIDFromJni(JNIEnv *env, jobject thiz) {
    // TODO: implement getIDFromJni()
    return 0;
}

1.1.2 手动创建jni源文件

由于IDE为我们完成了过多工作使我们无法看清jni在创建过程中具体做了什么,因此这里简单尝试一下手动进行静态注册jni的过程,稍微降低对IDE的依赖。

创建jni主要需要三个步骤:创建必要的源文件、添加CMakeLists.txt、配置gradle。

1) 创建相关源文件:在原有的普通Android工程中,创建需要调用Native方法的java源文件,如:

package com.example.myappdemo.nativeManager;

public class NativeManager {
    public native String getName(int id); // 本地方法声明
}

然后针对这个Java文件创建cpp的头文件,可以使用jdk指令:

step1: javac NativeManage.java // 生成class文件

step2: javah -jni com.example.jnitest2.nativeManager.NativeManager // 生成jni头文件

但是实验发现以上命令无法再适用于jdk10以上了,jdk10以后javah被包含在javac中,因此以上命令可以合并为:

javac -h -jni NativeManager.java // 直接在当前目录下创建jni目录并生成头文件,根据项目不同要求,可以将路径更改为c++目录下

生成的头文件如下:

/* DO NOT EDIT THIS FILE - it is machine generated */
#include 
/* Header for class com_example_myappdemo_nativeManager_NativeManager */

#ifndef _Included_com_example_myappdemo_nativeManager_NativeManager
#define _Included_com_example_myappdemo_nativeManager_NativeManager
#ifdef __cplusplus
extern "C" {
#endif
/*
 * Class:     com_example_myappdemo_nativeManager_NativeManager
 * Method:    getName
 * Signature: (I)Ljava/lang/String;
 */
JNIEXPORT jstring JNICALL Java_com_example_myappdemo_nativeManager_NativeManager_getName
  (JNIEnv *, jobject, jint);

#ifdef __cplusplus
}
#endif
#endif

注意到h文件方法的名字结构是比较规则的,是以Java_包名_类名_方法名的结构出现,这也是静态注册的特点之一。

然后创建同名的cpp文件(与h文件同名方便识别),如下:

#include "com_example_myappdemo_nativeManager_NativeManager.h"
JNIEXPORT jstring JNICALL Java_com_example_myappdemo_nativeManager_NativeManager_getName
  (JNIEnv *env, jobject obj, jint id) {
  std::string hello = "Hello from C++";
  return env->NewStringUTF(hello.c_str());
 }
  1. 创建CMakeLists.txt

CMakeLists.txt是用来生成makefile(或者project文件)的工具,用于编译c++文件。具体CMakeLists的细节不在这里介绍,这里主要介绍基本的CMakeLists.txt如何创建和实现。

首先创建CMakeLists.txt文件,然后列出编译基本规则,如下:

// 这里是要求的最低cmake版本,但不是指定版本,指定版本在gradle中说明
cmake_minimum_required(VERSION 3.4.1)
// 这里是指定include文件夹路径,是CMakeLists.txt的相对路径,include由开发者根据情况自行创建
include_directories(include)
// 需要添加的library,native-lib为so库的名字;SHARED表示类型为共享库;
add_library( native-lib SHARED
            // 编译该库需要的源文件
            com_example_myappdemo_nativeManager_NativeManager.cpp
            com_example_myappdemo_nativeManager_NativeManager.h  ) 
  1. 在gradle中配置CMakeLists.txt

Android Studio是通过gradle完成代码编译的,因此CMakeLists.txt也需要配置在gradle中才能实现动态库的编译和加载。app/build.gradle中需要配置CMakeLists的位置有两处,一处为defaultConfig选项,一处为android选项中.

其中在defaultConfig中externalNativeBuild的cmake参数为编译选项,如下所示:

    defaultConfig {
        applicationId "com.example.myappdemo"
        minSdkVersion 26
        targetSdkVersion 29
        versionCode 1
        versionName "1.0"

        testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
        
        externalNativeBuild {
            cmake {
                cppFlags "-std=c++11" // 此处表示使用c++11标准
            }
        }
    }

在android中的externalNativeBuild需要标时CMakeLists.txt的路径和使用的cmake版本:

    externalNativeBuild {
        cmake {
            // 这里的路径是相对build.gradle的
            path "src/main/cpp/CMakeLists.txt"
            version "3.10.2"
        }
    }

1.2 动态注册jni

动态注册jni不需要静态注册那样严格地要求方法名,但是需要手动将native方法和java方法关联起来,并手动注册。具体步骤如下:

  1. 创建java源文件,定义需要的native方法:
public final class DynamicJniRegister {
    private DynamicJniRegister() {}
    private static class SingleInstHolder {
        private static DynamicJniRegister sInstance = new DynamicJniRegister();
    }

    public static DynamicJniRegister getInstance() {
        return SingleInstHolder.sInstance;
    }
    // 定义的native方法
    public native String getGreetingsFromDynamicJni();
}
  1. 创建jni的cpp文件,可以自行命名,但方便后续开发,依然推荐取名为java类包名。源文件需要包含jni的基础功能头文件jni.h;

  2. 在cpp文件中定义native接口的实现,如:

jstring getGreetingsFromDynamicJni(JNIEnv* env, jobject obj) {
    std::string hello = "Greetings from dynamic C++";
    return env->NewStringUTF(hello.c_str());
}
  1. 定义jni方法与java方法的映射数组JNINativeMethod。这个数据类型主要包含三部分:java方法名、方法签名、jni函数指针。有关该数据结构和签名含义会在后面详细总结。
static const JNINativeMethod jniNativeMethod[] = {
        {"getGreetingsFromDynamicJni", "()Ljava/lang/String;", (jstring *)getGreetingsFromDynamicJni}
};
  1. 实现JNI_Onload函数。该函数会在java中加载so库时自动调用。由于动态注册的jni无法根据命名查找到正确的jni函数,需要手动在该方法中确定映射规则。因此方法的主要功能是使用RegisterNatives:
static const char* mClassName = "com/example/myappdemo/nativemanager/DynamicJniRegister";

jint JNI_OnLoad(JavaVM *vm, void *unused) {
    JNIEnv* env = NULL;
    int res = vm->GetEnv((void**)&env, JNI_VERSION_1_4);
    if (res != JNI_OK) {
        return -1;
    }
    jclass mainClass = env->FindClass(mClassName);
    res = env->RegisterNatives(mainClass, jniNativeMethod, 1);
    if (res != JNI_OK) {
        return -1;
    }

    return JNI_VERSION_1_4;
}
  1. 在CMakeLists.txt中增加c++源文件,编译动态库:
add_library( dynamic_native-lib SHARED
        jni_dynamic_register.cpp)
  1. 在java文件中加载该动态库:
    static {
        System.loadLibrary("dynamic_native-lib");
    }

8)最后,使用1.1.2中的3)相同的方法配置build.gradle。至此就完成了动态注册jni.

1.3 jni编译的重要文件CMakeLists.txt

CMakeLists.txt是用来生成makefile并编译c++/c源文件的,可以通过简单的指令输入对源文件的编译需求。在使用Android Studio时,CMakeLists.txt是在修改后sync时执行生成makefile的,编译时自动读取externalNativeBuild中的makefile,并编译c/c++代码。
这里总结一下CMakeLists的基本编写方法。

1) 首先使用cmake_minimum_required限定cmake的最低版本,否则会产生警告:

cmake_minimum_required(VERSION 3.4.1)

2) 构建新的静态/动态库

使用add_library可以根据已有的cpp源文件编译新的静态/动态库,命令的参数为:

add_library(

​ dynamic_native-lib // 该参数为库的名字,如果是动态库,最终文件名为:libdynamic_native_lib.so

​ SHARED // 库的类型,SHARED为共享动态库,STATIC为静态库

​ jni_dynamic_register.cpp) // 对应的源文件,所有的源文件都可以罗列在这里,无需标点分割。

3)使用预编译库中的函数

Android中预制了一些NDK的库供开发者使用,比如log库。这类的原生库可以通过find_library将该库与变量关联,再通过target_link_libraries将变量连接在希望使用它的库中,以便于使用该库中的函数。

find_library的参数结构为:

find_library( log-lib // 希望连接到的变量名,即定义一个变量与需要使用的库对应

​ log) // 希望用到的NDK预制库

target_link_libraries的参数结构为:

target_link_libraries( dynamic_native-lib // 希望用到NDK预制库方法的目标库

​ ${log-lib} // find_library定义好的变量,这里可以添加多个

4) 引入第三方so库
引入第三方库的方法与创建一个新的native库类似,区别在于最后一个参数,我们通过IMPORTED标志告知CMake只希望将库导入到项目中。
关于目标库的路径有几点需要说明:

a. CMAKE_SOURCE_DIR表示的是CMakeLists.txt所在的路径,当指定第三方so所在路径时,应当以这个常量为起点。
b, 在具体项目中可以为每种ABI提供单独的软件包,就可以在jinLibs(如果是project结构的目录则是libs)下建立多个目录,每个目录对应一种ABI接口类型,然后再通过${ANDROID_ABI}来泛化这一层目录的结构,这样有助于充分利用特定的CPU架构。第三方的库关联到原生库与NDK库关联到原生库的原理是一样的。
c. 为了确保CMake可以在编译时定位我们的头文件,需要将include_directories() 命令添加到 CMake构建脚本中并指定头文件的路径

add_library(
#指定目标导入库
imported-lib
#设置导入库的类型(静态或动态)为shared library.
SHARED
#告知 CMake imported-lib 是导入的库
IMPORTED )

set_target_properties(
#指定目标导入库
imported-lib
#指定属性(本地导入的已有库)
PROPERTIES IMPORTED_LOCATION
#指定你要导入库的路径. 比如:
${CMAKE_SOURCE_DIR}/libs/${ANDROID_ABI}/libimported-lib.so )
#为了确保 CMake 可以在编译时定位到我们的 头文件,我们需要使用include_directories()命令,
#并包含头文件的路径
include_directories(libs/include/)

#要将预构建库关联到我们的原生库,需要将其添加到CMake构建脚本的target_link_libraries()命令中
target_link_libraries(
#这里指定了三个库,分别是native-lib、imported-lib和log-lib.
native-lib
imported-lib
#log-lib是包含在NDK中的一个日志库
${log-lib} )

2. Jni接口编写

Jni为C/C++编程,但是具体使用风格还是会有稍许不同(比如在字符串编码以及常用数据类型等),这里我会简单总结一下jni编程中的主要关键词、主要方法以及其他需要主意的事项。

2.1 Jni的特殊描述符以及主要变量

在1.1.1和1.1.2中我们看到静态注册的jni的c++和h文件中有一些特殊的描述符、宏以及数据结构,这里主要总结一下这些描述符的含义:
#ifdef __cplusplus
extern “C” {
#endif
/*

  • Class: com_example_jnitest2_nativeManager_NativeManager
  • Method: getName
  • Signature: (I)Ljava/lang/String;
    */
    JNIEXPORT jstring JNICALL Java_com_example_jnitest2_nativeManager_StaticRegisterJniManager_getName
    (JNIEnv *, jobject);

#ifdef __cplusplus
}

2.1.1 Jni中的特殊描述符

  1. extern “C”:
    jni静态注册属于C/C++混合编程,是通过函数名来寻找函数入口的。C与C++在编译中对函数名字的处理有所区别。C++存在重载,因此不能用函数名作为唯一标识,会将参数列表与返回值引入参考。使用extern "C"以后,可以使被extern "C"标识的部分使用C的编译方法,函数名不会做特殊处理,以方便jni正确识别。
    为什么在jdk自动生成的h文件中增加了#ifdef __cplusplus条件限制呢?因为h文件不可避免有可能被C程序引入,而C程序本身是不识别extern "C"修饰符的,因此需要确认当前引入该头文件的是C++文件,才使该修饰符生效。
  2. JNIEXPORT:
    这个关键字表明这个函数是一个可导出函数。每一个C/C++库都有一个导出函数列表,只有在这个列表里面的函数才可以被外部直接调用,类似Java的public函数和private函数的区别。
  3. JNICALL:
    说明这个函数是一个JNI函数,用来和普通的C/C++函数进行区别。

2.1.2 JNIEnv和jobject类型总结

  1. JNIEnv
    JNIEnv类型实际上代表了Java环境,通过这个JNIEnv* 指针,就可以对Java端的代码进行操作。例如,创建Java类中的对象,调用Java对象的方法,获取Java对象中的属性等等。JNIEnv的指针会被JNI传入到本地方法的实现函数中来对Java端的代码进行操作。因此每个函数中都有JNIEnv的指针参数。
    JNIEnv的部分源码如下:
typedef _JNIEnv JNIEnv;
struct _JNIEnv {
    /* do not rename this; it does not seem to be entirely opaque */
    const struct JNINativeInterface* functions;

#if defined(__cplusplus)

    jint GetVersion()
    { return functions->GetVersion(this); }

    jclass DefineClass(const char *name, jobject loader, const jbyte* buf,
        jsize bufLen)
    { return functions->DefineClass(this, name, loader, buf, bufLen); }

    jclass FindClass(const char* name)
    { return functions->FindClass(this, name); }

    jmethodID FromReflectedMethod(jobject method)
    { return functions->FromReflectedMethod(this, method); }
    ......

通过该源码可以看到,JNIEnv中包含JNINativeInterface结构,而后面的所有函数均是对JNINativeInterface中函数的封装,并对参数进行了修改。并且又看到了熟悉的宏:#if defined(__cplusplus)。这里可以看出C和C++对JNIEnv的处理有所不同。对于C编写的Jni程序,JNIEnv等同于JNINativeInterface,而对于C++来说,JNIEnv则对其进行了一次封装。
2. jobject
如果native方法不是static的话,jobject就代表这个native方法的类实例。
如果native方法是static的话,jobject就代表这个native方法的类的class对象实例(static方法不需要类实例的,所以就代表这个类的class对象)。
3. JavaVM
JavaVM的源码如下:

struct _JavaVM {
    const struct JNIInvokeInterface* functions;

#if defined(__cplusplus)
    jint DestroyJavaVM()
    { return functions->DestroyJavaVM(this); }
    jint AttachCurrentThread(JNIEnv** p_env, void* thr_args)
    { return functions->AttachCurrentThread(this, p_env, thr_args); }
    jint DetachCurrentThread()
    { return functions->DetachCurrentThread(this); }
    jint GetEnv(void** env, jint version)
    { return functions->GetEnv(this, env, version); }
    jint AttachCurrentThreadAsDaemon(JNIEnv** p_env, void* thr_args)
    { return functions->AttachCurrentThreadAsDaemon(this, p_env, thr_args); }
#endif /*__cplusplus*/
};

从源码中可以看到JavaVM的主要成员函数都是围绕虚拟机进行的,包括:销毁虚拟机、归入线程、作为守护线程归入某线程、获取环境参数等等。因此这个变量是针对整个线程的性质进行描述和控制的。
在jni动态注册的初始时,就是通过GetEnv获取虚拟机环境参数的。
4. JNINativeMethod 源码如下:

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

JNINativeMethod是用来联系Java方法与本地方法的数据,一般会定义成数组(因为有多个本地方法需要注册)。其中name为Java方法,signature是用来描述参数以及返回值的签名,fnPtr是C++的函数指针。

2.2 Jni的主要函数

这里所讲的函数,是指jni.h中的函数,包括JNIEnv以及JavaVM中的部分成员函数。

  1. JNIEXPORT jint JNI_OnLoad(JavaVM* vm, void* reserved);
    这个是jni,h中声明的外部函数,由JNIEXPORT声明。该函数如果实现,则会在加载库成功后调用,提供Java虚拟机参数(第二个参数预留)。一般动态注册需要在这个函数中做一些自定义初始化工作,如寻找关联的Java类、动态注册本地函数等。
  2. JNIEXPORT void JNI_OnUnload(JavaVM* vm, void* reserved);
    当虚拟机释放该C库时,则会调用JNI_OnUnload()函数来进行善后清除动作。
  3. jint GetEnv(void** env, jint version)
    用于获取环境参数,是JavaVM的成员函数。一般用于在库刚刚加载的初始化工作中。
  4. jint RegisterNatives(jclass clazz, const JNINativeMethod* methods,
    jint nMethods)
    注册本地函数,是JNIEnv的成员函数,是用来动态注册Jni的函数。其中jclass需要额外获取相关类,JNINativeMethod中记录了Java函数和C++函数的映射关系,也是预先写好的。
  5. jclass FindClass(const char* name)
    用来根据类的全命名寻找类的class对象,也是JNIEnv的成员函数:
static const char* mClassName = "com/example/jnitest2/nativeManager/DynamicRegisterJniManager";
jclass nativeManagerCls = env->FindClass(mClassName);

一般是用来做反射使用(调用某Java类的成员方法),或者在动态注册jni的初始时,需要用它来寻找关联的Java类。

2.3 Jni的函数签名

在1.2 动态注册jni中,以及2.1.2的第4条JNINativeMethod结构 中可以看到函数关系表都是有
函数签名参与的,函数签名用来描述函数的参数以及返回值,当需要使用可以通过jdk直接查询签名表,如:javap -s -p StaticRegisterJniManager.class
结果参考下图:
Android Studio下的Jni编程总结_第1张图片
常见的签名如下表所示:

数据类型 签名
boolean Z
byte B
char C
short S
int I
long J
float F
double D
void V
object L开头,以/分隔包的完整类型,结尾加‘;’ 比如String为:Ljava/lang/String;
Array 以[开头,加上数组元素类型的签名。如int[]应为[I,int[][]为[[I

如何组织JNINativeMethod的签名格式呢?比如当我的Java方法是这样的:
String foo(int a, boolean b, Date[] c), 对应的签名应该为:
(参数1 参数2 参数3 …)返回值
因此应该为:
(I Z [Ljava/util/Date;)Ljava/lang/String;

2.4 Jni访问字符串

2.4.1 关于编码方式

在Java的JVM内码中,String的编码是utf16编码格式,(也就是说每个字符的大小为2~4个字节),但是在jni中一般使用utf8编码格式,而在C/C++中则普遍使用原始数据(ASCII码),1个字节,中文使用GB2312(2个字节)。因此在使用jni进行字符串传递时,一定需要进行编码的转换,否则会出现乱码情况。
编码的转换过程参考下图:
Android Studio下的Jni编程总结_第2张图片

2.4.2 需要用到的函数

JNIEnv中有关String的函数参考下表:

函数定义 函数功能
jstring (NewString)(JNIEnv, const jchar*, jsize) 创建新的String
jsize (GetStringLength)(JNIEnv, jstring) 返回Unicode字符串的字符数。
const jchar* (GetStringChars)(JNIEnv, jstring, jboolean*) 获取以Unicode格式编码的字符串
void (ReleaseStringChars)(JNIEnv, jstring, const jchar*) 释放字符串的空间
jstring (NewStringUTF)(JNIEnv, const char*) 创建新的jstring字符串
jsize (GetStringUTFLength)(JNIEnv, jstring) 返回UTF-8字符串的字节数,不包含末尾’\0’。
const char* (GetStringUTFChars)(JNIEnv, jstring, jboolean*) 把jstring指针(指向JVM内部Unicode序列)转化成UTF-8格式的C字符串
void (ReleaseStringUTFChars)(JNIEnv, jstring, const char*) 释放UTF-8编码的字符串

值得一提的是,GetStringLength与GetStringUTFChars两个函数的表现效果是不确定的,具体由可能会将提供的参数jstring指针直接强转编码形势后返回,也可能会开辟一段空间并将内容copy过去。第三个参数jboolean*会反应本次操作是否发生了拷贝。为了保险起见,返回的字符串不应该做任何修改,且使用完成应该对应使用ReleaseStringChars和ReleaseStringUTFChars进行释放,防止内存泄漏。

3 Jni中的反射

3.1 访问成员属性

Jni中每个函数都具有JNIEnv*和jobject两个参数,其中jobject在前文中介绍到,是调用者类的对象或者其class。如果当前native方法不是static时,欲访问其成员需要以下步骤(此处假设目标类的某成员属性为 int property = 0):

  1. 获得其class引用:
    jclass clazz = env->GetObjectClass(jobj); // 此处以C++举例
  2. 获取成员属性ID:
    jfieldID jfid = env->GetfieldID(clazz, “property”, “I”) //property为属性名,“I”为其类型签名
  3. 获取成员属性的值:
    jint val = env->GetIntField(jobj, jfid);
  4. 修改成员属性值:
    env->SetIntField(jobj, jfid, val + 100);
    note:当操作私有成员时,需要设置setAccessible(true)后才可以哦!
    如果访问的是静态变量,获取/设置字段需要更换为:
    val = env->GetStaticIntField(clazz, jobj, jfid);
    env->SetStaticIntField(clazz, jobj, jfid, val+100);

3.2 访问成员方法

访问成员方法一般需要按照以下流程(获得类的引用流程基本类似,因此这里我们将前提条件那个了更换,在3.1中假设调用者类就是需要反射的类,此处我们将假定反射任意一个类,例如类名为:FooClass,目标方法为:void methodName(String, int) ):

  1. 获得class引用,对于任意一个存在的类,需要使用FindClass来查找:
    jclass clz = env->FindClass(“FooClass”);
  2. 查找需要使用的 (假设是静态方法,与普通方法流程一致,但调用函数名有区别)
    jmethodID jmeid = env->GetStaticMethodID(clz, “methodName”, "
    (Ljava/lang/String;I)V");
  3. 调用该静态方法
    env->CallStaticVoidMethod(clz,jmeid,“test”,100);

你可能感兴趣的:(Java,android,jni)