掌握 Android JNI 基础

写在前面

最近在看一些底层源码,发现 JNI 这块还是有必要系统的看一下,索性就写一写博客,加深加深印象🍻

本文重点聊一聊一些干货,避免长篇大论

JNI 概述

JNI 是什么?

定义:Java Native Interface ,即 Java 本地接口 作用:使得 Java 与本地其他类型语言(如 C、C++ )进行交互

注意:

  • JNI 是 Java 调用 Native 语言的一种特性

  • JNI 是属于 Java 的,与 Android 平台无直接关系

以 Java 8 为例,JNI 最新在线 API: https://docs.oracle.com/javase/8/docs/technotes/guides/jni/spec/jniTOC.html

为什么会有 JNI ?

实际的使用中,Java 需要与本地代码进行交互,因为 Java 项目具备跨平台的特点,所以 Java 与本地代码交互的能力非常弱,采用 JNI 特性,增强 Java 与本地代码交互的能力

JNI 和 NDK 的关系

JNI 是 Java 平台提供的一套非常强大的框架 Java Native Interface,用于与本地代码进行相互调用

NDK 是 Android 平台提供的 Native 开发工具集,Native Development Kit的缩写。NDK 其中包含了 JNI 并对其进行了封装

关于 JNI 的入口头文件会有两份,分别在 JDK 和 NDK 中,进一步说明 Android 的 NDK 对 JDK 的 JNI 进行了二次封装

常见所在目录:

  • JDK: JAVA_HOME/include/jni.h

  • NDK: ~/Library/Android/sdk/ndk/26.1.10909125/toolchains/llvm/prebuilt/darwin-x86_64/sysroot/usr/include/jni.h

环境准备

  • Android Studio 版本:Android Studio Hedgehog | 2023.1.1 Patch 1

  • Gradle 版本:gradle-8.0

  • targetSdk:33

Android Studio 安装相关工具:

创建示例

File -> New,选择 Native++ 模板:

检查 NDK 相关配置是否正常:

native-lib.cpp C++ 示例代码:

MainActivity.java 示例代码:

CMake 构建

在 Android 开发中,CMake 用于编译 C/C++ 代码。从 Android Studio 2.2 版本开始,Google 引入了对 CMake 的支持,使得开发者可以通过 CMake 和 NDK 将 C/C++ 代码编译成底层的库,然后再配合 Gradle 的编译将库打包到 APK 中

CMake 具体的配置信息如下:

# cmake最低版本要求
cmake_minimum_required(VERSION 3.22.1)

# 配置库生成路径
# CMAKE_CURRENT_SOURCE_DIR是指 cmake库的源路径,通常是build/.../cmake/
# /../jniLibs/是指与CMakeList.txt所在目录的同级目录:jniLibs (如果没有会新建)
# ANDROID_ABI 生成库文件时,采用gradle配置的ABI策略(即:生成哪些平台对应的库文件)
set(CMAKE_LIBRARY_OUTPUT_DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR}/../jniLibs/${ANDROID_ABI})

# 添加库
add_library( # 库名
        native-lib

        # 类型:
        # SHARED 是指动态库,对应的是.so文件
        # STATIC 是指静态库,对应的是.a文件
        # 其他类型:略
        SHARED

        # native类路径
        native-lib.cpp)

# 查找依赖库
find_library(
        # 依赖库别名
        log-lib

        # 希望加到本地的NDK库名称,log指NDK的日志库
        log)


# 链接库,建立关系( 此处就是指把log-lib 链接给native-lib使用 )
target_link_libraries(
        # 目标库名称(native-lib就是咱们要生成的so库)
        native-lib

        # 要链接的库(上面查找的log库)
        ${log-lib})

摘自: https://www.cnblogs.com/qixingchao/p/11911787.html

默认 so 的目录(注意 AS 版本,不同版本不一致):

安装包反编译 so 路径位置:

示例解读

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

extern "C" JNIEXPORT jstring

JNICALL
Java_org_lulu_jnilearning_MainActivity_stringFromJNI(
        JNIEnv *env,
        jobject /* this */) {
    std::string hello = "这是 C++ 中的代码";
    char * str = "这是 C++ 中的代码";

    return env->NewStringUTF(str);
}
  • extern "C"避免按照 C++ 的方式去编译 C 函数

    为什么需要使用它呢? 这是因为 C++ 支持函数重载,所以编译器在编译函数时会将函数的参数类型也加到编译后的代码中,而不仅仅是函数名。而 C 语言并不支持函数重载,因此编译 C 语言代码的函数时不会带上函数的参数类型,一般只包括函数名

  • JNIEXPORT :用来表示该函数是否可导出(宏定义)

  • JNICALL:(可以缺少)jni call 约束函数入栈顺序和堆栈内存清理规则,在 Linux 中置空

  • jstring:代表 Java 中的 String

  • JNIEnv:C 和 Java 相互调用的桥梁,代表 Java 环境,内部包含了众多函数(后续详细介绍)

  • jobject:( Java 侧声明的 native 方法为非静态,传递时为 jobject)Java 传递过来的示例对象,即当前 Java 类的对象,示例中 MainActivity.this 就是它

  • jclass:( Java 侧声明的 native 方法为静态,传递时为 jclass)Java 传递过来的类对象,即当前 Java 类的 Class 对象,示例中 MainActivity.class 就是它

