什么是NDK呢?什么是JNI呢?
NDK(Native Development Kit)是一个允许开发者使用C和C++编写Android应用程序的工具集。它提供了一系列的工具和库,可以帮助开发者将高性能的原生代码集成到Android应用中。
NDK的主要目标是提供一种方式,让开发者能够在需要更高性能或更底层控制的情况下使用C和C++编写部分应用程序,而不仅仅依赖于Java。
JNI(Java Native Interface)是一种编程框架,用于在Java代码和原生代码(如C和C++)之间进行交互。通过JNI,开发者可以在Java代码中调用原生代码的函数,并且可以将Java对象传递给原生代码进行处理。
JNI的主要作用是提供一种标准的接口,使得Java代码能够与原生代码进行通信。开发者可以使用JNI定义Java和原生代码之间的函数接口,并在Java代码中调用这些接口。同时,JNI还提供了一些函数来处理Java对象和原生数据类型之间的转换。简单来说就是JNI相当于JAVA和C/C++之间的翻译官,不管是JAVA转C/C++,还是C/C++转JAVA都需要依靠于JNI进行桥接、转换。
Java的保密性相对于C/C++来说并不算安全,有的开发者为了安全就也会去调用C/C++的代码来增加其项目的安全性,想要调用C/C++代码就需要通过jni接口调用,而要使用jni接口那就需要配置NDK。在配置NDK之前需要创建一个项目,调用C/C++代码建议创建Native C++项目类型。创建Native C++项目类型的项目可以参考以下方式创建:
一、在向导的 Choose your project 部分中,选择 Native C++ 项目类型:
image-20230806152953679.png (87.64 KB, 下载次数: 0)
下载附件
2025-1-24 15:01 上传
二、填写向导下一部分中的所有其他字段:
image-20230806153250744.png (41.45 KB, 下载次数: 0)
下载附件
2025-1-24 15:02 上传
Minimum SDK选项选择您希望应用支持的最低 API 级别。当您选择较低的 API 级别时,您的应用可以使用的现代 Android API 会更少,但能够运行应用的 Android 设备的比例会更大。当选择较高的 API 级别时,情况正好相反。
三、自定义C++支持:
image-20230806154555878.png (29.04 KB, 下载次数: 0)
下载附件
2025-1-24 15:03 上传
使用下拉列表选择您想要使用哪种 C++ 标准化。选择 Toolchain Default,将使用默认的 CMake 设置。
最后点击Finish,项目创建成功。
一、Android Studio NDK的安装与配置
如需在 Android Studio 中安装 CMake 和默认 NDK,请执行以下操作:
image-20230729203004376.png (121.77 KB, 下载次数: 0)
下载附件
2025-1-24 15:03 上传
点击 OK。
此时系统会显示一个对话框,告诉您 NDK 软件包占用了多少磁盘空间。
点击 OK。
安装完成后,点击 Finish。
您的项目会自动同步 build 文件并执行构建。修正发生的所有错误。
Android Studio 会将所有版本的 NDK 安装在 android-sdk/ndk/ 目录中,我们需要将NDK安装目录添加到PATH环境变量中,NDK安装目录如:D:\AndroidStudio\SDK\ndk\25.2.9519653\build
二、Android Studio使用Native C++项目类型JNI开发
静态注册
我们安装好了NDK下面就开始体验JNI开发之静态注册:
刚开始的时候我不知道是不是Android Studio环境的问题,还是什么问题,每次创建好项目都会报错,如:“No matching variant of com.android.tools.build:gradle:7.4.0 was found. The consumer was configured to find a runtime of a library compatible with Java 8, packaged as a jar, and its dependencies declared externally, as well as attribute 'org.gradle.plugin.api-version' with value '7.5' but:......”像这样的报错信息,后面我才知道这个其实就是jdk的版本不符的问题,解决方法可以参考以下文章:
(32条消息) 解决Android Studio-jdk版本不符问题(No matching variant of com.android.tools.build:gradle:7.4.0 was found.)_冯_诺伊曼的废柴Jason的博客-CSDN博客
我去改变JDK的版本,准确来说并不是JDK11,而是需要JDK17及JDK17以上的版本才不会报这个错误。如果没有这个错误就可以继续使用擅长的JDK版本进行jni开发。
现在我们使用Native C++项目类型进行静态注册,在具体讲之前先讲一下静态注册的流程:
第一步:在Java层使用native修饰符声明一个C/C++的函数;
第二步:Java层调用C/C++的函数;
第三步:生成.c/.cpp文件,并在文件内编写C/C++函数;
第四步:在java代码中添加静态代码块加载指定名称的共享库。
接下来正式讲解JNI开发就需要使用支持C/C++类型的Native C++项目。
我们先在Java层使用native修饰符声明C/C++的函数,然后调用声明的C/C++层函数:
package com.example.as_jni_project;
import androidx.appcompat.app.AppCompatActivity;
import android.annotation.SuppressLint;
import android.content.Context;
import android.os.Bundle;
import android.widget.TextView;
import android.widget.Toast;
import com.example.as_jni_project.databinding.ActivityMainBinding;
public class MainActivity extends AppCompatActivity {
// 在应用程序启动时用于加载'as_jni_project'库。
static {
System.loadLibrary("as_jni_project"); // 加载模块名称
}
private ActivityMainBinding binding;
public String str = "Hello JAVA!我是普通字段";
public static String static_str = "Hello JAVA!我是静态字段";
public static AppCompatActivity your_this = null;
public void str_method() {
Toast.makeText(this, "普通方法", Toast.LENGTH_LONG).show();
}
public static void static_method() {
Toast.makeText(your_this, "静态方法", Toast.LENGTH_LONG).show();
}
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
your_this = MainActivity.this;
binding = ActivityMainBinding.inflate(getLayoutInflater());
setContentView(binding.getRoot());
// 调用本地方法的示例
TextView tv = binding.sampleText;
tv.setText(stringFromJNI());
Toast.makeText(this, stringFromJAVA(), Toast.LENGTH_LONG).show();
Toast.makeText(this, stringFromC(), Toast.LENGTH_LONG).show();
Toast.makeText(this, staticFromC(), Toast.LENGTH_LONG).show();
stringFromMethod();
staticFromMethod();
}
/**
* 由“as_jni_project”本地库实现的本地方法,该库已打包到该应用程序中。
*/
public native String stringFromJNI(); // 这个是Native C++类型项目创建时自带的C/C++方法声明,我就没有删除
public native String stringFromJAVA();
public native String stringFromC();
public native String staticFromC();
public native String stringFromMethod();
public native String staticFromMethod();
}
我的想法是来完整体验一下从JAVA层通过jni调用C/C++层函数,以及从C/C++层调用JAVA层函数,所以我打算做以下几件事:
1、从JAVA层通过jni调用C/C++层函数
2、从JAVA层通过jni调用C/C++层函数,然后从C/C++层修改JAVA层普通字段的值
3、从JAVA层通过jni调用C/C++层函数,然后从C/C++层修改JAVA层静态字段的值
4、从JAVA层通过jni调用C/C++层函数,然后从C/C++层调用JAVA层普通方法
5、从JAVA层通过jni调用C/C++层函数,然后从C/C++层调用JAVA层静态方法
在MainActivity.java文件中我添加了以下代码:
在java代码中添加静态代码块加载指定名称模块:
static {
System.loadLibrary("as_jni_project"); // 加载模块名称
}
那我们该怎么知道我们要加载的模块名称是什么呢?在native-lib.cpp文件的同一级文件夹下的CMakeLists.txt文件中有定义:
# 有关将 CMake 与 Android Studio 配合使用的更多信息,请阅读文档:
# https://d.android.com/studio/projects/add-native-code.html
# 设置生成本地库所需的最低 CMake 版本。
cmake_minimum_required(VERSION 3.22.1)
# 声明并命名项目。
project("as_jni_project")
# 创建并命名库,将其设置为 STATIC 或 SHARED,并提供其源代码的相对路径。
# 您可以定义多个库,CMake 会为您构建它们。
# Gradle 会自动将共享库与您的 APK 打包。
add_library(
# 设置库的名称。
as_jni_project
# 将库设置为共享库。
SHARED
# 提供源文件的相对路径。
native-lib.cpp)
# 搜索指定的预生成库并将路径存储为变量。
# 由于 CMake 默认在搜索路径中包含系统库,因此您只需指定要添加的公有 NDK 库的名称。
# CMake 会在完成构建之前验证库是否存在。
find_library( # Sets the name of the path variable.
log-lib
# Specifies the name of the NDK library that
# you want CMake to locate.
log)
# 指定 CMake 应链接到目标库的库。
# 可以链接多个库,例如在此生成脚本中定义的库、预生成的第三方库或系统库。
target_link_libraries( # Specifies the target library.
as_jni_project
# Links the target library to the log library
# included in the NDK.
${log-lib})
可以从CMakeLists.txt文件中的定义看出,将native-lib.cpp文件设置为共享库,库的名称为as_jni_project。所以我们在指定加载模块名称时需要设置为as_jni_project。
从JAVA层通过jni调用C/C++层函数:
Toast.makeText(this, stringFromJAVA(), Toast.LENGTH_LONG).show();
// 通过使用消息框显示C/C++层函数stringFromJAVA()返回回来的值
public native String stringFromJAVA(); // 在Java层使用native修饰符声明一个C/C++层函数stringFromJAVA
从JAVA层通过jni调用C/C++层函数,然后从C/C++层调用JAVA层普通字段:
public String str = "Hello JAVA!我是普通字段"; // 声明一个Java层String类型的普通变量
Toast.makeText(this, stringFromC(), Toast.LENGTH_LONG).show();
// 通过使用消息框显示C/C++层函数stringFromC()返回回来的值,该值是在C/C++层函数从Java层调用普通字段的值
public native String stringFromC(); // 在Java层使用native修饰符声明一个C/C++层函数stringFromC
从JAVA层通过jni调用C/C++层函数,然后从C/C++层调用JAVA层静态字段:
public static String static_str = "Hello JAVA!我是静态字段"; // 声明一个Java层String类型的静态变量
Toast.makeText(this, staticFromC(), Toast.LENGTH_LONG).show();
// 通过使用消息框显示C/C++层函数staticFromC()返回回来的值,该值是在C/C++层函数从Java层调用静态字段的值
public native String staticFromC(); // 在Java层使用native修饰符声明一个C/C++层函数staticFromC
从JAVA层通过jni调用C/C++层函数,然后从C/C++层调用JAVA层普通方法:
// 声明一个Java层无返回值的普通方法,通过C/C++函数调用该方法会弹出消息框显示"普通方法"
public void str_method() {
Toast.makeText(this, "普通方法", Toast.LENGTH_LONG).show();
}
stringFromMethod(); // 在Java层调用C/C++层函数stringFromMethod
public native String stringFromMethod(); // 在Java层使用native修饰符声明一个C/C++层函数stringFromMethod
从JAVA层通过jni调用C/C++层函数,然后从C/C++层调用JAVA层静态方法:
// 声明一个AppCompatActivity类型(MainActivity的父类)的静态变量用来接收this,因为static_method方法是静态方法,而静态方法内部不可以出现this关键字。
public static AppCompatActivity your_this;
// 声明一个Java层无返回值的静态方法,通过C/C++函数调用该方法会弹出消息框显示"静态方法"
public static void static_method() {
// Toast.makeText()方法的第一个参数应该存放this,但是因为该方法是一个静态方法,所以该方法是属于类的,而非实例的。
// 因为静态方法是属于类的,所以静态方法会和类一同创建;而this只能代表当前对象,所以导致静态方法中不可以出现this关键字。
Toast.makeText(your_this, "静态方法", Toast.LENGTH_LONG).show();
}
// 在Activity生命周期的onCreata()方法进行初始化时,将MainActivity.this赋值给AppCompatActivity类型的静态变量,这样就会优先创建this,这样做或许可以解决this关键字无法出现在静态方法中,让我们可以正常弹出消息框。
your_this = MainActivity.this;
staticFromMethod(); // 在Java层调用C/C++层函数staticFromMethod
public native String staticFromMethod(); // 在Java层使用native修饰符声明一个C/C++层函数staticFromMethod
除去我添加的这些代码,其余代码都是Native C++类型项目创建时自带的代码。
写完了Java层的代码,我们接下来去详细介绍.cpp文件:
#include // jni.h是Java Native Interface(JNI)的头文件,用于提供JNI函数和数据类型的定义
#include // string是C++中的标准库头文件,用于操作字符串
extern "C" JNIEXPORT jstring JNICALL
Java_com_example_as_1jni_1project_MainActivity_stringFromJNI(
JNIEnv* env,
jobject /* this */) {
std::string hello = "Hello from C++";
return env->NewStringUTF(hello.c_str());
}
// extern "C"表示下面的代码使用的是C的编译方式,如果是文件后缀名为.c的C环境,那是不需要加的,因为那已经是C的编译方式了。
extern "C"
// JNIEXPORT是JNI重要标记关键字,主要作用是标记该方法让其可以被外部调用
// JNICALL 修饰符的作用就是告诉编译器使用JNI规范定义的调用约定来处理函数
JNIEXPORT jstring JNICALL
// Java_com_example_as_1jni_1project_MainActivity_stringFromJAVA表示该JNI函数的命名规则
// Java_包名_类名_方法名,如果包名、类名、方法名中自带_(下划线),那么在JNI函数的命名规则中会用_1表示该下划线是Java的下划线
Java_com_example_as_1jni_1project_MainActivity_stringFromJAVA(JNIEnv *env, jobject thiz) {
// JNIEnv *env 是一个指向JNI环境的指针,它提供了一系列的函数,用于在Java代码和C代码之间进行交互。
// jobject thiz 是一个表示当前对象的引用,在JNI中,jobject thiz 通常用于表示调用当前JNI函数的Java对象。
// 举个例子:stringFromJAVA函数是在MainActivity.onCreate方法中被调用,那么jobject类型thiz参数则表示MainActivity.this。
// TODO: implement stringFromJAVA()
}
extern "C"
JNIEXPORT jstring JNICALL
Java_com_example_as_1jni_1project_MainActivity_stringFromC(JNIEnv *env, jobject thiz) {
// TODO: implement stringFromC()
}
extern "C"
JNIEXPORT jstring JNICALL
Java_com_example_as_1jni_1project_MainActivity_staticFromC(JNIEnv *env, jobject thiz) {
// TODO: implement staticFromC()
}
extern "C"
JNIEXPORT jstring JNICALL
Java_com_example_as_1jni_1project_MainActivity_stringFromMethod(JNIEnv *env, jobject thiz) {
// TODO: implement stringFromMethod()
}
extern "C"
JNIEXPORT jstring JNICALL
Java_com_example_as_1jni_1project_MainActivity_staticFromMethod(JNIEnv *env, jobject thiz) {
// TODO: implement staticFromMethod()
}
定义了两个C/C++层函数,其中的一些关键字、修饰符、函数的命名规则我都用注释简单说明了一下,但在这些代码中,我认为有一行代码需要详细讲解:
// extern "C"表示下面的代码使用的是C的编译方式,如果是文件后缀名为.c的C环境,那是不需要加的,因为那已经是C的编译方式了。
extern "C"
可能有的朋友会很好奇,这一行代码有啥必要详细讲解的?这一行代码为什么要详细讲解这还得从JNI的核心JNIEnv开始讲起:
因为JNI的核心就是JNIEnv,而JNIEnv的核心则是在jni.h文件中使用C语言结构体定义的三百多个C语言函数。在Android Studio可以按住CTRL键点击要跳转的类、方法可以跳转到目标位置,按住CTRL键点击JNIEnv后跳转到以下位置:
image-20230804180643312.png (117.18 KB, 下载次数: 0)
下载附件
2025-1-24 15:05 上传
主要看这段代码:
#if defined(__cplusplus)
typedef _JNIEnv JNIEnv;
typedef _JavaVM JavaVM;
#else
typedef const struct JNINativeInterface* JNIEnv;
typedef const struct JNIInvokeInterface* JavaVM;
#endif
这里进行了一个判断,判断是使用C还是C++,有人可能好奇怎么进行判断,很简单,看写C/C++层代码的文件是.c(C语言源文件)还是.cpp(C++源文件),如果是.cpp那就走这段代码:
typedef _JNIEnv JNIEnv;
typedef _JavaVM JavaVM;
如果是.c那就走另一段代码:
typedef const struct JNINativeInterface* JNIEnv;
typedef const struct JNIInvokeInterface* JavaVM;
因为我们写C/C++层代码的文件是以.cpp为后缀的代码文件,所以我们讲解以下后缀名为.cpp时执行的代码:
typedef _JNIEnv JNIEnv; 将 _JNIEnv类型定义一个别名,该别名为JNIEnv类型。
typedef _JavaVM JavaVM; 将 _JavaVM 类型定义一个别名,该别名为 JavaVM 类型。
但是我们的主角是JNIEnv,而JNIEnv类型是_JNIEnv类型的别名,那我们去看看_JNIEnv类型到底是何方神圣:
image-20230804184551634.png (134.24 KB, 下载次数: 0)
下载附件
2025-1-24 15:06 上传
可以看到_JNIEnv类型是一个结构体,而这个结构体当中有一个比较眼熟的身影,JNINativeInterface是不是在哪里见过,没错就是前面判断是C还是C++的时候,如果写C/C++层代码的文件是.c的时候会去执行的代码:
typedef const struct JNINativeInterface* JNIEnv;
我们点进去看看:
image-20230804185418885.png (144.39 KB, 下载次数: 0)
下载附件
2025-1-24 15:07 上传
这个是什么呢?这个就是jni.h文件中使用C语言结构体定义的三百多个C语言函数,不管是Java调用C/C++函数,还是C/C++调用Java函数,都需要通过调用jni.h文件中的这三百多个C语言函数去调用。
但是要注意一个点,C++结构体_JNIEnv是一个对象的包装器,通常覆盖在C结构体JNINativeInterface上。这意味着_JNIEnv结构体中的第一个成员变量将是指向JNINativeInterface结构体的指针。这种关系表明_JNIEnv结构体是对JNINativeInterface结构体的封装。
看到这里你会发现无论是C还是C++都会访问JNINativeInterface结构体,而这个结构体是C语言的结构体,这个结构体有三百多个C语言函数,而这三百多个C语言函数必须采用C的编译方式。结构流程图大致是这样的:
image-20230806183356930.png (30.63 KB, 下载次数: 0)
下载附件
2025-1-24 15:07 上传
现在再来看看extern "C"这行代码,是不是就明白为什么表示下面的代码使用的是C的编译方式了。
讲完了extern "C"这行代码,我们继续讲解.cpp文件:
我们先从stringFromJAVA函数写起,我们要在这个函数中实现在C/C++层返回一个字符串给Java层,那需要在stringFromJAVA函数中添加以下代码:
return env->NewStringUTF("第一个native层返回的字符串!!!");
聪明的你应该发现这行.cpp文件中的代码和之前简单体验JNI开发时在.c文件中写的有点不一样,这是之前写的:
return (*env)-> NewStringUTF(env, "Hello From JNITest Function(getJNIString)");
那为什么会出现这种差别呢?这还得从jni.h文件说起了,可能有的朋友因为没有安装Android Studio没法查看jni.h文件,这种情况可以去访问以下网址:
jdk/src/java.base/share/native/include/jni.h at master · openjdk/jdk · GitHub
首先先说结论:导致这个差别的原因是因为如果是.c文件,那么JNIEnv env是二级指针,如果是.cpp文件,那么JNIEnv env是一级指针。有些朋友可能看完结论还是一脸懵逼,但是没关系,我们慢慢道来:
还记得在讲解extern "C"时出现的这个条件判断语句吗?没错,这次还是跟它有关系:
#if defined(__cplusplus)
typedef _JNIEnv JNIEnv;
typedef _JavaVM JavaVM;
#else
typedef const struct JNINativeInterface* JNIEnv;
typedef const struct JNIInvokeInterface* JavaVM;
#endif
之前我们追过去看了,无论是C还是C++实际都定义在 JNINativeInterface 结构体中,最终都会指向JNINativeInterface结构体。而在这个过程中就产生了差别,要明白为什么需要先简单了解一下C中的指针是什么,如果你有兴趣去了解C语言的指针,可以去阅读以下这篇文章:
c语言指针用法及实际应用详解,通俗易懂超详细! - 知乎 (zhihu.com)
如果没兴趣了解C语言的指针,那我在这个简单的说一下指针和指针变量。
指针是一种概念,所谓的指针其实就是数据的存储地址,指针可以方便CPU访问数据。
而指针变量它是指针这个概念的具体应用之一,指针变量和普通变量不同,指针变量它只能存储数据地址。
之前我们在C/C++层经常可以看到*出现,而*在C语言中可以用来获取指针变量指向那个内存地址的数据,比如:
result = *ptr;
也可以用来定义一个指针变量,比如:
int *ptr;
现在应该对指针有一点了解了吧!那好我们看如果native-lib文件的后缀名为.cpp时JNIEnv是怎么样的:
typedef _JNIEnv JNIEnv;
之前讲过这行代码将 _JNIEnv类型定义一个别名,该别名为JNIEnv类型,_JNIEnv 结构体是对 JNINativeInterface 结构体的封装,通过这种方式可以访问 JNINativeInterface 结构体中的函数指针( JNINativeInterface 结构体中定义的C语言函数),只需要调用_JNIEnv结构体中的方法即可。当后缀名为.cpp时,可以通过 _JNIEnv 类型的对象来执行 _JNIEnv 结构体中的函数,这些函数提供了与 Java Native Interface (JNI) 相关的功能,用于在 C++ 代码中与 Java 代码进行交互。
我们再看看文件后缀名为.cpp时的C++环境中JNIEnv env 参数,可以发现,C++环境中所谓的JNIEnv env 参数就是先给_JNIEnv结构体定义了一个别名为JNIEnv类型,然后再使用JNIEnv类型定义一个指针变量env,最后我们使用env可以直接调用 _JNIEnv 结构体中的函数,将_JNIEnv 结构体当做一个对象使用。回顾整个过程可以发现只有在JNIEnv *env 参数这定义了一个指针变量,所以文件后缀名为.cpp时,JNIEnv *env参数是一级指针。
想要调用一级指针下的函数,那需要使用到->。在 C++ 中,-> 是一个成员访问运算符,用于通过指针访问对象的成员函数。所以这就是如果文件后缀名为.cpp时为什么是env->函数名称 , 即可完成调用的原因。
那如果是C环境,为什么是二级指针呢?我们看else分支下的 JNIEnv 类型:
typedef const struct JNINativeInterface* JNIEnv;
这里JNINativeInterface*是 JNINativeInterface 结构体指针类型。可以看到将 JNINativeInterface 结构体指针类型定义了一个别名,该别名为JNIEnv类型。那么这里的JNIEnv类型已经是一级指针了,然后又在JNIEnv *env 参数这定义了一个指针变量,那这里的env参数就已经是二级指针了。
在 C 语言中,-> 运算符用于访问结构体指针的成员。当我们有一个指向结构体的指针时,可以使用 -> 运算符来访问该结构体的成员变量或成员函数。所以我们想要使用->访问JNINativeInterface 结构体下的成员变量或成员函数时,那么C环境下的JNIEnv env 参数就必须是一级指针,要将二级指针变为一级指针就需要解引用。在 C 语言中,`()是解引用运算符的语法。解引用运算符用于访问指针所指向的对象。当我们有一个指针时,可以使用(*)` 运算符来获取该指针所指向的对象。
那么在C环境中我们需要使用 (*env) 的形式来解引用 env 指针,然后再使用 -> 运算符来访问 JNIEnv 结构体中的成员函数。所以这就是如果文件后缀名为.c时为什么是(*env)->函数指针 , 即可完成调用的原因。
对比C环境中访问 JNIEnv 结构体中的成员函数和C++环境中访问 JNIEnv 结构体中的成员函数你可以发现它们之间还有一个差别,那就是C环境中访问 JNIEnv 结构体中的成员函数比C++环境中访问 JNIEnv 结构体中的成员函数多一个参数env。出现这个差别的原因也很简单,这是因为C是没有对象的,想要持有env环境,那就必须将JNIEnv *env 参数传递进去。而C++不需要将JNIEnv *env 参数传递进去是因为C++是有对象的本来就会持有env环境,所以不需要传。
为什么要详细讲这些呢?详细讲这些感觉作用不大。我这里详细讲这些是因为我们主要是要搞逆向,如果想在逆向途中如鱼得水,那就需要对开发有一些基础了解,但对开发的了解又不能在于表面,需要知道一些机制的具体原因,这样才能帮我们减轻阻碍。
写完了stringFromJAVA函数,接下来我们写stringFromC函数,我们要在这个函数中实现从C/C++层获取到JAVA层普通字段后修改该字段的值。既然我们要修改JAVA层字段的值,那么我们需要用到这个函数:
void SetObjectField(jobject obj, jfieldID fieldID, jobject value)
看函数名就很直白的告诉我们这个函数是用来修改Object类型字段用的,我们要传入三个参数:
SetObjectField函数的jobject obj参数:Java对象的引用。这里想要SetObjectField函数修改字段的值,那你就得告诉它这个字段所属的实例对象是谁。之前讲过JNI函数的jobject thiz参数是表示当前对象的引用,用于表示调用当前JNI函数的Java对象。那当前JNI函数的Java对象是谁呢?是MainActivity.this,而我们要修改字段的所属的实例对象是谁呢?也是MainActivity.this,所以jobject obj参数传入中传入jobject thiz参数。
SetObjectField函数的jfieldID fieldID参数:要修改的字段的ID。字段的ID是一个jfieldID类型的变量,它是用于表示一个Java字段在JNI环境中的唯一标识符。
我们现在是缺少jfieldID类型的变量,所以我们需要去创建一个jfieldID类型的变量去获取要修改的字段的ID。那么我们需要用到这个函数:
jfieldID GetFieldID(jclass clazz, const char* name, const char* sig)
其实这些函数名都很直白的把函数的作用告诉了我们,这个函数就很直白的告诉我们这是用来获取字段的ID用的,但是这里也要传入三个参数:
GetFieldID函数的jclass clazz参数:Java类的Class对象。Java类的Class对象是一个jclass类型的变量,它是用于表示一个Java类在JNI环境中的引用。
jclass clazz参数我们是缺少的,因为我们没有一个jclass类型的变量,所以我们又需要去创建一个jclass类型的变量。那么我们需要用到这个函数:
jclass FindClass(const char* name)
这个函数是第一种获取Java类的Class对象的方式,之后会讲第二种。我们来看这个函数唯一的参数const char* name要传入什么值:
FindClass函数的const char* name参数:要查找的Java类的名称,是一个字符串。
这次总算不要再为了这个参数去创建一些其他的变量了,这个参数我们需要将Java类的名称以字符串类型传递进去,格式是"包名/类名",并且要注意包名的.要换成/才行,比如:"com/example/as_jni_project/MainActivity"。我们再使用一个jclass类型的变量去接收FindClass函数返回的jclass类型的值:
jclass clazz = env->FindClass("com/example/as_jni_project/MainActivity");
这样我们就创建了一个jclass类型的变量,我们再将这个jclass类型的变量传递给GetFieldID函数的jclass clazz参数。
GetFieldID函数的const char* name参数:字段的名称。字段的名称是一个字符串,它表示要获取的字段的名称。我们要修改的字段的名称是str,那直接填写"str"就可以了。
GetFieldID函数的const char* sig参数:字段的签名。字段的签名是一个字符串,它表示要获取的字段的类型。我们要修改的字段的类型是String类型,是一个引用类型,所以需要填写"Ljava/lang/String;"。
通过观察字段的签名可以发现这里的签名规则和smali的签名规则是一样的:
[table]
[tr]
JNI类型[/td]
java类型[/td]
注释[/td]
[/tr]
[tr]
[td]V