JNIEnv

C 和 Java 相互调用的桥梁,代表 Java 环境

通过 JNIEnv 就可以对 Java 端的代码进行操作:

  • 创建 Java 对象

  • 调用 Java 对象方法

  • 获取 Java 对象的属性

  • ...

C++ 中 JNIEnv 指向_JNIEnv,而_JNIEnv是定义的一个结构体,包裹了 JNINativeInterface,而在 C 中JNIEnv就是 JNINativeInterface

常用的方法:

函数名称 作用
NewObject 创建Java类中的对象
NewString 创建Java类中的String对象
NewArray 创建类型为Type的数组对象
GetField 获得类型为Type的字段
SetField 设置类型为Type的字段
GetStaticField 获得类型为Type的static的字段
SetStaticField 设置类型为Type的static的字段
CallMethod 调用返回值类型为Type的static方法
CallStaticMethod 调用返回值类型为Type的static方法
FindClass 通过类路径获取 Java 类的类对象
GetObjectClass 通过类对象获取 Java 类的类对象

Java、JNI、C/C++ 基本类型映射

Java 、C/C++都有一些常用的数据类型,分别是如何与JNI类型对应的呢?做以下整理:

JNI中定义的别名 Java类型 C/C++类型
jint / jsize int int
jshort short short
jlong long long / long long (__int64)
jbyte byte signed char
jboolean boolean unsigned char
jchar char unsigned short
jfloat float float
jdouble double double
jobject Object _jobject*

JNI描述符(签名)

JNI 开发时,我们除了写本地C/C++实现,还可以通过JNIEnv *env 调用 Java 层代码,如获得某个字段、获取某个函数、执行某个函数等:

//获得某类中定义的字段id
jfieldID GetFieldID(jclass clazz, const char* name, const char* sig)
    { return functions->GetFieldID(this, clazz, name, sig); }

//获得某类中定义的函数id
jmethodID GetMethodID(jclass clazz, const char* name, const char* sig)
    { return functions->GetMethodID(this, clazz, name, sig); }

以上函数和 Java 的反射比较类似,参数说明:

  • clazz:类的 class 对象

  • name:字段名称、函数名称

  • sig:字段描述符(字段签名),函数描述符(函数签名)

特别的,对 sig 进行解释:

  1. 如果是字段,表示字段类型的描述符

  2. 如果是函数,表示函数结构的描述符,即:每个参数类型描述符 + 返回值类型描述符

后面会结合实际案例进一步说明

整理字段类型签名:

Java类型 字段描述符(签名) 备注
int I int 的首字母、大写
float F float 的首字母、大写
double D double 的首字母、大写
short S short 的首字母、大写
long L long 的首字母、大写
char C char 的首字母、大写
byte B byte 的首字母、大写
boolean Z 因 B 已被 byte 使用,所以 JNI 规定使用 Z
object L + /分隔完整类名 String 如: Ljava/lang/String
array [ + 类型描述符 int[] 如:[I

整理函数类型签名:

Java函数 函数描述符(签名) 备注
void V 无返回值类型
Method (参数字段描述符...)返回值字段描述符 int add(int a,int b) 如:(II)I

如何通过指令获取当前 class 的签名:

找到此目录: 执行以下命令:

javap -s -p MainActivity.class 

相关描述符信息如下:

Compiled from "MainActivity.kt"
public final class org.lulu.jnilearning.MainActivity extends androidx.appcompat.app.AppCompatActivity {
  //...
  private java.lang.String nonStaticField;
    descriptor: Ljava/lang/String;
  private static java.lang.String staticField;
    descriptor: Ljava/lang/String;
    //...
}

Java和C++交互示例

我们以从 Native 侧修改 Java 侧非静态字段和静态字段为例,编写一段简单 Java 和 C++ 交互的示例:

修改非静态字段:

MainActivity.kt

private const val TAG = "MainActivity"

class MainActivity : AppCompatActivity() {
    /**
     * 待修改的非静态字段
     */
    private var nonStaticField = "Java 非静态字段"

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
  //...
        Log.d(TAG, "changeNonStaticField before: $nonStaticField")
        changeNonStaticField()
        Log.d(TAG, "changeNonStaticField after: $nonStaticField")
    }

 /**
     * 修改非静态字段的 Native 方法
     */
    external fun changeNonStaticField()

    companion object {
        // Used to load the 'jnilearning' library on application startup.
        init {
            System.loadLibrary("jnilearning")
        }
    }
    //...
}

native-lib.cpp

extern "C"
JNIEXPORT void JNICALL
Java_org_lulu_jnilearning_MainActivity_changeNonStaticField(JNIEnv *env, jobject thiz) {
    //1. 获取当前实例的类对象,即 MainActivity.class
    //   有两种方式
    // 1.1 jclass FindClass(const char* name)
    //jclass clazz = env->FindClass("org/lulu/jnilearning/MainActivity");
    // 1.2 【推荐】jclass GetObjectClass(jobject obj)
    jclass clazz = env->GetObjectClass(thiz);
    //2. 获取要修改的非静态字段 Id
    //jfieldID GetFieldID(jclass clazz, const char* name, const char* sig)
    jfieldID nonStaticFieldId = env->GetFieldID(clazz, "nonStaticField", "Ljava/lang/String;");
    //3. 创建一个新的 jstring,准备赋值
    //jstring NewStringUTF(const char* bytes)
    jstring jstr = env->NewStringUTF("Java 非静态字段,C++ 已修改");
    //4. 这是这个新的 jstring 给 nonStaticField 字段
    //void SetObjectField(jobject obj, jfieldID fieldID, jobject value)
    env->SetObjectField(thiz, nonStaticFieldId, jstr);
}

代码执行结果如下:

changeNonStaticField before: Java 非静态字段
changeNonStaticField after: Java 非静态字段,C++ 已修改

静态变量的修改类似,仅贴出C++代码:

extern "C"
JNIEXPORT void JNICALL
Java_org_lulu_jnilearning_MainActivity_changeStaticField(JNIEnv *env, jobject thiz) {
    //1. 获取当前实例的类对象,即 MainActivity.class
    //   有两种方式
    // 1.1 jclass FindClass(const char* name)
    //jclass clazz = env->FindClass("org/lulu/jnilearning/MainActivity");
    // 1.2 【推荐】jclass GetObjectClass(jobject obj)
    jclass clazz = env->GetObjectClass(thiz);
    //2. 获取要修改的静态字段 Id
    //jfieldID GetStaticFieldID(jclass clazz, const char* name, const char* sig)
    jfieldID staticFieldId = env->GetStaticFieldID(clazz, "staticField", "Ljava/lang/String;");
    //3. 创建一个新的 jstring,准备赋值
    //jstring NewStringUTF(const char* bytes)
    jstring jstr = env->NewStringUTF("Java 静态字段,C++ 已修改");
    //4. 这是这个新的 jstring 给 staticField 字段
    //void SetStaticObjectField(jclass clazz, jfieldID fieldID, jobject value)
    env->SetStaticObjectField(clazz, staticFieldId, jstr);
}

最后

这篇文章就到这里了,希望大家能够学到一些有用的知识,也欢迎你们在评论区留言交流。如果你觉得这篇文章有趣或者有帮助,不妨给我点个赞或者分享给你的朋友。感谢你们的阅读,我们下次再见!

相关推荐

  1. 掌握Jenknis基础概念

    2024-01-31 07:42:03       34 阅读
  2. 基础掌握Kafka

    2024-01-31 07:42:03       12 阅读
  3. 快速掌握TS基础知识

    2024-01-31 07:42:03       34 阅读
  4. fpga 需要掌握哪些基础知识?

    2024-01-31 07:42:03       27 阅读
  5. 【DevOps基础篇】Dockerfile快速掌握

    2024-01-31 07:42:03       21 阅读
  6. Lua-掌握Lua语言基础1

    2024-01-31 07:42:03       16 阅读
  7. 前端需要掌握的 mysql 基础知识

    2024-01-31 07:42:03       23 阅读

最近更新

  1. TCP协议是安全的吗?

    2024-01-31 07:42:03       18 阅读
  2. 阿里云服务器执行yum,一直下载docker-ce-stable失败

    2024-01-31 07:42:03       19 阅读
  3. 【Python教程】压缩PDF文件大小

    2024-01-31 07:42:03       19 阅读
  4. 通过文章id递归查询所有评论(xml)

    2024-01-31 07:42:03       20 阅读

热门阅读

  1. 在 C# 中 checked 和 unchecked 关键字

    2024-01-31 07:42:03       30 阅读
  2. MyBatis-Plus同款ES ORM框架

    2024-01-31 07:42:03       37 阅读
  3. 面试问答总结之ElasticSearch

    2024-01-31 07:42:03       42 阅读
  4. centos7常用命令之安装插件2

    2024-01-31 07:42:03       40 阅读
  5. react列表拖拽排序组件

    2024-01-31 07:42:03       36 阅读
  6. 2024 高级前端面试题之 React 「精选篇」

    2024-01-31 07:42:03       48 阅读
  7. 创建型模式之单例模式

    2024-01-31 07:42:03       33 阅读